package org.apereo.cas.util.http;
import com.google.common.base.Throwables;
import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.client.AuthenticationStrategy;
import org.apache.http.client.ConnectionBackoffStrategy;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.RedirectStrategy;
import org.apache.http.client.ServiceUnavailableRetryStrategy;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.routing.HttpRoute;
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.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultBackoffStrategy;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.DefaultServiceUnavailableRetryStrategy;
import org.apache.http.impl.client.FutureRequestExecutionService;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.ProxyAuthenticationStrategy;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.FactoryBean;
import javax.annotation.PreDestroy;
import javax.net.ssl.HostnameVerifier;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* The factory to build a {@link SimpleHttpClient}.
*
* @author Jerome Leleu
* @since 4.1.0
*/
public class SimpleHttpClientFactoryBean implements FactoryBean<SimpleHttpClient> {
/**
* Max connections per route.
*/
public static final int MAX_CONNECTIONS_PER_ROUTE = 50;
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleHttpClientFactoryBean.class);
private static final int MAX_POOLED_CONNECTIONS = 100;
private static final int DEFAULT_THREADS_NUMBER = 200;
private static final int DEFAULT_TIMEOUT = 5000;
/**
* The default status codes we accept.
*/
private static final int[] DEFAULT_ACCEPTABLE_CODES = new int[]{
HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_NOT_MODIFIED,
HttpURLConnection.HTTP_MOVED_TEMP, HttpURLConnection.HTTP_MOVED_PERM,
HttpURLConnection.HTTP_ACCEPTED, HttpURLConnection.HTTP_NO_CONTENT};
/**
* 20% of the total of threads in the pool to handle overhead.
*/
private static final int DEFAULT_QUEUE_SIZE = (int) (DEFAULT_THREADS_NUMBER * 0.2);
/**
* The number of threads used to build the pool of threads (if no executorService provided).
*/
private int threadsNumber = DEFAULT_THREADS_NUMBER;
/**
* The queue size to absorb additional tasks when the threads pool is saturated (if no executorService provided).
*/
private int queueSize = DEFAULT_QUEUE_SIZE;
/**
* The Max pooled connections.
*/
private int maxPooledConnections = MAX_POOLED_CONNECTIONS;
/**
* The Max connections per each route connections.
*/
private int maxConnectionsPerRoute = MAX_CONNECTIONS_PER_ROUTE;
/**
* List of HTTP status codes considered valid by the caller.
*/
private List<Integer> acceptableCodes = IntStream.of(DEFAULT_ACCEPTABLE_CODES).boxed().collect(Collectors.toList());
private long connectionTimeout = DEFAULT_TIMEOUT;
private int readTimeout = DEFAULT_TIMEOUT;
private RedirectStrategy redirectionStrategy = new DefaultRedirectStrategy();
/**
* The socket factory to be used when verifying the validity of the endpoint.
*/
private SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactory.getSocketFactory();
/**
* The hostname verifier to be used when verifying the validity of the endpoint.
*/
private HostnameVerifier hostnameVerifier = new DefaultHostnameVerifier();
/**
* The credentials provider for endpoints that require authentication.
*/
private CredentialsProvider credentialsProvider;
/**
* The cookie store for authentication.
*/
private CookieStore cookieStore;
/**
* Interface for deciding whether a connection can be re-used for subsequent requests and should be kept alive.
**/
private ConnectionReuseStrategy connectionReuseStrategy = new DefaultConnectionReuseStrategy();
/**
* When managing a dynamic number of connections for a given route, this strategy assesses whether a
* given request execution outcome should result in a backoff
* signal or not, based on either examining the Throwable that resulted or by examining
* the resulting response (e.g. for its status code).
*/
private ConnectionBackoffStrategy connectionBackoffStrategy = new DefaultBackoffStrategy();
/**
* Strategy interface that allows API users to plug in their own logic to control whether or not a retry
* should automatically be done, how many times it should be retried and so on.
*/
private ServiceUnavailableRetryStrategy serviceUnavailableRetryStrategy = new DefaultServiceUnavailableRetryStrategy();
/**
* Default headers to be sent.
**/
private Collection<? extends Header> defaultHeaders = Collections.emptyList();
/**
* Default strategy implementation for proxy host authentication.
**/
private AuthenticationStrategy proxyAuthenticationStrategy = new ProxyAuthenticationStrategy();
/**
* Determines whether circular redirects (redirects to the same location) should be allowed.
**/
private boolean circularRedirectsAllowed = true;
/**
* Determines whether authentication should be handled automatically.
**/
private boolean authenticationEnabled;
/**
* Determines whether redirects should be handled automatically.
**/
private boolean redirectsEnabled = true;
/**
* The executor service used to create a {@link #buildRequestExecutorService}.
*/
private ExecutorService executorService;
@Override
public SimpleHttpClient getObject() throws Exception {
final CloseableHttpClient httpClient = buildHttpClient();
final FutureRequestExecutionService requestExecutorService = buildRequestExecutorService(httpClient);
return new SimpleHttpClient(this.acceptableCodes, httpClient, requestExecutorService);
}
@Override
public Class<?> getObjectType() {
return SimpleHttpClient.class;
}
@Override
public boolean isSingleton() {
return false;
}
/**
* Build a HTTP client based on the current properties.
*
* @return the built HTTP client
*/
private CloseableHttpClient buildHttpClient() {
try {
final ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory();
final LayeredConnectionSocketFactory sslsf = this.sslSocketFactory;
final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", plainsf)
.register("https", sslsf)
.build();
final PoolingHttpClientConnectionManager connMgmr = new PoolingHttpClientConnectionManager(registry);
connMgmr.setMaxTotal(this.maxPooledConnections);
connMgmr.setDefaultMaxPerRoute(this.maxConnectionsPerRoute);
connMgmr.setValidateAfterInactivity(DEFAULT_TIMEOUT);
final HttpHost httpHost = new HttpHost(InetAddress.getLocalHost());
final HttpRoute httpRoute = new HttpRoute(httpHost);
connMgmr.setMaxPerRoute(httpRoute, MAX_CONNECTIONS_PER_ROUTE);
final RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(this.readTimeout)
.setConnectTimeout(Long.valueOf(this.connectionTimeout).intValue())
.setConnectionRequestTimeout(Long.valueOf(this.connectionTimeout).intValue())
.setCircularRedirectsAllowed(this.circularRedirectsAllowed)
.setRedirectsEnabled(this.redirectsEnabled)
.setAuthenticationEnabled(this.authenticationEnabled)
.build();
final HttpClientBuilder builder = HttpClients.custom()
.setConnectionManager(connMgmr)
.setDefaultRequestConfig(requestConfig)
.setSSLSocketFactory(sslsf)
.setSSLHostnameVerifier(this.hostnameVerifier)
.setRedirectStrategy(this.redirectionStrategy)
.setDefaultCredentialsProvider(this.credentialsProvider)
.setDefaultCookieStore(this.cookieStore)
.setConnectionReuseStrategy(this.connectionReuseStrategy)
.setConnectionBackoffStrategy(this.connectionBackoffStrategy)
.setServiceUnavailableRetryStrategy(this.serviceUnavailableRetryStrategy)
.setProxyAuthenticationStrategy(this.proxyAuthenticationStrategy)
.setDefaultHeaders(this.defaultHeaders)
.useSystemProperties();
return builder.build();
} catch (final Exception e) {
LOGGER.error(e.getMessage(), e);
throw Throwables.propagate(e);
}
}
/**
* Build a {@link FutureRequestExecutionService} from the current properties and a HTTP client.
*
* @param httpClient the provided HTTP client
* @return the built request executor service
*/
private FutureRequestExecutionService buildRequestExecutorService(final CloseableHttpClient httpClient) {
if (this.executorService == null) {
this.executorService = new ThreadPoolExecutor(this.threadsNumber, this.threadsNumber,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(this.queueSize));
}
return new FutureRequestExecutionService(httpClient, this.executorService);
}
public ExecutorService getExecutorService() {
return this.executorService;
}
public void setExecutorService(final ExecutorService executorService) {
this.executorService = executorService;
}
public int getThreadsNumber() {
return this.threadsNumber;
}
public void setThreadsNumber(final int threadsNumber) {
this.threadsNumber = threadsNumber;
}
public int getQueueSize() {
return this.queueSize;
}
public void setQueueSize(final int queueSize) {
this.queueSize = queueSize;
}
public int getMaxPooledConnections() {
return this.maxPooledConnections;
}
public void setMaxPooledConnections(final int maxPooledConnections) {
this.maxPooledConnections = maxPooledConnections;
}
public int getMaxConnectionsPerRoute() {
return this.maxConnectionsPerRoute;
}
public void setMaxConnectionsPerRoute(final int maxConnectionsPerRoute) {
this.maxConnectionsPerRoute = maxConnectionsPerRoute;
}
public List<Integer> getAcceptableCodes() {
return Collections.unmodifiableList(this.acceptableCodes);
}
public void setAcceptableCodes(final int[] acceptableCodes) {
this.acceptableCodes = IntStream.of(acceptableCodes).boxed().collect(Collectors.toList());
}
public long getConnectionTimeout() {
return this.connectionTimeout;
}
public void setConnectionTimeout(final long connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
public int getReadTimeout() {
return this.readTimeout;
}
public void setReadTimeout(
final int readTimeout) {
this.readTimeout = readTimeout;
}
public RedirectStrategy getRedirectionStrategy() {
return this.redirectionStrategy;
}
public void setRedirectionStrategy(final RedirectStrategy redirectionStrategy) {
this.redirectionStrategy = redirectionStrategy;
}
public SSLConnectionSocketFactory getSslSocketFactory() {
return this.sslSocketFactory;
}
public void setSslSocketFactory(final SSLConnectionSocketFactory sslSocketFactory) {
this.sslSocketFactory = sslSocketFactory;
}
public HostnameVerifier getHostnameVerifier() {
return this.hostnameVerifier;
}
public void setHostnameVerifier(final HostnameVerifier hostnameVerifier) {
this.hostnameVerifier = hostnameVerifier;
}
public CredentialsProvider getCredentialsProvider() {
return this.credentialsProvider;
}
public void setCredentialsProvider(final CredentialsProvider credentialsProvider) {
this.credentialsProvider = credentialsProvider;
}
public CookieStore getCookieStore() {
return this.cookieStore;
}
public void setCookieStore(final CookieStore cookieStore) {
this.cookieStore = cookieStore;
}
public ConnectionReuseStrategy getConnectionReuseStrategy() {
return this.connectionReuseStrategy;
}
public void setConnectionReuseStrategy(final ConnectionReuseStrategy connectionReuseStrategy) {
this.connectionReuseStrategy = connectionReuseStrategy;
}
public ConnectionBackoffStrategy getConnectionBackoffStrategy() {
return this.connectionBackoffStrategy;
}
public void setConnectionBackoffStrategy(final ConnectionBackoffStrategy connectionBackoffStrategy) {
this.connectionBackoffStrategy = connectionBackoffStrategy;
}
public ServiceUnavailableRetryStrategy getServiceUnavailableRetryStrategy() {
return this.serviceUnavailableRetryStrategy;
}
public void setServiceUnavailableRetryStrategy(final ServiceUnavailableRetryStrategy serviceUnavailableRetryStrategy) {
this.serviceUnavailableRetryStrategy = serviceUnavailableRetryStrategy;
}
public Collection<? extends Header> getDefaultHeaders() {
return this.defaultHeaders;
}
public void setDefaultHeaders(final Collection<? extends Header> defaultHeaders) {
this.defaultHeaders = defaultHeaders;
}
public AuthenticationStrategy getProxyAuthenticationStrategy() {
return this.proxyAuthenticationStrategy;
}
public void setProxyAuthenticationStrategy(final AuthenticationStrategy proxyAuthenticationStrategy) {
this.proxyAuthenticationStrategy = proxyAuthenticationStrategy;
}
public boolean isCircularRedirectsAllowed() {
return this.circularRedirectsAllowed;
}
public void setCircularRedirectsAllowed(final boolean circularRedirectsAllowed) {
this.circularRedirectsAllowed = circularRedirectsAllowed;
}
public boolean isAuthenticationEnabled() {
return this.authenticationEnabled;
}
public void setAuthenticationEnabled(final boolean authenticationEnabled) {
this.authenticationEnabled = authenticationEnabled;
}
public boolean isRedirectsEnabled() {
return this.redirectsEnabled;
}
public void setRedirectsEnabled(final boolean redirectsEnabled) {
this.redirectsEnabled = redirectsEnabled;
}
/**
* Destroy.
*/
@PreDestroy
public void destroy() {
if (this.executorService != null) {
this.executorService.shutdownNow();
this.executorService = null;
}
}
/**
* The type Default http client.
*/
public static class DefaultHttpClient extends SimpleHttpClientFactoryBean {
}
/**
* The type Ssl trust store aware http client.
*/
public static class SslTrustStoreAwareHttpClient extends DefaultHttpClient {
}
}