/*
* Copyright (C) 2011 JFrog Ltd.
*
* 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 org.jfrog.build.client;
import com.google.common.collect.Sets;
import org.apache.commons.io.IOUtils;
import org.apache.http.*;
import org.apache.http.auth.*;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.ServiceUnavailableRetryStrategy;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.*;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;
import org.jfrog.build.api.util.Log;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
/**
* Wrapper of HttpClient that forces preemptive BASIC authentication if user credentials exist.
*
* @author Yossi Shaul
*/
public class PreemptiveHttpClient {
private final static String CLIENT_VERSION;
private final boolean requestSentRetryEnabled = true;
private CloseableHttpClient httpClient;
private BasicHttpContext localContext;
private ResponseHandler<HttpResponse> responseHandler = new PreemptiveHttpClientHandler();
private int connectionRetries;
private int retryCounter;
private Log log;
static {
// initialize client version
Properties properties = new Properties();
InputStream is = PreemptiveHttpClient.class.getResourceAsStream("/bi.client.properties");
if (is != null) {
try {
properties.load(is);
is.close();
} catch (IOException e) {
// ignore, use the default value
}
}
CLIENT_VERSION = properties.getProperty("client.version", "unknown");
}
public PreemptiveHttpClient(int timeout) {
this(null, null, timeout, null, ArtifactoryHttpClient.DEFAULT_CONNECTION_RETRY);
}
public PreemptiveHttpClient(String userName, String password, int timeout) {
this(userName, password, timeout, null, ArtifactoryHttpClient.DEFAULT_CONNECTION_RETRY);
}
public PreemptiveHttpClient(String userName, String password, int timeout, ProxyConfiguration proxyConfiguration, int connectionRetries) {
HttpClientBuilder httpClientBuilder = createHttpClientBuilder(userName, password, timeout, connectionRetries);
if (proxyConfiguration != null) {
setProxyConfiguration(httpClientBuilder, proxyConfiguration);
}
httpClient = httpClientBuilder.build();
}
private void setProxyConfiguration(HttpClientBuilder httpClientBuilder, ProxyConfiguration proxyConfiguration) {
HttpHost proxy = new HttpHost(proxyConfiguration.host, proxyConfiguration.port);
httpClientBuilder.setProxy(proxy);
if (proxyConfiguration.username != null) {
BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider();
basicCredentialsProvider.setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
new UsernamePasswordCredentials(proxyConfiguration.username, proxyConfiguration.password));
httpClientBuilder.setDefaultCredentialsProvider(basicCredentialsProvider);
}
}
public HttpResponse execute(HttpUriRequest request) throws IOException {
if (localContext != null) {
return httpClient.execute(request, responseHandler, localContext);
} else {
return httpClient.execute(request, responseHandler);
}
}
private HttpClientBuilder createHttpClientBuilder(String userName, String password, int timeout) {
return createHttpClientBuilder(userName, password, timeout, ArtifactoryHttpClient.DEFAULT_CONNECTION_RETRY);
}
private HttpClientBuilder createHttpClientBuilder(String userName, String password, int timeout, int connectionRetries) {
this.connectionRetries = connectionRetries;
int timeoutMilliSeconds = timeout * 1000;
RequestConfig requestConfig = RequestConfig
.custom()
.setSocketTimeout(timeoutMilliSeconds)
.setConnectTimeout(timeoutMilliSeconds)
.setCircularRedirectsAllowed(true)
.build();
HttpClientBuilder builder = HttpClientBuilder
.create()
.setDefaultRequestConfig(requestConfig);
if (userName != null && !"".equals(userName)) {
BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider();
basicCredentialsProvider.setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
new UsernamePasswordCredentials(userName, password));
builder.setDefaultCredentialsProvider(basicCredentialsProvider);
localContext = new BasicHttpContext();
// Generate BASIC scheme object and stick it to the local execution context
BasicScheme basicAuth = new BasicScheme();
localContext.setAttribute("preemptive-auth", basicAuth);
// Add as the first request interceptor
builder.addInterceptorFirst(new PreemptiveAuth());
}
int retryCount = connectionRetries < 0 ? ArtifactoryHttpClient.DEFAULT_CONNECTION_RETRY : connectionRetries;
builder.setRetryHandler(new PreemptiveRetryHandler(retryCount));
builder.setServiceUnavailableRetryStrategy(new PreemptiveRetryStrategy());
builder.setRedirectStrategy(new PreemptiveRedirectStrategy());
// set the following user agent with each request
String userAgent = "ArtifactoryBuildClient/" + CLIENT_VERSION;
builder.setUserAgent(userAgent);
return builder;
}
public void close() {
try {
httpClient.close();
} catch (IOException e) {
// Do nothing
}
}
public void setLog(Log log) {
this.log = log;
}
/**
* Sets the Exceptions that would not be retried if those exceptions are thrown.
*
* @return set of Exceptions that will not be retired
*/
private Set<Class<? extends IOException>> getNonRetriableClasses() {
Set<Class<? extends IOException>> classSet = new HashSet<Class<? extends IOException>>();
classSet.add(SSLException.class);
return classSet;
}
static class PreemptiveAuth implements HttpRequestInterceptor {
public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
AuthState authState = (AuthState) context.getAttribute(HttpClientContext.TARGET_AUTH_STATE);
// If no auth scheme available yet, try to initialize it preemptively
if (authState.getAuthScheme() == null) {
AuthScheme authScheme = (AuthScheme) context.getAttribute("preemptive-auth");
CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute(
HttpClientContext.CREDS_PROVIDER);
HttpHost targetHost = (HttpHost) context.getAttribute(HttpCoreContext.HTTP_TARGET_HOST);
if (authScheme != null) {
Credentials creds = credsProvider.getCredentials(
new AuthScope(targetHost.getHostName(), targetHost.getPort()));
if (creds == null) {
throw new HttpException("No credentials for preemptive authentication");
}
authState.update(authScheme, creds);
}
}
}
}
/**
* Class to handle retries when 5xx errors occurs.
*/
private class PreemptiveRetryStrategy implements ServiceUnavailableRetryStrategy {
@Override
public boolean retryRequest(HttpResponse response, int executionCount, HttpContext context) {
retryCounter++;
if (response.getStatusLine().getStatusCode() > 500) {
HttpClientContext clientContext = HttpClientContext.adapt(context);
log.warn("Error occurred for request " + clientContext.getRequest().getRequestLine().toString() +
". Received status code " + response.getStatusLine().getStatusCode() +
" and message: " + response.getStatusLine().getReasonPhrase() + ".");
if (retryCounter <= connectionRetries) {
log.warn("Attempting retry #" + retryCounter);
return true;
}
}
retryCounter = 0;
return false;
}
@Override
public long getRetryInterval() {
return 0;
}
}
/**
* Class to handle retries when exception occurs.
*/
private class PreemptiveRetryHandler extends DefaultHttpRequestRetryHandler {
PreemptiveRetryHandler(int connectionRetries) {
super(connectionRetries, requestSentRetryEnabled, getNonRetriableClasses());
}
@Override
public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
retryCounter++;
HttpClientContext clientContext = HttpClientContext.adapt(context);
log.warn("Error occurred for request " + clientContext.getRequest().getRequestLine().toString() + ": " + exception.getMessage() + ".");
if (retryCounter > connectionRetries) {
retryCounter = 0;
return false;
}
boolean shouldRetry = super.retryRequest(exception, retryCounter, context);
if (shouldRetry) {
log.warn("Attempting retry #" + retryCounter);
return true;
}
retryCounter = 0;
return false;
}
}
/**
* Class for performing redirection for the following status codes:
* SC_MOVED_PERMANENTLY (301)
* SC_MOVED_TEMPORARILY (302)
* SC_SEE_OTHER (303)
* SC_TEMPORARY_REDIRECT (307)
*/
private class PreemptiveRedirectStrategy extends DefaultRedirectStrategy {
private Set<String> redirectableMethods = Sets.newHashSet(
HttpGet.METHOD_NAME.toLowerCase(),
HttpPost.METHOD_NAME.toLowerCase(),
HttpHead.METHOD_NAME.toLowerCase(),
HttpDelete.METHOD_NAME.toLowerCase(),
HttpPut.METHOD_NAME.toLowerCase());
@Override
public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException {
URI uri = getLocationURI(request, response, context);
log.debug("Redirecting to " + uri);
return RequestBuilder.copy(request).setUri(uri).build();
}
@Override
protected boolean isRedirectable(String method) {
String message = "The method " + method;
if (redirectableMethods.contains(method.toLowerCase())) {
log.debug(message + " can be redirected.");
return true;
}
log.error(message + " cannot be redirected.");
return false;
}
}
/**
* gets responses from the underlying HttpClient and closes them (so you don't have to) the response body is
* buffered in an intermediary byte array.
* Will throw a {@link IOException} if the request failed.
*/
private class PreemptiveHttpClientHandler implements ResponseHandler<HttpResponse> {
@Override
public HttpResponse handleResponse(HttpResponse response) throws IOException {
HttpResponse newResponse = DefaultHttpResponseFactory.INSTANCE.newHttpResponse(response.getStatusLine(), new HttpClientContext());
newResponse.setHeaders(response.getAllHeaders());
int statusCode = response.getStatusLine().getStatusCode();
//Response entity might be null, 500 and 405 also give the html itself so skip it
if (response.getEntity() != null && statusCode != 500 && statusCode != 405) {
try {
InputStream entityInputStream = IOUtils.toBufferedInputStream(response.getEntity().getContent());
newResponse.setEntity(new InputStreamEntity(entityInputStream));
} catch (IOException e) {
//Ignore
} catch (NullPointerException e) {
//Null entity - Ignore
} finally {
HttpClientUtils.closeQuietly((CloseableHttpResponse) response);
}
}
return newResponse;
}
}
}