diff --git a/engines/base/pom.xml b/engines/base/pom.xml index 9c7185b5..2a84b839 100644 --- a/engines/base/pom.xml +++ b/engines/base/pom.xml @@ -90,7 +90,6 @@ org.apache.httpcomponents httpclient - test org.apache.httpcomponents diff --git a/engines/base/src/main/java/org/alfresco/transform/base/WebClientBuilderAdjuster.java b/engines/base/src/main/java/org/alfresco/transform/base/WebClientBuilderAdjuster.java new file mode 100644 index 00000000..1147ee67 --- /dev/null +++ b/engines/base/src/main/java/org/alfresco/transform/base/WebClientBuilderAdjuster.java @@ -0,0 +1,16 @@ +/* + * Copyright 2015-2023 Alfresco Software, Ltd. All rights reserved. + * + * License rights for this program may be obtained from Alfresco Software, Ltd. + * pursuant to a written agreement and any use of this program without such an + * agreement is prohibited. + */ +package org.alfresco.transform.base; + +import org.springframework.web.reactive.function.client.WebClient; + +@FunctionalInterface +public interface WebClientBuilderAdjuster +{ + void adjust(WebClient.Builder builder); +} diff --git a/engines/base/src/main/java/org/alfresco/transform/base/config/MTLSConfig.java b/engines/base/src/main/java/org/alfresco/transform/base/config/MTLSConfig.java new file mode 100644 index 00000000..12ed0761 --- /dev/null +++ b/engines/base/src/main/java/org/alfresco/transform/base/config/MTLSConfig.java @@ -0,0 +1,196 @@ +/* + * #%L + * Alfresco Transform Core + * %% + * Copyright (C) 2005 - 2023 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.transform.base.config; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import org.alfresco.transform.base.WebClientBuilderAdjuster; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManagerFactory; +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.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +@Configuration +public class MTLSConfig { + + @Value("${client.ssl.key-store:#{null}}") + private Resource keyStoreResource; + + @Value("${client.ssl.key-store-password:}") + private char[] keyStorePassword; + + @Value("${client.ssl.key-store-type:}") + private String keyStoreType; + + @Value("${client.ssl.trust-store:#{null}}") + private Resource trustStoreResource; + + @Value("${client.ssl.trust-store-password:}") + private char[] trustStorePassword; + + @Value("${client.ssl.trust-store-type:}") + private String trustStoreType; + + @Bean + public WebClientBuilderAdjuster webClientBuilderAdjuster(SslContextBuilder nettySslContextBuilder) + { + return builder -> { + if(isTlsOrMtlsConfigured()) + { + HttpClient httpClientWithSslContext = null; + try { + httpClientWithSslContext = createHttpClientWithSslContext(nettySslContextBuilder); + } catch (SSLException e) { + throw new RuntimeException(e); + } + builder.clientConnector(new ReactorClientHttpConnector(httpClientWithSslContext)); + } + }; + } + + @Bean + public RestTemplate restTemplate(SSLContextBuilder apacheSSLContextBuilder) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, UnrecoverableKeyException + { + if(isTlsOrMtlsConfigured()) + { + return createRestTemplateWithSslContext(apacheSSLContextBuilder); + } else { + return new RestTemplate(); + } + } + + @Bean + public SSLContextBuilder apacheSSLContextBuilder() throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, UnrecoverableKeyException { + SSLContextBuilder sslContextBuilder = new SSLContextBuilder(); + if(isKeystoreConfigured()) + { + KeyStore keyStore = getKeyStore(keyStoreType, keyStoreResource, keyStorePassword); + sslContextBuilder.loadKeyMaterial(keyStore, keyStorePassword); + } + if(isTruststoreConfigured()) + { + sslContextBuilder.loadTrustMaterial(trustStoreResource.getURL(), trustStorePassword); + } + + return sslContextBuilder; + } + + @Bean + public SslContextBuilder nettySslContextBuilder() throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); + if(isKeystoreConfigured()) + { + KeyManagerFactory keyManagerFactory = initKeyManagerFactory(); + sslContextBuilder.keyManager(keyManagerFactory); + } + + if(isTruststoreConfigured()) + { + TrustManagerFactory trustManagerFactory = initTrustManagerFactory(); + sslContextBuilder.trustManager(trustManagerFactory); + } + + return sslContextBuilder; + } + + private boolean isTlsOrMtlsConfigured() + { + return isTruststoreConfigured() || isKeystoreConfigured(); + } + + private boolean isTruststoreConfigured() + { + return trustStoreResource != null; + } + + private boolean isKeystoreConfigured() + { + return keyStoreResource != null; + } + + private HttpClient createHttpClientWithSslContext(SslContextBuilder sslContextBuilder) throws SSLException { + SslContext sslContext = sslContextBuilder.build(); + return HttpClient.create().secure(p -> p.sslContext(sslContext)); + } + + private RestTemplate createRestTemplateWithSslContext(SSLContextBuilder sslContextBuilder) throws NoSuchAlgorithmException, KeyManagementException { + SSLContext sslContext = sslContextBuilder.build(); + SSLConnectionSocketFactory sslContextFactory = new SSLConnectionSocketFactory(sslContext); + CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslContextFactory).build(); + ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + return new RestTemplate(requestFactory); + } + + private KeyStore getKeyStore(String keyStoreType, Resource keyStoreResource, char[] keyStorePassword) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException + { + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + try (InputStream keyStoreInputStream = keyStoreResource.getInputStream()) + { + keyStore.load(keyStoreInputStream, keyStorePassword); + } + return keyStore; + } + + private TrustManagerFactory initTrustManagerFactory() throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException + { + KeyStore trustStore = getKeyStore(trustStoreType, trustStoreResource, trustStorePassword); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + return trustManagerFactory; + } + + private KeyManagerFactory initKeyManagerFactory() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException + { + KeyStore clientKeyStore = getKeyStore(keyStoreType, keyStoreResource, keyStorePassword); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(clientKeyStore, keyStorePassword); + return keyManagerFactory; + } +} diff --git a/engines/base/src/main/java/org/alfresco/transform/base/config/WebApplicationConfig.java b/engines/base/src/main/java/org/alfresco/transform/base/config/WebApplicationConfig.java index 4ffb51e3..57f61390 100644 --- a/engines/base/src/main/java/org/alfresco/transform/base/config/WebApplicationConfig.java +++ b/engines/base/src/main/java/org/alfresco/transform/base/config/WebApplicationConfig.java @@ -35,7 +35,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; -import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -64,12 +63,6 @@ public class WebApplicationConfig implements WebMvcConfigurer .addPathPatterns(ENDPOINT_TRANSFORM, "/live", "/ready"); } - @Bean - public RestTemplate restTemplate() - { - return new RestTemplate(); - } - @Bean public TransformRequestValidator transformRequestValidator() { diff --git a/engines/base/src/main/java/org/alfresco/transform/base/sfs/SharedFileStoreClient.java b/engines/base/src/main/java/org/alfresco/transform/base/sfs/SharedFileStoreClient.java index 2772a35a..b54f07ef 100644 --- a/engines/base/src/main/java/org/alfresco/transform/base/sfs/SharedFileStoreClient.java +++ b/engines/base/src/main/java/org/alfresco/transform/base/sfs/SharedFileStoreClient.java @@ -2,7 +2,7 @@ * #%L * Alfresco Transform Core * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * - @@ -34,6 +34,7 @@ import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; import java.io.File; +import org.alfresco.transform.base.WebClientBuilderAdjuster; import org.alfresco.transform.exceptions.TransformException; import org.alfresco.transform.base.model.FileRefResponse; import org.slf4j.Logger; @@ -53,6 +54,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; import javax.annotation.PostConstruct; +import javax.net.ssl.SSLException; /** * Simple Rest client that call Alfresco Shared File Store @@ -68,12 +70,16 @@ public class SharedFileStoreClient @Autowired private RestTemplate restTemplate; + @Autowired + private WebClientBuilderAdjuster adjuster; + private WebClient client; @PostConstruct - public void init() - { - client = WebClient.builder().baseUrl(url.endsWith("/") ? url : url + "/") + public void init() throws SSLException { + final WebClient.Builder clientBuilder = WebClient.builder(); + adjuster.adjust(clientBuilder); + client = clientBuilder.baseUrl(url.endsWith("/") ? url : url + "/") .defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) .defaultHeader(ACCEPT, APPLICATION_JSON_VALUE) .build(); diff --git a/engines/base/src/test/java/org/alfresco/transform/base/MtlsTestUtils.java b/engines/base/src/test/java/org/alfresco/transform/base/MtlsTestUtils.java new file mode 100644 index 00000000..3f7f5fdf --- /dev/null +++ b/engines/base/src/test/java/org/alfresco/transform/base/MtlsTestUtils.java @@ -0,0 +1,76 @@ +package org.alfresco.transform.base; + +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +import javax.net.ssl.SSLContext; +import java.io.File; +import java.io.FileInputStream; +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.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +public class MtlsTestUtils { + + private static final boolean MTLS_ENABLED = Boolean.parseBoolean(System.getProperty("test-mtls-enabled")); + + public static boolean isMtlsEnabled() + { + return MTLS_ENABLED; + } + + public static CloseableHttpClient httpClientWithMtls() throws NoSuchAlgorithmException, KeyManagementException, UnrecoverableKeyException, KeyStoreException, IOException, CertificateException + { + String keyStoreFile = System.getProperty("test-client-keystore-file"); + String keyStoreType = System.getProperty("test-client-keystore-type"); + char[] keyStorePassword = System.getProperty("test-client-keystore-password").toCharArray(); + String trustStoreFile = System.getProperty("test-client-truststore-file"); + String trustStoreType = System.getProperty("test-client-truststore-type"); + char[] trustStorePassword = System.getProperty("test-client-truststore-password").toCharArray(); + + SSLContextBuilder sslContextBuilder = new SSLContextBuilder(); + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + try (InputStream keyStoreInputStream = new FileInputStream(keyStoreFile)) + { + keyStore.load(keyStoreInputStream, keyStorePassword); + sslContextBuilder.loadKeyMaterial(keyStore, keyStorePassword); + } + + File trustStore = new File(trustStoreFile); + sslContextBuilder.loadTrustMaterial(trustStore, trustStorePassword); + + SSLContext sslContext = sslContextBuilder.build(); + SSLConnectionSocketFactory sslContextFactory = new SSLConnectionSocketFactory(sslContext); + return HttpClients.custom().setSSLSocketFactory(sslContextFactory).build(); + } + + public static RestTemplate restTemplateWithMtls() + { + ClientHttpRequestFactory requestFactory = null; + try { + requestFactory = new HttpComponentsClientHttpRequestFactory(httpClientWithMtls()); + } catch (Exception e) { + e.printStackTrace(); + } + return new RestTemplate(requestFactory); + } + + public static RestTemplate getRestTemplate() + { + return MtlsTestUtils.isMtlsEnabled() ? MtlsTestUtils.restTemplateWithMtls() : new RestTemplate(); + } + + public static CloseableHttpClient getHttpClient() throws UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException { + return MtlsTestUtils.isMtlsEnabled() ? MtlsTestUtils.httpClientWithMtls() : HttpClients.createDefault(); + } +} diff --git a/engines/base/src/test/java/org/alfresco/transform/base/clients/HttpClient.java b/engines/base/src/test/java/org/alfresco/transform/base/clients/HttpClient.java index 842682e8..ab51b887 100644 --- a/engines/base/src/test/java/org/alfresco/transform/base/clients/HttpClient.java +++ b/engines/base/src/test/java/org/alfresco/transform/base/clients/HttpClient.java @@ -13,6 +13,7 @@ import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; import java.util.Map; +import org.alfresco.transform.base.MtlsTestUtils; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpEntity; @@ -27,8 +28,8 @@ import org.springframework.web.client.RestTemplate; */ public class HttpClient { - private static final RestTemplate REST_TEMPLATE = new RestTemplate(); - + private static final RestTemplate REST_TEMPLATE = MtlsTestUtils.getRestTemplate(); + public static ResponseEntity sendTRequest( final String engineUrl, final String sourceFile, final String sourceMimetype, final String targetMimetype, final String targetExtension) diff --git a/engines/base/src/test/java/org/alfresco/transform/base/clients/SfsClient.java b/engines/base/src/test/java/org/alfresco/transform/base/clients/SfsClient.java index 025c9ef0..f31db5da 100644 --- a/engines/base/src/test/java/org/alfresco/transform/base/clients/SfsClient.java +++ b/engines/base/src/test/java/org/alfresco/transform/base/clients/SfsClient.java @@ -21,6 +21,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import org.alfresco.transform.base.MtlsTestUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; @@ -31,7 +32,6 @@ import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.entity.mime.content.FileBody; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.slf4j.LoggerFactory; @@ -56,7 +56,7 @@ public class SfsClient ((Logger) LoggerFactory.getLogger("org.apache.http.wire")).setAdditive(false); } - private static final String SFS_BASE_URL = "http://localhost:8099"; + private static final String SFS_BASE_URL = MtlsTestUtils.isMtlsEnabled() ? "https://localhost:8099" : "http://localhost:8099"; public static String uploadFile(final String fileToUploadName) throws Exception { @@ -75,7 +75,7 @@ public class SfsClient .addPart("file", new FileBody(file, ContentType.DEFAULT_BINARY)) .build()); - try (CloseableHttpClient client = HttpClients.createDefault()) + try (CloseableHttpClient client = MtlsTestUtils.getHttpClient()) { final HttpResponse response = client.execute(post); int status = response.getStatusLine().getStatusCode(); @@ -134,7 +134,7 @@ public class SfsClient sfsBaseUrl+"/alfresco/api/-default-/private/sfs/versions/1/file/{0}", uuid)); - try (CloseableHttpClient client = HttpClients.createDefault()) + try (CloseableHttpClient client = MtlsTestUtils.getHttpClient()) { final HttpResponse response = client.execute(head); final int status = response.getStatusLine().getStatusCode(); @@ -153,7 +153,7 @@ public class SfsClient sfsBaseUrl+"/alfresco/api/-default-/private/sfs/versions/1/file/{0}", uuid)); - try (CloseableHttpClient client = HttpClients.createDefault()) + try (CloseableHttpClient client = MtlsTestUtils.getHttpClient()) { final HttpResponse response = client.execute(get); final int status = response.getStatusLine().getStatusCode();