package io.dropwizard.client; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.httpclient.HttpClientMetricNameStrategies; import com.codahale.metrics.httpclient.HttpClientMetricNameStrategy; import com.codahale.metrics.httpclient.InstrumentedHttpClientConnectionManager; import com.codahale.metrics.httpclient.InstrumentedHttpRequestExecutor; import com.google.common.annotations.VisibleForTesting; import io.dropwizard.client.proxy.AuthConfiguration; import io.dropwizard.client.proxy.NonProxyListProxyRoutePlanner; import io.dropwizard.client.proxy.ProxyConfiguration; import io.dropwizard.client.ssl.TlsConfiguration; import io.dropwizard.lifecycle.Managed; import io.dropwizard.setup.Environment; import io.dropwizard.util.Duration; import javax.net.ssl.HostnameVerifier; import org.apache.http.ConnectionReuseStrategy; import org.apache.http.Header; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.HttpClient; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.RedirectStrategy; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.config.SocketConfig; import org.apache.http.conn.DnsResolver; import org.apache.http.conn.routing.HttpRoutePlanner; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.DefaultConnectionReuseStrategy; import org.apache.http.impl.NoConnectionReuseStrategy; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy; import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; import org.apache.http.impl.conn.SystemDefaultDnsResolver; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpProcessor; import java.util.List; /** * A convenience class for building {@link HttpClient} instances. * <p> * Among other things, * <ul> * <li>Disables stale connection checks by default</li> * <li>Disables Nagle's algorithm</li> * <li>Disables cookie management by default</li> * </ul> * </p> */ public class HttpClientBuilder { private static final HttpRequestRetryHandler NO_RETRIES = (exception, executionCount, context) -> false; private final MetricRegistry metricRegistry; private String environmentName; private Environment environment; private HttpClientConfiguration configuration = new HttpClientConfiguration(); private DnsResolver resolver = new SystemDefaultDnsResolver(); private HostnameVerifier verifier; private HttpRequestRetryHandler httpRequestRetryHandler; private Registry<ConnectionSocketFactory> registry; private CredentialsProvider credentialsProvider = null; private HttpClientMetricNameStrategy metricNameStrategy = HttpClientMetricNameStrategies.METHOD_ONLY; private HttpRoutePlanner routePlanner = null; private RedirectStrategy redirectStrategy; private boolean disableContentCompression; private List<? extends Header> defaultHeaders; private HttpProcessor httpProcessor; public HttpClientBuilder(MetricRegistry metricRegistry) { this.metricRegistry = metricRegistry; } public HttpClientBuilder(Environment environment) { this(environment.metrics()); name(environment.getName()); this.environment = environment; } /** * Use the given environment name. This is used in the user agent. * * @param environmentName an environment name to use in the user agent. * @return {@code this} */ public HttpClientBuilder name(String environmentName) { this.environmentName = environmentName; return this; } /** * Use the given {@link HttpClientConfiguration} instance. * * @param configuration a {@link HttpClientConfiguration} instance * @return {@code this} */ public HttpClientBuilder using(HttpClientConfiguration configuration) { this.configuration = configuration; return this; } /** * Use the given {@link DnsResolver} instance. * * @param resolver a {@link DnsResolver} instance * @return {@code this} */ public HttpClientBuilder using(DnsResolver resolver) { this.resolver = resolver; return this; } /** * Use the give (@link HostnameVerifier} instance. * * @param verifier a {@link HostnameVerifier} instance * @return {@code this} */ public HttpClientBuilder using(HostnameVerifier verifier) { this.verifier = verifier; return this; } /** * Uses the {@link HttpRequestRetryHandler} for handling request retries. * * @param httpRequestRetryHandler an httpRequestRetryHandler * @return {@code this} */ public HttpClientBuilder using(HttpRequestRetryHandler httpRequestRetryHandler) { this.httpRequestRetryHandler = httpRequestRetryHandler; return this; } /** * Use the given {@link Registry} instance. * * @param registry * @return {@code this} */ public HttpClientBuilder using(Registry<ConnectionSocketFactory> registry) { this.registry = registry; return this; } /** * Use the given {@link HttpRoutePlanner} instance. * * @param routePlanner a {@link HttpRoutePlanner} instance * @return {@code this} */ public HttpClientBuilder using(HttpRoutePlanner routePlanner) { this.routePlanner = routePlanner; return this; } /** * Use the given {@link CredentialsProvider} instance. * * @param credentialsProvider a {@link CredentialsProvider} instance * @return {@code this} */ public HttpClientBuilder using(CredentialsProvider credentialsProvider) { this.credentialsProvider = credentialsProvider; return this; } /** * Use the given {@link HttpClientMetricNameStrategy} instance. * * @param metricNameStrategy a {@link HttpClientMetricNameStrategy} instance * @return {@code this} */ public HttpClientBuilder using(HttpClientMetricNameStrategy metricNameStrategy) { this.metricNameStrategy = metricNameStrategy; return this; } /** * Use the given {@link org.apache.http.client.RedirectStrategy} instance. * * @param redirectStrategy a {@link org.apache.http.client.RedirectStrategy} instance * @return {@code this} */ public HttpClientBuilder using(RedirectStrategy redirectStrategy) { this.redirectStrategy = redirectStrategy; return this; } /** * Use the given default headers for each HTTP request * * @param defaultHeaders HTTP headers * @return {@code} this */ public HttpClientBuilder using(List<? extends Header> defaultHeaders) { this.defaultHeaders = defaultHeaders; return this; } /** * Use the given {@link HttpProcessor} instance * * @param httpProcessor a {@link HttpProcessor} instance * @return {@code} this */ public HttpClientBuilder using(HttpProcessor httpProcessor) { this.httpProcessor = httpProcessor; return this; } /** * Disable support of decompression of responses * * @param disableContentCompression {@code true}, if disabled * @return {@code this} */ public HttpClientBuilder disableContentCompression(boolean disableContentCompression) { this.disableContentCompression = disableContentCompression; return this; } /** * Builds the {@link HttpClient}. * * @param name * @return an {@link CloseableHttpClient} */ public CloseableHttpClient build(String name) { final CloseableHttpClient client = buildWithDefaultRequestConfiguration(name).getClient(); // If the environment is present, we tie the client with the server lifecycle if (environment != null) { environment.lifecycle().manage(new Managed() { @Override public void start() throws Exception { } @Override public void stop() throws Exception { client.close(); } }); } return client; } /** * For internal use only, used in {@link io.dropwizard.client.JerseyClientBuilder} * to create an instance of {@link io.dropwizard.client.DropwizardApacheConnector} * * @param name * @return an {@link io.dropwizard.client.ConfiguredCloseableHttpClient} */ ConfiguredCloseableHttpClient buildWithDefaultRequestConfiguration(String name) { return createClient(org.apache.http.impl.client.HttpClientBuilder.create(), createConnectionManager(createConfiguredRegistry(), name), name); } /** * Configures an Apache {@link org.apache.http.impl.client.HttpClientBuilder HttpClientBuilder}. * * Intended for use by subclasses to inject HttpClientBuilder * configuration. The default implementation is an identity * function. */ protected org.apache.http.impl.client.HttpClientBuilder customizeBuilder( org.apache.http.impl.client.HttpClientBuilder builder ) { return builder; } /** * Map the parameters in {@link HttpClientConfiguration} to configuration on a * {@link org.apache.http.impl.client.HttpClientBuilder} instance * * @param builder * @param manager * @param name * @return the configured {@link CloseableHttpClient} */ protected ConfiguredCloseableHttpClient createClient( final org.apache.http.impl.client.HttpClientBuilder builder, final InstrumentedHttpClientConnectionManager manager, final String name) { final String cookiePolicy = configuration.isCookiesEnabled() ? CookieSpecs.DEFAULT : CookieSpecs.IGNORE_COOKIES; final Integer timeout = (int) configuration.getTimeout().toMilliseconds(); final Integer connectionTimeout = (int) configuration.getConnectionTimeout().toMilliseconds(); final Integer connectionRequestTimeout = (int) configuration.getConnectionRequestTimeout().toMilliseconds(); final long keepAlive = configuration.getKeepAlive().toMilliseconds(); final ConnectionReuseStrategy reuseStrategy = keepAlive == 0 ? new NoConnectionReuseStrategy() : new DefaultConnectionReuseStrategy(); final HttpRequestRetryHandler retryHandler = configuration.getRetries() == 0 ? NO_RETRIES : (httpRequestRetryHandler == null ? new DefaultHttpRequestRetryHandler(configuration.getRetries(), false) : httpRequestRetryHandler); final RequestConfig requestConfig = RequestConfig.custom().setCookieSpec(cookiePolicy) .setSocketTimeout(timeout) .setConnectTimeout(connectionTimeout) .setConnectionRequestTimeout(connectionRequestTimeout) .build(); final SocketConfig socketConfig = SocketConfig.custom() .setTcpNoDelay(true) .setSoTimeout(timeout) .build(); customizeBuilder(builder) .setRequestExecutor(new InstrumentedHttpRequestExecutor(metricRegistry, metricNameStrategy, name)) .setConnectionManager(manager) .setDefaultRequestConfig(requestConfig) .setDefaultSocketConfig(socketConfig) .setConnectionReuseStrategy(reuseStrategy) .setRetryHandler(retryHandler) .setUserAgent(createUserAgent(name)); if (keepAlive != 0) { // either keep alive based on response header Keep-Alive, // or if the server can keep a persistent connection (-1), then override based on client's configuration builder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy() { @Override public long getKeepAliveDuration(HttpResponse response, HttpContext context) { final long duration = super.getKeepAliveDuration(response, context); return (duration == -1) ? keepAlive : duration; } }); } // create a tunnel through a proxy host if it's specified in the config final ProxyConfiguration proxy = configuration.getProxyConfiguration(); if (proxy != null) { final HttpHost httpHost = new HttpHost(proxy.getHost(), proxy.getPort(), proxy.getScheme()); builder.setRoutePlanner(new NonProxyListProxyRoutePlanner(httpHost, proxy.getNonProxyHosts())); // if the proxy host requires authentication then add the host credentials to the credentials provider final AuthConfiguration auth = proxy.getAuth(); if (auth != null) { if (credentialsProvider == null) { credentialsProvider = new BasicCredentialsProvider(); } credentialsProvider.setCredentials(new AuthScope(httpHost), new UsernamePasswordCredentials(auth.getUsername(), auth.getPassword())); } } if (credentialsProvider != null) { builder.setDefaultCredentialsProvider(credentialsProvider); } if (routePlanner != null) { builder.setRoutePlanner(routePlanner); } if (disableContentCompression) { builder.disableContentCompression(); } if (redirectStrategy != null) { builder.setRedirectStrategy(redirectStrategy); } if (defaultHeaders != null) { builder.setDefaultHeaders(defaultHeaders); } if (verifier != null) { builder.setSSLHostnameVerifier(verifier); } if (httpProcessor != null) { builder.setHttpProcessor(httpProcessor); } return new ConfiguredCloseableHttpClient(builder.build(), requestConfig); } /** * Create a user agent string using the configured user agent if defined, otherwise * using a combination of the environment name and this client name * * @param name the name of this client * @return the user agent string to be used by this client */ protected String createUserAgent(String name) { final String defaultUserAgent = environmentName == null ? name : String.format("%s (%s)", environmentName, name); return configuration.getUserAgent().orElse(defaultUserAgent); } /** * Create a InstrumentedHttpClientConnectionManager based on the * HttpClientConfiguration. It sets the maximum connections per route and * the maximum total connections that the connection manager can create * * @param registry * @param name * @return a InstrumentedHttpClientConnectionManger instance */ protected InstrumentedHttpClientConnectionManager createConnectionManager(Registry<ConnectionSocketFactory> registry, String name) { final Duration ttl = configuration.getTimeToLive(); final InstrumentedHttpClientConnectionManager manager = new InstrumentedHttpClientConnectionManager( metricRegistry, registry, null, null, resolver, ttl.getQuantity(), ttl.getUnit(), name); return configureConnectionManager(manager); } @VisibleForTesting Registry<ConnectionSocketFactory> createConfiguredRegistry() { if (registry != null) { return registry; } TlsConfiguration tlsConfiguration = configuration.getTlsConfiguration(); if (tlsConfiguration == null && verifier != null) { tlsConfiguration = new TlsConfiguration(); } final SSLConnectionSocketFactory sslConnectionSocketFactory; if (tlsConfiguration == null) { sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory(); } else { sslConnectionSocketFactory = new DropwizardSSLConnectionSocketFactory(tlsConfiguration, verifier).getSocketFactory(); } return RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", sslConnectionSocketFactory) .build(); } @VisibleForTesting protected InstrumentedHttpClientConnectionManager configureConnectionManager( InstrumentedHttpClientConnectionManager connectionManager) { connectionManager.setDefaultMaxPerRoute(configuration.getMaxConnectionsPerRoute()); connectionManager.setMaxTotal(configuration.getMaxConnections()); connectionManager.setValidateAfterInactivity((int) configuration.getValidateAfterInactivityPeriod().toMilliseconds()); return connectionManager; } }