diff --git a/core/src/main/java/org/alfresco/httpclient/HttpClientFactory.java b/core/src/main/java/org/alfresco/httpclient/HttpClientFactory.java index 45a70358e4..5ad7649446 100644 --- a/core/src/main/java/org/alfresco/httpclient/HttpClientFactory.java +++ b/core/src/main/java/org/alfresco/httpclient/HttpClientFactory.java @@ -21,7 +21,6 @@ package org.alfresco.httpclient; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.security.AlgorithmParameters; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -32,14 +31,11 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import org.alfresco.encryption.AlfrescoKeyStore; import org.alfresco.encryption.AlfrescoKeyStoreImpl; import org.alfresco.encryption.EncryptionUtils; -import org.alfresco.encryption.Encryptor; -import org.alfresco.encryption.KeyProvider; import org.alfresco.encryption.KeyResourceLoader; import org.alfresco.encryption.KeyStoreParameters; import org.alfresco.encryption.ssl.AuthSSLProtocolSocketFactory; import org.alfresco.encryption.ssl.SSLEncryptionParameters; import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.util.Pair; import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; @@ -53,8 +49,6 @@ import org.apache.commons.httpclient.SimpleHttpConnectionManager; import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.apache.commons.httpclient.cookie.CookiePolicy; -import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; -import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.params.DefaultHttpParams; import org.apache.commons.httpclient.params.DefaultHttpParamsFactory; import org.apache.commons.httpclient.params.HttpClientParams; @@ -75,23 +69,25 @@ import org.apache.commons.logging.LogFactory; */ public class HttpClientFactory { + /** + * Communication type for HttpClient: + * - NONE is plain http + * - SECRET is plain http with a shared secret via request header + * - HTTPS is mTLS with client authentication (certificates are required) + */ public static enum SecureCommsType { - HTTPS, NONE; + HTTPS, NONE, SECRET; public static SecureCommsType getType(String type) { - if(type.equalsIgnoreCase("https")) + switch (type.toLowerCase()) { - return HTTPS; - } - else if(type.equalsIgnoreCase("none")) - { - return NONE; - } - else - { - throw new IllegalArgumentException("Invalid communications type"); + case "https": return HTTPS; + case "none": return NONE; + case "secret": return SECRET; + default: throw new IllegalArgumentException("Invalid communications type"); + } } }; @@ -122,14 +118,24 @@ public class HttpClientFactory private int connectionTimeout = 0; + // Shared secret parameters + private String sharedSecret; + private String sharedSecretHeader = DEFAULT_SHAREDSECRET_HEADER; + + // Default name for HTTP Request Header when using shared secret communication + public static final String DEFAULT_SHAREDSECRET_HEADER = "X-Alfresco-Search-Secret"; + public HttpClientFactory() { } - + + /** + * Default constructor for legacy subsystems. + */ public HttpClientFactory(SecureCommsType secureCommsType, SSLEncryptionParameters sslEncryptionParameters, - KeyResourceLoader keyResourceLoader, KeyStoreParameters keyStoreParameters, - MD5EncryptionParameters encryptionParameters, String host, int port, int sslPort, int maxTotalConnections, - int maxHostConnections, int socketTimeout) + KeyResourceLoader keyResourceLoader, KeyStoreParameters keyStoreParameters, + MD5EncryptionParameters encryptionParameters, String host, int port, int sslPort, + int maxTotalConnections, int maxHostConnections, int socketTimeout) { this.secureCommsType = secureCommsType; this.sslEncryptionParameters = sslEncryptionParameters; @@ -145,6 +151,21 @@ public class HttpClientFactory init(); } + /** + * Recommended constructor for subsystems supporting Shared Secret communication. + * This constructor supports Shared Secret ("secret") communication method additionally to the legacy ones: "none" and "https". + */ + public HttpClientFactory(SecureCommsType secureCommsType, SSLEncryptionParameters sslEncryptionParameters, + KeyResourceLoader keyResourceLoader, KeyStoreParameters keyStoreParameters, + MD5EncryptionParameters encryptionParameters, String sharedSecret, String sharedSecretHeader, + String host, int port, int sslPort, int maxTotalConnections, int maxHostConnections, int socketTimeout) + { + this(secureCommsType, sslEncryptionParameters, keyResourceLoader, keyStoreParameters, encryptionParameters, + host, port, sslPort, maxTotalConnections, maxHostConnections, socketTimeout); + this.sharedSecret = sharedSecret; + this.sharedSecretHeader = sharedSecretHeader; + } + public void init() { this.sslKeyStore = new AlfrescoKeyStoreImpl(sslEncryptionParameters.getKeyStoreParameters(), keyResourceLoader); @@ -272,10 +293,44 @@ public class HttpClientFactory this.connectionTimeout = connectionTimeout; } - protected HttpClient constructHttpClient() + /** + * Shared secret used for SECRET communication + * @param secret shared secret word + */ + public void setSharedSecret(String sharedSecret) + { + this.sharedSecret = sharedSecret; + } + + /** + * @return Shared secret used for SECRET communication + */ + public String getSharedSecret() + { + return sharedSecret; + } + + /** + * HTTP Request header used for SECRET communication + * @param sharedSecretHeader HTTP Request header + */ + public void setSharedSecretHeader(String sharedSecretHeader) + { + this.sharedSecretHeader = sharedSecretHeader; + } + + /** + * @return HTTP Request header used for SECRET communication + */ + public String getSharedSecretHeader() + { + return sharedSecretHeader; + } + + protected RequestHeadersHttpClient constructHttpClient() { MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager(); - HttpClient httpClient = new HttpClient(connectionManager); + RequestHeadersHttpClient httpClient = new RequestHeadersHttpClient(connectionManager); HttpClientParams params = httpClient.getParams(); params.setBooleanParameter(HttpConnectionParams.TCP_NODELAY, true); params.setBooleanParameter(HttpConnectionParams.STALE_CONNECTION_CHECK, true); @@ -291,15 +346,15 @@ public class HttpClientFactory return httpClient; } - protected HttpClient getHttpsClient() + protected RequestHeadersHttpClient getHttpsClient() { return getHttpsClient(host, sslPort); } - protected HttpClient getHttpsClient(String httpsHost, int httpsPort) + protected RequestHeadersHttpClient getHttpsClient(String httpsHost, int httpsPort) { // Configure a custom SSL socket factory that will enforce mutual authentication - HttpClient httpClient = constructHttpClient(); + RequestHeadersHttpClient httpClient = constructHttpClient(); // Default port is 443 for the HostFactory, when including customised port (like 8983) the port name is skipped from "getHostURL" string HttpHostFactory hostFactory = new HttpHostFactory(new Protocol("https", sslSocketFactory, HttpsURL.DEFAULT_PORT)); httpClient.setHostConfiguration(new HostConfigurationWithHostFactory(hostFactory)); @@ -307,28 +362,54 @@ public class HttpClientFactory return httpClient; } - protected HttpClient getDefaultHttpClient() + protected RequestHeadersHttpClient getDefaultHttpClient() { return getDefaultHttpClient(host, port); } - protected HttpClient getDefaultHttpClient(String httpHost, int httpPort) + protected RequestHeadersHttpClient getDefaultHttpClient(String httpHost, int httpPort) { - HttpClient httpClient = constructHttpClient(); + RequestHeadersHttpClient httpClient = constructHttpClient(); httpClient.getHostConfiguration().setHost(httpHost, httpPort); return httpClient; } + + /** + * Build HTTP Client using default headers + * @return RequestHeadersHttpClient including default header for shared secret method + */ + protected RequestHeadersHttpClient constructSharedSecretHttpClient() + { + RequestHeadersHttpClient client = constructHttpClient(); + client.setDefaultHeaders(Map.of(sharedSecretHeader, sharedSecret)); + return client; + } + + protected RequestHeadersHttpClient getSharedSecretHttpClient() + { + return getSharedSecretHttpClient(host, port); + } + + protected RequestHeadersHttpClient getSharedSecretHttpClient(String httpHost, int httpPort) + { + RequestHeadersHttpClient httpClient = constructSharedSecretHttpClient(); + httpClient.getHostConfiguration().setHost(httpHost, httpPort); + return httpClient; + } protected AlfrescoHttpClient getAlfrescoHttpsClient() { - AlfrescoHttpClient repoClient = new HttpsClient(getHttpsClient()); - return repoClient; + return new HttpsClient(getHttpsClient()); } protected AlfrescoHttpClient getAlfrescoHttpClient() { - AlfrescoHttpClient repoClient = new DefaultHttpClient(getDefaultHttpClient()); - return repoClient; + return new DefaultHttpClient(getDefaultHttpClient()); + } + + protected AlfrescoHttpClient getAlfrescoSharedSecretClient() + { + return new DefaultHttpClient(getSharedSecretHttpClient()); } protected HttpClient getMD5HttpClient(String host, int port) @@ -341,66 +422,37 @@ public class HttpClientFactory public AlfrescoHttpClient getRepoClient(String host, int port) { - AlfrescoHttpClient repoClient = null; - - if(secureCommsType == SecureCommsType.HTTPS) + switch (secureCommsType) { - repoClient = getAlfrescoHttpsClient(); + case HTTPS: return getAlfrescoHttpsClient(); + case NONE: return getAlfrescoHttpClient(); + case SECRET: return getAlfrescoSharedSecretClient(); + default: throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in [solr|alfresco].secureComms, should be 'ssl', 'none' or 'secret'"); } - else if(secureCommsType == SecureCommsType.NONE) + } + + public RequestHeadersHttpClient getHttpClient() + { + switch (secureCommsType) { - repoClient = getAlfrescoHttpClient(); + case HTTPS: return getHttpsClient(); + case NONE: return getDefaultHttpClient(); + case SECRET: return getSharedSecretHttpClient(); + default: throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in [solr|alfresco].secureComms, should be 'ssl', 'none' or 'secret'"); } - else - { - throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'"); - } - - return repoClient; } - public HttpClient getHttpClient() + public RequestHeadersHttpClient getHttpClient(String host, int port) { - HttpClient httpClient = null; - - if(secureCommsType == SecureCommsType.HTTPS) + switch (secureCommsType) { - httpClient = getHttpsClient(); + case HTTPS: return getHttpsClient(host, port); + case NONE: return getDefaultHttpClient(host, port); + case SECRET: return getSharedSecretHttpClient(host, port); + default: throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in [solr|alfresco].secureComms, should be 'ssl', 'none' or 'secret'"); } - else if(secureCommsType == SecureCommsType.NONE) - { - httpClient = getDefaultHttpClient(); - } - else - { - throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'"); - } - - return httpClient; } - public HttpClient getHttpClient(String host, int port) - { - HttpClient httpClient = null; - - if(secureCommsType == SecureCommsType.HTTPS) - { - httpClient = getHttpsClient(host, port); - } - else if(secureCommsType == SecureCommsType.NONE) - { - httpClient = getDefaultHttpClient(host, port); - } - else - { - throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'"); - } - - return httpClient; - } - - - /** * A secure client connection to the repository. * diff --git a/core/src/main/java/org/alfresco/httpclient/RequestHeadersHttpClient.java b/core/src/main/java/org/alfresco/httpclient/RequestHeadersHttpClient.java new file mode 100644 index 0000000000..78e5d36727 --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/RequestHeadersHttpClient.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2005-2021 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.httpclient; + +import java.io.IOException; +import java.util.Map; + +import org.apache.commons.httpclient.HostConfiguration; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.HttpState; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; + +/** + * Since Apache HttpClient 3.1 doesn't support including custom headers by default, + * this class is adding that custom headers every time a method is invoked. + */ +public class RequestHeadersHttpClient extends HttpClient +{ + + private Map defaultHeaders; + + public RequestHeadersHttpClient(MultiThreadedHttpConnectionManager connectionManager) + { + super(connectionManager); + } + + public Map getDefaultHeaders() + { + return defaultHeaders; + } + + public void setDefaultHeaders(Map defaultHeaders) + { + this.defaultHeaders = defaultHeaders; + } + + private void addDefaultHeaders(HttpMethod method) + { + if (defaultHeaders != null) + { + defaultHeaders.forEach((k,v) -> { + method.addRequestHeader(k, v); + }); + } + } + + @Override + public int executeMethod(HttpMethod method) throws IOException, HttpException + { + addDefaultHeaders(method); + return super.executeMethod(method); + } + + @Override + public int executeMethod(HostConfiguration hostConfiguration, HttpMethod method) throws IOException, HttpException + { + addDefaultHeaders(method); + return super.executeMethod(hostConfiguration, method); + } + + @Override + public int executeMethod(HostConfiguration hostconfig, HttpMethod method, HttpState state) + throws IOException, HttpException + { + addDefaultHeaders(method); + return super.executeMethod(hostconfig, method, state); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilter.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilter.java index c67b7c5beb..bbce3d1b86 100644 --- a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilter.java +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilter.java @@ -25,21 +25,18 @@ */ package org.alfresco.repo.web.scripts.solr; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.PrintWriter; import javax.servlet.FilterChain; import javax.servlet.ServletContext; import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.httpclient.HttpClientFactory; import org.alfresco.repo.web.filter.beans.DependencyInjectedFilter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -88,9 +85,7 @@ public class SOLRAuthenticationFilter implements DependencyInjectedFilter, Initi private String sharedSecret; - private String sharedSecretHeader = DEFAULT_SHAREDSECRET_HEADER; - - private static final String DEFAULT_SHAREDSECRET_HEADER = "X-Alfresco-Search-Secret"; + private String sharedSecretHeader = HttpClientFactory.DEFAULT_SHAREDSECRET_HEADER; public void setSecureComms(String type) { diff --git a/remote-api/src/main/java/org/alfresco/web/app/servlet/AlfrescoX509ServletFilter.java b/remote-api/src/main/java/org/alfresco/web/app/servlet/AlfrescoX509ServletFilter.java index f433c0dbf2..77399944b6 100644 --- a/remote-api/src/main/java/org/alfresco/web/app/servlet/AlfrescoX509ServletFilter.java +++ b/remote-api/src/main/java/org/alfresco/web/app/servlet/AlfrescoX509ServletFilter.java @@ -31,6 +31,7 @@ import java.util.Properties; import javax.servlet.ServletContext; +import org.alfresco.httpclient.HttpClientFactory.SecureCommsType; import org.alfresco.web.scripts.servlet.X509ServletFilterBase; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -70,7 +71,9 @@ public class AlfrescoX509ServletFilter extends X509ServletFilterBase * Return true or false based on the property. This will switch on/off X509 enforcement in the X509ServletFilterBase. */ - if (prop == null || "none".equals(prop)) + if (prop == null || + SecureCommsType.getType(prop) == SecureCommsType.NONE || + SecureCommsType.getType(prop) == SecureCommsType.SECRET) { return false; } diff --git a/repository/src/main/resources/alfresco/subsystems/Search/solr6/solr-search-context.xml b/repository/src/main/resources/alfresco/subsystems/Search/solr6/solr-search-context.xml index 5c5d83f415..8400c484a7 100644 --- a/repository/src/main/resources/alfresco/subsystems/Search/solr6/solr-search-context.xml +++ b/repository/src/main/resources/alfresco/subsystems/Search/solr6/solr-search-context.xml @@ -165,6 +165,8 @@ + + diff --git a/repository/src/test/java/org/alfresco/repo/search/impl/solr/SolrStoreMappingWrapperTest.java b/repository/src/test/java/org/alfresco/repo/search/impl/solr/SolrStoreMappingWrapperTest.java index db668a7951..e80af814e0 100644 --- a/repository/src/test/java/org/alfresco/repo/search/impl/solr/SolrStoreMappingWrapperTest.java +++ b/repository/src/test/java/org/alfresco/repo/search/impl/solr/SolrStoreMappingWrapperTest.java @@ -31,14 +31,16 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import java.io.UnsupportedEncodingException; + import org.alfresco.httpclient.HttpClientFactory; +import org.alfresco.httpclient.RequestHeadersHttpClient; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.util.Pair; import org.apache.commons.codec.net.URLCodec; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.protocol.Protocol; -import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -46,8 +48,6 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.beans.factory.BeanFactory; -import java.io.UnsupportedEncodingException; - /** * @author Andy * @@ -64,34 +64,34 @@ public class SolrStoreMappingWrapperTest HttpClientFactory httpClientFactory; @Mock - HttpClient httpClientCommon; + RequestHeadersHttpClient httpClientCommon; @Mock - HttpClient httpClient1; + RequestHeadersHttpClient httpClient1; @Mock - HttpClient httpClient2; + RequestHeadersHttpClient httpClient2; @Mock - HttpClient httpClient3; + RequestHeadersHttpClient httpClient3; @Mock - HttpClient httpClient4; + RequestHeadersHttpClient httpClient4; @Mock - HttpClient httpClient5; + RequestHeadersHttpClient httpClient5; @Mock - HttpClient httpClient6; + RequestHeadersHttpClient httpClient6; @Mock - HttpClient httpClient7; + RequestHeadersHttpClient httpClient7; @Mock - HttpClient httpClient8; + RequestHeadersHttpClient httpClient8; @Mock - HttpClient httpClient9; + RequestHeadersHttpClient httpClient9; @Mock HostConfiguration hostConfigurationCommon;