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;