diff --git a/app/client/src/entities/Datasource/RestAPIForm.ts b/app/client/src/entities/Datasource/RestAPIForm.ts index 90fb5777ba..a945925433 100644 --- a/app/client/src/entities/Datasource/RestAPIForm.ts +++ b/app/client/src/entities/Datasource/RestAPIForm.ts @@ -8,6 +8,11 @@ export enum AuthType { bearerToken = "bearerToken", } +export enum SSLType { + DEFAULT = "DEFAULT", + SELF_SIGNED_CERTIFICATE = "SELF_SIGNED_CERTIFICATE", +} + export enum ApiKeyAuthType { QueryParams = "queryParams", Header = "header", @@ -25,6 +30,20 @@ export type Authentication = | ApiKey | BearerToken; +export interface Connection { + ssl: SSL; +} + +export interface SSL { + authType: SSLType; + certificateFile: Certificate; +} + +export interface Certificate { + name: string; + base64Content: string | ArrayBuffer | null; +} + export interface ApiDatasourceForm { datasourceId: string; pluginId: string; @@ -37,6 +56,7 @@ export interface ApiDatasourceForm { sessionSignatureKey: string; authType: AuthType; authentication?: Authentication; + connection?: Connection; } export interface Oauth2Common { diff --git a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx index da21c462fc..2e8f62b692 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx @@ -38,6 +38,7 @@ import { ApiKeyAuthType, AuthType, GrantType, + SSLType, } from "entities/Datasource/RestAPIForm"; import { createMessage, @@ -479,10 +480,48 @@ class DatasourceRestAPIEditor extends React.Component { )} {this.renderAuthFields()} + + {this.renderDropdownControlViaFormControl( + "connection.ssl.authType", + [ + { + label: "No", + value: "DEFAULT", + }, + { + label: "Yes", + value: "SELF_SIGNED_CERTIFICATE", + }, + ], + "Use Self-signed certificate", + "", + true, + "", + "DEFAULT", + )} + + {this.renderSelfSignedCertificateFields()} ); }; + renderSelfSignedCertificateFields = () => { + const { connection } = this.props.formData; + if (connection?.ssl.authType === SSLType.SELF_SIGNED_CERTIFICATE) { + return ( + + {this.renderFilePickerControlViaFormControl( + "connection.ssl.certificateFile", + "Upload Certificate", + "", + false, + true, + )} + + ); + } + }; + renderAuthFields = () => { const { authType } = this.props.formData; @@ -954,6 +993,7 @@ class DatasourceRestAPIEditor extends React.Component { placeholderText: string, isRequired: boolean, subtitle?: string, + initialValue?: any, ) { const config = { id: "", @@ -967,6 +1007,7 @@ class DatasourceRestAPIEditor extends React.Component { conditionals: {}, placeholderText: placeholderText, formName: DATASOURCE_REST_API_FORM, + initialValue: initialValue, }; return ( { /> ); } + + renderFilePickerControlViaFormControl( + configProperty: string, + label: string, + placeholderText: string, + isRequired: boolean, + encrypted: boolean, + ) { + const config = { + id: "", + configProperty: configProperty, + isValid: false, + controlType: "FILE_PICKER", + placeholderText: placeholderText, + encrypted: encrypted, + label: label, + conditionals: {}, + formName: DATASOURCE_REST_API_FORM, + isRequired: isRequired, + }; + return ( + + ); + } } const mapStateToProps = (state: AppState, props: any) => { diff --git a/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts b/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts index d1744fba2a..912f2781b8 100644 --- a/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts +++ b/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts @@ -11,6 +11,7 @@ import { Basic, ApiKey, BearerToken, + SSLType, } from "entities/Datasource/RestAPIForm"; import _ from "lodash"; @@ -22,6 +23,11 @@ export const datasourceToFormValues = ( "datasourceConfiguration.authentication.authenticationType", AuthType.NONE, ); + const connection = _.get(datasource, "datasourceConfiguration.connection", { + ssl: { + authType: SSLType.DEFAULT, + }, + }); const authentication = datasourceToFormAuthentication(authType, datasource); const isSendSessionEnabled = _.get(datasource, "datasourceConfiguration.properties[0].value", "N") === @@ -43,6 +49,7 @@ export const datasourceToFormValues = ( sessionSignatureKey: sessionSignatureKey, authType: authType, authentication: authentication, + connection: connection, }; }; @@ -69,6 +76,7 @@ export const formValuesToDatasource = ( { key: "sessionSignatureKey", value: form.sessionSignatureKey }, ], authentication: authentication, + connection: form.connection, }, } as Datasource; }; diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/ExceptionHelper.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/ExceptionHelper.java new file mode 100644 index 0000000000..138f848959 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/ExceptionHelper.java @@ -0,0 +1,14 @@ +package com.appsmith.external.helpers; + +public class ExceptionHelper { + + public static Throwable getRootCause(Throwable e) { + Throwable cause = null; + Throwable result = e; + + while (null != (cause = result.getCause()) && (result != cause)) { + result = cause; + } + return result; + } +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SSLHelper.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SSLHelper.java new file mode 100644 index 0000000000..586926aa24 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SSLHelper.java @@ -0,0 +1,53 @@ +package com.appsmith.external.helpers; + +import com.appsmith.external.models.UploadedFile; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +public class SSLHelper { + + private static final String X_509_TYPE = "X.509"; + private static final String CERT_ALIAS = "caCert"; + private static final String SSL_PROTOCOL = "TLS"; + + public static SSLContext getSslContext(UploadedFile certificate) + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, KeyManagementException { + + final TrustManagerFactory trustManagerFactory = getSslTrustManagerFactory(certificate); + + SSLContext sslContext = SSLContext.getInstance(SSL_PROTOCOL); + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + + return sslContext; + } + + public static TrustManagerFactory getSslTrustManagerFactory(UploadedFile certificate) + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + InputStream certificateIs = + new ByteArrayInputStream(certificate.getDecodedContent()); + CertificateFactory certificateFactory = CertificateFactory.getInstance(X_509_TYPE); + X509Certificate caCertificate = + (X509Certificate) certificateFactory.generateCertificate(certificateIs); + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + keyStore.setCertificateEntry(CERT_ALIAS, caCertificate); + + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + + return trustManagerFactory; + } +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java index 7a20ec36c6..acb6868b72 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java @@ -2,6 +2,7 @@ package com.appsmith.external.models; import com.appsmith.external.exceptions.BaseException; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.helpers.ExceptionHelper; import com.appsmith.external.plugins.AppsmithPluginErrorUtils; import com.fasterxml.jackson.databind.JsonNode; import lombok.Getter; @@ -56,6 +57,6 @@ public class ActionExecutionResult { } public void setErrorInfo(Throwable error) { - this.setErrorInfo(error, null); + this.setErrorInfo(ExceptionHelper.getRootCause(error), null); } } diff --git a/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/utils/SSLUtils.java b/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/utils/SSLUtils.java index a76a8d92f3..1b58ca7a77 100644 --- a/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/utils/SSLUtils.java +++ b/app/server/appsmith-plugins/arangoDBPlugin/src/main/java/com/external/utils/SSLUtils.java @@ -2,53 +2,20 @@ package com.external.utils; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.helpers.SSLHelper; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.SSLDetails; import com.arangodb.ArangoDB.Builder; import org.pf4j.util.StringUtils; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.security.KeyManagementException; -import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; public class SSLUtils { - private static final String X_509_TYPE = "X.509"; - private static final String CERT_ALIAS = "caCert"; - private static final String SSL_PROTOCOL = "TLS"; - - public static SSLContext getSslContext(DatasourceConfiguration datasourceConfiguration) throws CertificateException - , KeyStoreException, IOException, NoSuchAlgorithmException, KeyManagementException { - InputStream certificateIs = - new ByteArrayInputStream(datasourceConfiguration.getConnection().getSsl() - .getCaCertificateFile().getDecodedContent()); - CertificateFactory certificateFactory = CertificateFactory.getInstance(X_509_TYPE); - X509Certificate caCertificate = - (X509Certificate) certificateFactory.generateCertificate(certificateIs); - - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(null); - keyStore.setCertificateEntry(CERT_ALIAS, caCertificate); - - TrustManagerFactory trustManagerFactory = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(keyStore); - - SSLContext sslContext = SSLContext.getInstance(SSL_PROTOCOL); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - - return sslContext; - } - public static boolean isCaCertificateAvailable(DatasourceConfiguration datasourceConfiguration) { if (datasourceConfiguration.getConnection() != null && datasourceConfiguration.getConnection().getSsl() != null @@ -97,7 +64,7 @@ public class SSLUtils { case FILE: case BASE64_STRING: try { - builder.sslContext(getSslContext(datasourceConfiguration)); + builder.sslContext(SSLHelper.getSslContext(datasourceConfiguration.getConnection().getSsl().getCaCertificateFile())); } catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException | KeyManagementException e) { throw new AppsmithPluginException( diff --git a/app/server/appsmith-plugins/restApiPlugin/pom.xml b/app/server/appsmith-plugins/restApiPlugin/pom.xml index 6f1c2990bc..39fbc72066 100644 --- a/app/server/appsmith-plugins/restApiPlugin/pom.xml +++ b/app/server/appsmith-plugins/restApiPlugin/pom.xml @@ -134,6 +134,11 @@ 5.2.3.RELEASE test + + io.projectreactor.netty + reactor-netty-http + provided + diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java index 4057fed20c..718041c4ad 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java @@ -5,6 +5,7 @@ import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.helpers.DataTypeStringUtils; import com.appsmith.external.helpers.MustacheHelper; +import com.appsmith.external.helpers.SSLHelper; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionRequest; import com.appsmith.external.models.ActionExecutionResult; @@ -13,6 +14,8 @@ import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.PaginationField; import com.appsmith.external.models.PaginationType; import com.appsmith.external.models.Property; +import com.appsmith.external.models.SSLDetails; +import com.appsmith.external.models.UploadedFile; import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.external.plugins.SmartSubstitutionInterface; @@ -40,6 +43,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.util.CollectionUtils; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.client.ClientResponse; @@ -48,6 +52,9 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.Exceptions; import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; +import reactor.netty.tcp.DefaultSslContextSpec; import reactor.util.function.Tuple2; import javax.crypto.SecretKey; @@ -59,6 +66,9 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.time.Instant; import java.util.ArrayList; import java.util.Date; @@ -281,7 +291,32 @@ public class RestApiPlugin extends BasePlugin { } // Initializing webClient to be used for http call - WebClient.Builder webClientBuilder = WebClient.builder(); + final ConnectionProvider provider = ConnectionProvider + .builder("rest-api-provider") + .build(); + + HttpClient httpClient = HttpClient.create(provider) + .secure(sslContextSpec -> { + + final DefaultSslContextSpec sslContextSpec1 = DefaultSslContextSpec.forClient(); + + if (datasourceConfiguration.getConnection() != null && + datasourceConfiguration.getConnection().getSsl() != null && + datasourceConfiguration.getConnection().getSsl().getAuthType() == SSLDetails.AuthType.SELF_SIGNED_CERTIFICATE) { + + sslContextSpec1.configure(sslContextBuilder -> { + try { + final UploadedFile certificateFile = datasourceConfiguration.getConnection().getSsl().getCertificateFile(); + sslContextBuilder.trustManager(SSLHelper.getSslTrustManagerFactory(certificateFile)); + } catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException e) { + e.printStackTrace(); + } + }); + } + sslContextSpec.sslContext(sslContextSpec1); + }); + + WebClient.Builder webClientBuilder = WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)); // Adding headers from datasource if (datasourceConfiguration.getHeaders() != null) {