Java tutorial
/* * Artifactory is a binaries repository manager. * Copyright (C) 2014 JFrog Ltd. * * Artifactory 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. * * Artifactory 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 Artifactory. If not, see <http://www.gnu.org/licenses/>. */ package org.artifactory.util; import org.apache.commons.lang.StringUtils; import org.apache.http.HeaderElement; import org.apache.http.HeaderElementIterator; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.auth.AuthSchemeProvider; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.auth.NTCredentials; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.config.AuthSchemes; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.routing.HttpRoute; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.DefaultRoutePlanner; import org.apache.http.impl.conn.DefaultSchemePortResolver; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHeaderElementIterator; import org.apache.http.protocol.HTTP; import org.apache.http.protocol.HttpContext; import org.artifactory.common.ConstantValues; import org.artifactory.descriptor.repo.ProxyDescriptor; import org.artifactory.repo.http.CloseableHttpClientDecorator; import org.artifactory.security.crypto.CryptoHelper; import org.artifactory.util.bearer.BearerSchemeFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.net.UnknownHostException; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; /** * Builder for HTTP client. * * @author Yossi Shaul */ public class HttpClientConfigurator { private static final Logger log = LoggerFactory.getLogger(HttpClientConfigurator.class); // in httpclient 4.4 handling of stale connections was changed, // previously, the code would check every connection by default before re-using it. // The code now only checks connection if the elapsed time since the last use of // the connection exceeds the timeout that has been set private static final int INACTIVITY_TIMEOUT = 500; private static final int DEFAULT_PORT = 80; // max connections for localhost:80 private static final int DEFAULT_POOL_MAX_CONNECTIONS_PER_ROUTE = 50; private static final String LOCALHOST = "localhost"; private static final int MAX_CONNECTIONS_PER_HOST = 50; private static final int MAX_TOTAL_CONNECTIONS = 50; private HttpClientBuilder builder = HttpClients.custom(); private RequestConfig.Builder config = RequestConfig.custom(); private String host; private BasicCredentialsProvider credsProvider; boolean explicitCookieSupport; public HttpClientConfigurator() { builder.setUserAgent(HttpUtils.getArtifactoryUserAgent()); credsProvider = new BasicCredentialsProvider(); handleGzipResponse(ConstantValues.httpAcceptEncodingGzip.getBoolean()); config.setMaxRedirects(20); config.setCircularRedirectsAllowed(true); } public CloseableHttpClient getClient() { if (!explicitCookieSupport && !ConstantValues.enableCookieManagement.getBoolean()) { builder.disableCookieManagement(); } if (hasCredentials()) { builder.setDefaultCredentialsProvider(credsProvider); } builder.setDefaultRequestConfig(config.build()); /** * Connection management */ builder.setKeepAliveStrategy(getConnectionKeepAliveStrategy()); PoolingHttpClientConnectionManager connectionMgr = createConnectionMgr(); builder.setConnectionManager(connectionMgr); return new CloseableHttpClientDecorator(builder.build(), connectionMgr); } /** * Creates custom Http Client connection pool to be used by Http Client * * @return {@link PoolingHttpClientConnectionManager} */ private PoolingHttpClientConnectionManager createConnectionMgr() { PoolingHttpClientConnectionManager connectionMgr; connectionMgr = new PoolingHttpClientConnectionManager(INACTIVITY_TIMEOUT, TimeUnit.MILLISECONDS); connectionMgr.setMaxTotal(MAX_TOTAL_CONNECTIONS); connectionMgr.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_HOST); HttpHost localhost = new HttpHost(LOCALHOST, DEFAULT_PORT); connectionMgr.setMaxPerRoute(new HttpRoute(localhost), DEFAULT_POOL_MAX_CONNECTIONS_PER_ROUTE); return connectionMgr; } /** * Produces a {@link ConnectionKeepAliveStrategy} * * @return keep-alive strategy to be used for connection pool */ private ConnectionKeepAliveStrategy getConnectionKeepAliveStrategy() { return new ConnectionKeepAliveStrategy() { @Override public long getKeepAliveDuration(HttpResponse response, HttpContext context) { // Honor 'keep-alive' header HeaderElementIterator it = new BasicHeaderElementIterator( response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while (it.hasNext()) { HeaderElement he = it.nextElement(); String param = he.getName(); String value = he.getValue(); if (value != null && param.equalsIgnoreCase("timeout")) { try { return Long.parseLong(value) * 1000; } catch (NumberFormatException ignore) { } } } HttpHost target = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST); if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) { return 5 * 1000; } else { return 30 * 1000; } } }; } /** * Disable the automatic gzip compression on read. * Once disabled cannot be activated. */ public HttpClientConfigurator handleGzipResponse(boolean handleGzipResponse) { if (!handleGzipResponse) { builder.disableContentCompression(); } return this; } /** * May throw a runtime exception when the given URL is invalid. */ public HttpClientConfigurator hostFromUrl(String urlStr) { if (StringUtils.isNotBlank(urlStr)) { try { URL url = new URL(urlStr); host(url.getHost()); } catch (MalformedURLException e) { throw new IllegalArgumentException("Cannot parse the url " + urlStr, e); } } return this; } /** * Ignores blank values */ public HttpClientConfigurator host(String host) { if (StringUtils.isNotBlank(host)) { this.host = host; builder.setRoutePlanner(new DefaultHostRoutePlanner(host)); } return this; } public HttpClientConfigurator defaultMaxConnectionsPerHost(int maxConnectionsPerHost) { builder.setMaxConnPerRoute(maxConnectionsPerHost); return this; } public HttpClientConfigurator maxTotalConnections(int maxTotalConnections) { builder.setMaxConnTotal(maxTotalConnections); return this; } public HttpClientConfigurator connectionTimeout(int connectionTimeout) { config.setConnectTimeout(connectionTimeout); return this; } public HttpClientConfigurator soTimeout(int soTimeout) { config.setSocketTimeout(soTimeout); return this; } /** * see {@link org.apache.http.client.config.RequestConfig#isStaleConnectionCheckEnabled()} */ public HttpClientConfigurator staleCheckingEnabled(boolean staleCheckingEnabled) { config.setStaleConnectionCheckEnabled(staleCheckingEnabled); return this; } /** * Disable request retries on service unavailability. */ public HttpClientConfigurator noRetry() { return retry(0, false); } /** * Number of retry attempts. Default is 3 retries. * * @param retryCount Number of retry attempts. 0 means no retries. */ public HttpClientConfigurator retry(int retryCount, boolean requestSentRetryEnabled) { if (retryCount == 0) { builder.disableAutomaticRetries(); } else { builder.setRetryHandler(new DefaultHttpRequestRetryHandler(retryCount, requestSentRetryEnabled)); } return this; } /** * Ignores blank or invalid input */ public HttpClientConfigurator localAddress(String localAddress) { if (StringUtils.isNotBlank(localAddress)) { try { InetAddress address = InetAddress.getByName(localAddress); config.setLocalAddress(address); } catch (UnknownHostException e) { throw new IllegalArgumentException("Invalid local address: " + localAddress, e); } } return this; } /** * Ignores null credentials */ public HttpClientConfigurator authentication(UsernamePasswordCredentials creds) { if (creds != null) { authentication(creds.getUserName(), creds.getPassword()); } return this; } /** * Configures preemptive authentication on this client. Ignores blank username input. */ public HttpClientConfigurator authentication(String username, String password) { return authentication(username, password, false); } /** * Configures preemptive authentication on this client. Ignores blank username input. */ public HttpClientConfigurator authentication(String username, String password, boolean allowAnyHost) { if (StringUtils.isNotBlank(username)) { if (StringUtils.isBlank(host)) { throw new IllegalStateException("Cannot configure authentication when host is not set."); } AuthScope authscope = allowAnyHost ? new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM) : new AuthScope(host, AuthScope.ANY_PORT, AuthScope.ANY_REALM); credsProvider.setCredentials(authscope, new UsernamePasswordCredentials(username, password)); builder.addInterceptorFirst(new PreemptiveAuthInterceptor()); } return this; } /** * Enable cookie management for this client. */ public HttpClientConfigurator enableCookieManagement(boolean enableCookieManagement) { if (enableCookieManagement) { config.setCookieSpec(CookieSpecs.BROWSER_COMPATIBILITY); } else { config.setCookieSpec(null); } explicitCookieSupport = enableCookieManagement; return this; } public HttpClientConfigurator enableTokenAuthentication(boolean enableTokenAuthentication, String username, String password) { if (enableTokenAuthentication) { if (StringUtils.isBlank(host)) { throw new IllegalStateException("Cannot configure authentication when host is not set."); } config.setTargetPreferredAuthSchemes(Collections.singletonList("Bearer")); // We need dummy credentials otherwise we won't respond to a challenge properly AuthScope authScope = new AuthScope(host, AuthScope.ANY_PORT, AuthScope.ANY_REALM); // Dummy:dummy is the specification for forcing token authentication UsernamePasswordCredentials dummyCredentials = new UsernamePasswordCredentials("dummy", "dummy"); credsProvider.setCredentials(authScope, dummyCredentials); // The real credentials are passed to the Bearer that will get the token with them UsernamePasswordCredentials realCredentials = null; if (StringUtils.isNotBlank(username)) { realCredentials = new UsernamePasswordCredentials(username, password); } Registry<AuthSchemeProvider> bearerRegistry = RegistryBuilder.<AuthSchemeProvider>create() .register("Bearer", new BearerSchemeFactory(realCredentials)).build(); builder.setDefaultAuthSchemeRegistry(bearerRegistry); } return this; } public HttpClientConfigurator proxy(@Nullable ProxyDescriptor proxyDescriptor) { configureProxy(proxyDescriptor); return this; } private void configureProxy(ProxyDescriptor proxy) { if (proxy != null) { config.setProxy(new HttpHost(proxy.getHost(), proxy.getPort())); if (proxy.getUsername() != null) { Credentials creds = null; if (proxy.getDomain() == null) { creds = new UsernamePasswordCredentials(proxy.getUsername(), CryptoHelper.decryptIfNeeded(proxy.getPassword())); //This will demote the NTLM authentication scheme so that the proxy won't barf //when we try to give it traditional credentials. If the proxy doesn't do NTLM //then this won't hurt it (jcej at tragus dot org) List<String> authPrefs = Arrays.asList(AuthSchemes.DIGEST, AuthSchemes.BASIC, AuthSchemes.NTLM); config.setProxyPreferredAuthSchemes(authPrefs); // preemptive proxy authentication builder.addInterceptorFirst(new ProxyPreemptiveAuthInterceptor()); } else { try { String ntHost = StringUtils.isBlank(proxy.getNtHost()) ? InetAddress.getLocalHost().getHostName() : proxy.getNtHost(); creds = new NTCredentials(proxy.getUsername(), CryptoHelper.decryptIfNeeded(proxy.getPassword()), ntHost, proxy.getDomain()); } catch (UnknownHostException e) { log.error("Failed to determine required local hostname for NTLM credentials.", e); } } if (creds != null) { credsProvider.setCredentials( new AuthScope(proxy.getHost(), proxy.getPort(), AuthScope.ANY_REALM), creds); if (proxy.getRedirectedToHostsList() != null) { for (String hostName : proxy.getRedirectedToHostsList()) { credsProvider.setCredentials( new AuthScope(hostName, AuthScope.ANY_PORT, AuthScope.ANY_REALM), creds); } } } } } } private boolean hasCredentials() { return credsProvider.getCredentials(AuthScope.ANY) != null; } static class DefaultHostRoutePlanner extends DefaultRoutePlanner { private final HttpHost defaultHost; public DefaultHostRoutePlanner(String defaultHost) { super(DefaultSchemePortResolver.INSTANCE); this.defaultHost = new HttpHost(defaultHost); } @Override public HttpRoute determineRoute(HttpHost host, HttpRequest request, HttpContext context) throws HttpException { if (host == null) { host = defaultHost; } return super.determineRoute(host, request, context); } public HttpHost getDefaultHost() { return defaultHost; } } }