com.helger.httpclient.HttpClientFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.helger.httpclient.HttpClientFactory.java

Source

/**
 * Copyright (C) 2016-2018 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.httpclient;

import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;

import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.protocol.RequestAcceptEncoding;
import org.apache.http.client.protocol.RequestAddCookies;
import org.apache.http.client.protocol.ResponseContentEncoding;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.DnsResolver;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLInitializationException;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.conn.SystemDefaultDnsResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.random.RandomHelper;
import com.helger.commons.ws.HostnameVerifierVerifyAll;
import com.helger.commons.ws.TrustManagerTrustAll;
import com.helger.http.tls.ETLSVersion;
import com.helger.http.tls.ITLSConfigurationMode;
import com.helger.http.tls.TLSConfigurationMode;
import com.helger.httpclient.HttpClientRetryHandler.ERetryMode;

/**
 * A factory for creating {@link CloseableHttpClient} that is e.g. to be used in
 * the {@link HttpClientManager}.
 *
 * @author Philip Helger
 */
@NotThreadSafe
public class HttpClientFactory implements IHttpClientProvider {
    /**
     * Default configuration modes uses TLS 1.2, 1.1 or 1.0 and no specific cipher
     * suites
     */
    public static final ITLSConfigurationMode DEFAULT_TLS_CONFIG_MODE = new TLSConfigurationMode(
            new ETLSVersion[] { ETLSVersion.TLS_12, ETLSVersion.TLS_11, ETLSVersion.TLS_10 }, new String[0]);
    public static final boolean DEFAULT_USE_SYSTEM_PROPERTIES = false;
    public static final boolean DEFAULT_USE_DNS_CACHE = true;
    public static final int DEFAULT_RETRIES = 0;
    public static final ERetryMode DEFAULT_RETRY_MODE = ERetryMode.RETRY_IDEMPOTENT_ONLY;

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientFactory.class);

    private boolean m_bUseSystemProperties = DEFAULT_USE_SYSTEM_PROPERTIES;
    private boolean m_bUseDNSClientCache = DEFAULT_USE_DNS_CACHE;
    private SSLContext m_aSSLContext;
    private ITLSConfigurationMode m_aTLSConfigurationMode;
    private HostnameVerifier m_aHostnameVerifier;
    private HttpHost m_aProxy;
    private Credentials m_aProxyCredentials;
    private int m_nRetries = DEFAULT_RETRIES;
    private ERetryMode m_eRetryMode = DEFAULT_RETRY_MODE;

    /**
     * Default constructor.
     */
    public HttpClientFactory() {
    }

    /**
     * @return <code>true</code> if system properties for HTTP client should be
     *         used, <code>false</code> if not. Default is <code>false</code>.
     * @since 8.7.1
     */
    public final boolean isUseSystemProperties() {
        return m_bUseSystemProperties;
    }

    /**
     * Enable the usage of system properties in the HTTP client?<br>
     * Supported properties are (source:
     * http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/HttpClientBuilder.html):
     * <ul>
     * <li>ssl.TrustManagerFactory.algorithm</li>
     * <li>javax.net.ssl.trustStoreType</li>
     * <li>javax.net.ssl.trustStore</li>
     * <li>javax.net.ssl.trustStoreProvider</li>
     * <li>javax.net.ssl.trustStorePassword</li>
     * <li>ssl.KeyManagerFactory.algorithm</li>
     * <li>javax.net.ssl.keyStoreType</li>
     * <li>javax.net.ssl.keyStore</li>
     * <li>javax.net.ssl.keyStoreProvider</li>
     * <li>javax.net.ssl.keyStorePassword</li>
     * <li>https.protocols</li>
     * <li>https.cipherSuites</li>
     * <li>http.proxyHost</li>
     * <li>http.proxyPort</li>
     * <li>http.nonProxyHosts</li>
     * <li>http.keepAlive</li>
     * <li>http.maxConnections</li>
     * <li>http.agent</li>
     * </ul>
     *
     * @param bUseSystemProperties
     *        <code>true</code> if system properties should be used,
     *        <code>false</code> if not.
     * @return this for chaining
     * @since 8.7.1
     */
    @Nonnull
    public final HttpClientFactory setUseSystemProperties(final boolean bUseSystemProperties) {
        m_bUseSystemProperties = bUseSystemProperties;
        return this;
    }

    /**
     * @return <code>true</code> if DNS client caching is enabled (default),
     *         <code>false</code> if it is disabled.
     * @since 8.8.0
     */
    public final boolean isUseDNSClientCache() {
        return m_bUseDNSClientCache;
    }

    /**
     * Enable or disable DNS client caching. By default caching is enabled.
     *
     * @param bUseDNSClientCache
     *        <code>true</code> to use DNS caching, <code>false</code> to disable
     *        it.
     * @return this for chaining
     * @since 8.8.0
     */
    @Nonnull
    public final HttpClientFactory setUseDNSClientCache(final boolean bUseDNSClientCache) {
        m_bUseDNSClientCache = bUseDNSClientCache;
        return this;
    }

    /**
     * Create a custom SSLContext to use for the SSL Socket factory.
     *
     * @return <code>null</code> if no custom context is present.
     */
    @Nullable
    public final SSLContext getSSLContext() {
        return m_aSSLContext;
    }

    /**
     * Set the SSL Context to be used. By default no SSL context is present.
     *
     * @param aSSLContext
     *        The SSL context to be used. May be <code>null</code>.
     * @return this for chaining
     * @since 9.0.0
     */
    @Nonnull
    public final HttpClientFactory setSSLContext(@Nullable final SSLContext aSSLContext) {
        m_aSSLContext = aSSLContext;
        return this;
    }

    /**
     * Attention: INSECURE METHOD!<br>
     * Set the a special SSL Context that does not expect any specific server
     * certificate. To be totally loose, you should also set a hostname verifier
     * that accepts all hostnames.
     *
     * @return this for chaining
     * @throws GeneralSecurityException
     *         In case TLS initialization fails
     * @since 9.0.1
     */
    @Nonnull
    public final HttpClientFactory setSSLContextTrustAll() throws GeneralSecurityException {
        final SSLContext aSSLContext = SSLContext.getInstance("TLS");
        aSSLContext.init(null, new TrustManager[] { new TrustManagerTrustAll(false) },
                RandomHelper.getSecureRandom());
        return setSSLContext(aSSLContext);
    }

    /**
     * @return The current hostname verifier to be used. Default to
     *         <code>null</code>.
     * @since 8.8.2
     */
    @Nullable
    public final HostnameVerifier getHostnameVerifier() {
        return m_aHostnameVerifier;
    }

    /**
     * Set the hostname verifier to be used.
     *
     * @param aHostnameVerifier
     *        Verifier to be used. May be <code>null</code>.
     * @return this for chaining
     * @since 8.8.2
     */
    @Nonnull
    public final HttpClientFactory setHostnameVerifier(@Nullable final HostnameVerifier aHostnameVerifier) {
        m_aHostnameVerifier = aHostnameVerifier;
        return this;
    }

    /**
     * Attention: INSECURE METHOD!<br>
     * Set a hostname verifier that trusts all hostnames.
     *
     * @return this for chaining
     * @since 9.0.1
     */
    @Nonnull
    public final HttpClientFactory setHostnameVerifierVerifyAll() {
        return setHostnameVerifier(new HostnameVerifierVerifyAll(false));
    }

    /**
     * @return The TLS configuration mode to be used. <code>null</code> means to
     *         use the default settings without specific cipher suites.
     * @since 9.0.5
     */
    @Nullable
    public final ITLSConfigurationMode getTLSConfigurationMode() {
        return m_aTLSConfigurationMode;
    }

    /**
     * Set the TLS configuration mode to use.
     *
     * @param aTLSConfigurationMode
     *        The configuration mode to use. <code>null</code> means use system
     *        default.
     * @return this for chaining
     * @since 9.0.5
     */
    @Nonnull
    public final HttpClientFactory setTLSConfigurationMode(
            @Nullable final ITLSConfigurationMode aTLSConfigurationMode) {
        m_aTLSConfigurationMode = aTLSConfigurationMode;
        return this;
    }

    /**
     * @return The proxy host to be used. May be <code>null</code>.
     */
    @Nullable
    public final HttpHost getProxyHost() {
        return m_aProxy;
    }

    /**
     * @return The proxy server credentials to be used. May be <code>null</code>.
     */
    @Nullable
    public final Credentials getProxyCredentials() {
        return m_aProxyCredentials;
    }

    /**
     * Set a proxy host without proxy server credentials.
     *
     * @param aProxy
     *        The proxy host to be used. May be <code>null</code>.
     * @since 8.8.0
     * @see #setProxy(HttpHost, Credentials)
     */
    public final void setProxy(@Nullable final HttpHost aProxy) {
        setProxy(aProxy, (Credentials) null);
    }

    /**
     * Set proxy host and proxy credentials.
     *
     * @param aProxy
     *        The proxy host to be used. May be <code>null</code>.
     * @param aProxyCredentials
     *        The proxy server credentials to be used. May be <code>null</code>.
     *        They are only used if a proxy host is present! Usually they are of
     *        type {@link org.apache.http.auth.UsernamePasswordCredentials}.
     * @since 8.8.0
     * @see #setProxy(HttpHost)
     */
    public final void setProxy(@Nullable final HttpHost aProxy, @Nullable final Credentials aProxyCredentials) {
        m_aProxy = aProxy;
        m_aProxyCredentials = aProxyCredentials;
    }

    /**
     * @return The number of retries. Defaults to {@link #DEFAULT_RETRIES}.
     * @since 9.0.0
     */
    @Nonnegative
    public final int getRetries() {
        return m_nRetries;
    }

    /**
     * Set the number of internal retries.
     *
     * @param nRetries
     *        Retries to use. Must be &ge; 0.
     * @return this for chaining
     * @since 9.0.0
     */
    @Nonnull
    public final HttpClientFactory setRetries(@Nonnegative final int nRetries) {
        ValueEnforcer.isGE0(nRetries, "Retries");
        m_nRetries = nRetries;
        return this;
    }

    /**
     * @return The retry-mode. Never <code>null</code>. The default is
     *         {@link #DEFAULT_RETRY_MODE}.
     * @since 9.0.0
     */
    @Nonnull
    public final ERetryMode getRetryMode() {
        return m_eRetryMode;
    }

    /**
     * Set the retry mode to use.
     *
     * @param eRetryMode
     *        Retry mode to use. Must not be <code>null</code>.
     * @return this for chaining
     * @since 9.0.0
     */
    @Nonnull
    public final HttpClientFactory setRetryMode(@Nonnull final ERetryMode eRetryMode) {
        ValueEnforcer.notNull(eRetryMode, "RetryMode");
        m_eRetryMode = eRetryMode;
        return this;
    }

    @Nullable
    public LayeredConnectionSocketFactory createSSLFactory() {
        LayeredConnectionSocketFactory aSSLFactory = null;

        try {
            // First try with a custom SSL context
            if (m_aSSLContext != null) {
                // Choose correct TLS configuration mode
                final ITLSConfigurationMode aTLSConfigMode = m_aTLSConfigurationMode != null
                        ? m_aTLSConfigurationMode
                        : DEFAULT_TLS_CONFIG_MODE;

                // Custom hostname verifier preferred
                HostnameVerifier aHostnameVerifier = m_aHostnameVerifier;
                if (aHostnameVerifier == null)
                    aHostnameVerifier = SSLConnectionSocketFactory.getDefaultHostnameVerifier();

                if (LOGGER.isDebugEnabled())
                    LOGGER.debug("Using the following TLS versions: " + aTLSConfigMode.getAllTLSVersionIDs());

                if (LOGGER.isDebugEnabled())
                    LOGGER.debug("Using the following TLS cipher suites: " + aTLSConfigMode.getAllCipherSuites());

                if (LOGGER.isDebugEnabled())
                    LOGGER.debug("Using the following hostname verifier: " + aHostnameVerifier);

                aSSLFactory = new SSLConnectionSocketFactory(m_aSSLContext,
                        aTLSConfigMode.getAllTLSVersionIDsAsArray(), aTLSConfigMode.getAllCipherSuitesAsArray(),
                        aHostnameVerifier);
            }
        } catch (final SSLInitializationException ex) {
            // Fall through
            LOGGER.warn(
                    "Failed to init custom SSLConnectionSocketFactory - falling back to default SSLConnectionSocketFactory",
                    ex);
        }

        if (aSSLFactory == null) {
            // No custom SSL context present - use system defaults
            try {
                aSSLFactory = SSLConnectionSocketFactory.getSystemSocketFactory();
            } catch (final SSLInitializationException ex) {
                try {
                    aSSLFactory = SSLConnectionSocketFactory.getSocketFactory();
                } catch (final SSLInitializationException ex2) {
                    // Fall through
                }
            }
        }
        return aSSLFactory;
    }

    /**
     * @return The connection builder used by the
     *         {@link PoolingHttpClientConnectionManager} to create the default
     *         connection configuration.
     */
    @Nonnull
    public ConnectionConfig.Builder createConnectionConfigBuilder() {
        return ConnectionConfig.custom().setMalformedInputAction(CodingErrorAction.IGNORE)
                .setUnmappableInputAction(CodingErrorAction.IGNORE).setCharset(StandardCharsets.UTF_8);
    }

    /**
     * @return The default connection configuration used by the
     *         {@link PoolingHttpClientConnectionManager}.
     */
    @Nonnull
    public ConnectionConfig createConnectionConfig() {
        return createConnectionConfigBuilder().build();
    }

    /**
     * @return The DNS resolver to be used for
     *         {@link PoolingHttpClientConnectionManager}. May be
     *         <code>null</code> to use the default.
     * @see #isUseDNSClientCache()
     * @see #setUseDNSClientCache(boolean)
     * @since 8.8.0
     */
    @Nullable
    public DnsResolver createDNSResolver() {
        // If caching is active, use the default System resolver
        return m_bUseDNSClientCache ? SystemDefaultDnsResolver.INSTANCE : NonCachingDnsResolver.INSTANCE;
    }

    @Nonnull
    public HttpClientConnectionManager createConnectionManager(
            @Nonnull final LayeredConnectionSocketFactory aSSLFactory) {
        final Registry<ConnectionSocketFactory> aConSocketRegistry = RegistryBuilder
                .<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", aSSLFactory).build();
        final DnsResolver aDNSResolver = createDNSResolver();
        final PoolingHttpClientConnectionManager aConnMgr = new PoolingHttpClientConnectionManager(
                aConSocketRegistry, aDNSResolver);
        aConnMgr.setDefaultMaxPerRoute(100);
        aConnMgr.setMaxTotal(200);
        aConnMgr.setValidateAfterInactivity(1000);

        final ConnectionConfig aConnectionConfig = createConnectionConfig();
        aConnMgr.setDefaultConnectionConfig(aConnectionConfig);

        return aConnMgr;
    }

    @Nonnull
    public RequestConfig.Builder createRequestConfigBuilder() {
        return RequestConfig.custom().setCookieSpec(CookieSpecs.DEFAULT).setSocketTimeout(10_000)
                .setConnectTimeout(5_000).setConnectionRequestTimeout(5_000).setCircularRedirectsAllowed(false)
                .setRedirectsEnabled(true);
    }

    @Nonnull
    public RequestConfig createRequestConfig() {
        return createRequestConfigBuilder().build();
    }

    @Nullable
    public CredentialsProvider createCredentialsProvider() {
        final HttpHost aProxyHost = getProxyHost();
        final Credentials aProxyCredentials = getProxyCredentials();
        if (aProxyHost != null && aProxyCredentials != null) {
            final CredentialsProvider aCredentialsProvider = new BasicCredentialsProvider();
            aCredentialsProvider.setCredentials(new AuthScope(aProxyHost), aProxyCredentials);
            return aCredentialsProvider;
        }
        return null;
    }

    @Nullable
    public HttpRequestRetryHandler createRequestRetryHandler(@Nonnegative final int nMaxRetries,
            @Nonnull final ERetryMode eRetryMode) {
        return new HttpClientRetryHandler(nMaxRetries, eRetryMode);
    }

    @Nonnull
    public HttpClientBuilder createHttpClientBuilder() {
        final LayeredConnectionSocketFactory aSSLFactory = createSSLFactory();
        if (aSSLFactory == null)
            throw new IllegalStateException("Failed to create SSL SocketFactory");

        final HttpClientConnectionManager aConnMgr = createConnectionManager(aSSLFactory);
        final RequestConfig aRequestConfig = createRequestConfig();
        final HttpHost aProxyHost = getProxyHost();
        final CredentialsProvider aCredentialsProvider = createCredentialsProvider();

        final HttpClientBuilder aHCB = HttpClients.custom().setConnectionManager(aConnMgr)
                .setDefaultRequestConfig(aRequestConfig).setProxy(aProxyHost)
                .setDefaultCredentialsProvider(aCredentialsProvider);

        // Allow gzip,compress
        aHCB.addInterceptorLast(new RequestAcceptEncoding());
        // Add cookies
        aHCB.addInterceptorLast(new RequestAddCookies());
        // Un-gzip or uncompress
        aHCB.addInterceptorLast(new ResponseContentEncoding());

        // Enable usage of Java networking system properties
        if (m_bUseSystemProperties)
            aHCB.useSystemProperties();

        // Set retry handler (if needed)
        if (m_nRetries > 0)
            aHCB.setRetryHandler(createRequestRetryHandler(m_nRetries, m_eRetryMode));

        return aHCB;
    }

    @Nonnull
    public CloseableHttpClient createHttpClient() {
        final HttpClientBuilder aBuilder = createHttpClientBuilder();
        return aBuilder.build();
    }
}