HTTPConnectionFactory.java
package emissary.client;
import emissary.config.ConfigUtil;
import emissary.config.Configurator;
import emissary.util.PkiUtil;
import com.google.common.annotations.VisibleForTesting;
import org.apache.hc.client5.http.impl.DefaultClientConnectionReuseStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.ConnectionReuseStrategy;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.SecureRandom;
import javax.annotation.Nullable;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
/**
* Emissary HTTP Connection Factory. This is a singleton class that allows for the central configuration of an Apache
* HTTP Client Connection manager and also provides a method for building default HTTP Clients. This object can be
* configured by providing an HTTPConnectionFactory.cfg with the following:<br>
*
* <pre>
* // Standard SSL Properties
* javax.net.ssl.trustStore = "[Path to trust store]"
* javax.net.ssl.trustStoreType = "[Trust Store type, defaults to JKS]"
* javax.net.ssl.trustStorePassword = "[Trust store password OR path to file, see below]"
* javax.net.ssl.keyStore = "[Path to key store]"
* javax.net.ssl.keyStoreType = "[Key Store type, defaults to JKS]"
* javax.net.ssl.keyStorePassword = "[Key store password OR path to file, see below]"
* </pre>
* <p>
* Password configs: For the key or trust store options, if the values are prepended with "file://", this class will
* attempt to load the password from the file on the path. This is intended to be a single line text file. This is
* provided to allow for passwords to be placed in limited access files and directories and to eliminate the need to
* pass these options in JVM System properties which are easily found.
*/
public class HTTPConnectionFactory {
static final String CFG_TRUST_STORE = "javax.net.ssl.trustStore";
static final String CFG_TRUST_STORE_TYPE = "javax.net.ssl.trustStoreType";
static final String CFG_TRUST_STORE_PW = "javax.net.ssl.trustStorePassword";
static final String CFG_KEY_STORE = "javax.net.ssl.keyStore";
static final String CFG_KEY_STORE_TYPE = "javax.net.ssl.keyStoreType";
static final String CFG_KEY_STORE_PW = "javax.net.ssl.keyStorePassword";
static final String CFG_HTTP_KEEPALIVE = "http.keepAlive";
static final String CFG_HTTP_MAXCONNS = "http.maxConnections";
static final String CFG_HTTP_AGENT = "http.agent";
static final String CFG_NOOP_VERIFIER = "https.useNoopHostnameVerifier";
static final String CFG_SSLCONTEXT_TYPE = "emissary.sslcontext.type";
static final String DEFAULT_HTTP_AGENT = "emissary";
static final int DFLT_MAXCONNS = 200;
static final boolean DFLT_KEEPALIVE = true;
static final String DFLT_STORE_TYPE = "JKS";
static final String DFLT_CONTEXT_TYPE = "TLS";
// meaningful constants
private static final String HTTP = "http";
private static final String HTTPS = "https";
private static final Logger log = Logger.getLogger(HTTPConnectionFactory.class);
// singleton
private static final HTTPConnectionFactory FACTORY = new HTTPConnectionFactory();
final PoolingHttpClientConnectionManager connMan;
private ConnectionReuseStrategy connReuseStrategy = DefaultClientConnectionReuseStrategy.INSTANCE;
int maxConns = DFLT_MAXCONNS;
String userAgent = DEFAULT_HTTP_AGENT;
private HTTPConnectionFactory() {
this(null);
}
@VisibleForTesting
HTTPConnectionFactory(@Nullable final Configurator config) {
Registry<ConnectionSocketFactory> registry = null;
try {
final Configurator cfg = config == null ? ConfigUtil.getConfigInfo(HTTPConnectionFactory.class) : config;
// if someone doesn't want keep alives...
if (!cfg.findBooleanEntry(CFG_HTTP_KEEPALIVE, DFLT_KEEPALIVE)) {
this.connReuseStrategy = (httpRequest, httpResponse, httpContext) -> false;
}
this.maxConns = cfg.findIntEntry(CFG_HTTP_MAXCONNS, DFLT_MAXCONNS);
this.userAgent = cfg.findStringEntry(CFG_HTTP_AGENT, DEFAULT_HTTP_AGENT);
final SSLContext sslContext = build(cfg);
// mainly for using in test environments where cert name may not match host name
final HostnameVerifier v = cfg.findBooleanEntry(CFG_NOOP_VERIFIER, false) ? new NoopHostnameVerifier() : new DefaultHostnameVerifier();
registry =
RegistryBuilder.<ConnectionSocketFactory>create().register(HTTP, PlainConnectionSocketFactory.getSocketFactory())
.register(HTTPS, new SSLConnectionSocketFactory(sslContext, v)).build();
} catch (IOException | GeneralSecurityException ex) {
log.error("Error configuring HTTPConnectionFactory. The connection factory will use HTTP Client default settings", ex);
}
if (registry == null) {
this.connMan = new PoolingHttpClientConnectionManager();
} else {
this.connMan = new PoolingHttpClientConnectionManager(registry);
}
this.connMan.setMaxTotal(this.maxConns);
}
/**
* This method will attempt to configure an SSLSocketFactory using configuration parameters from the
* HTTPConnectionFactory.cfg.
*
* @param cfg The configurator.
* @return the SSLContext
* @throws IOException If there is some I/O problem.
* @throws GeneralSecurityException If there is some security problem.
*/
SSLContext build(final Configurator cfg) throws IOException, GeneralSecurityException {
final char[] kpChar = PkiUtil.loadPassword(cfg.findStringEntry(CFG_KEY_STORE_PW));
final char[] tsChar = PkiUtil.loadPassword(cfg.findStringEntry(CFG_TRUST_STORE_PW));
final KeyStore keyStore =
PkiUtil.buildStore(cfg.findStringEntry(CFG_KEY_STORE), kpChar, cfg.findStringEntry(CFG_KEY_STORE_TYPE, DFLT_STORE_TYPE));
final KeyStore trustStore =
PkiUtil.buildStore(cfg.findStringEntry(CFG_TRUST_STORE), tsChar, cfg.findStringEntry(CFG_TRUST_STORE_TYPE, DFLT_STORE_TYPE));
if ((trustStore == null) && (keyStore == null)) {
log.debug("Trust Store and Key Store are null. Using JDK default SSLContext");
return SSLContext.getDefault();
}
final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, kpChar);
final SSLContext sc = SSLContext.getInstance(cfg.findStringEntry(CFG_SSLCONTEXT_TYPE, DFLT_CONTEXT_TYPE));
sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
return sc;
}
/**
* Return the configured connection manager with TLS SSL if configured.
*
* @return the connection manager
*/
public PoolingHttpClientConnectionManager getDefaultConnectionManager() {
return this.connMan;
}
/**
* Returns a CloseableHttpClient using the configuration options of the factory singleton. Detailed information:
* <ul>
* <li>The connection manager will be set
* <li>The Client will have the connection manager marked as shared to preserve cached connections
* <li>The Client will use the configured reuse strategy (HTTP Keep Alive)
* </ul>
*
* @return a CloseableHttpClient
*/
public CloseableHttpClient buildDefaultClient() {
return HttpClientBuilder.create().setConnectionManager(this.connMan).setConnectionManagerShared(true).setUserAgent(this.userAgent)
.setConnectionReuseStrategy(this.connReuseStrategy).build();
}
/**
* Returns the Factory
*
* @return the connection factory
*/
public static HTTPConnectionFactory getFactory() {
return FACTORY;
}
}