com.identityx.auth.client.HttpClientRequestExecutor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of daon-http-digest-auth Show documentation
Show all versions of daon-http-digest-auth Show documentation
Client library used for adding authentication to http messages as required by IdentityX Rest Services
/*
* Copyright Daon.
*
* 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.identityx.auth.client;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.Random;
import java.util.UUID;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.NoHttpResponseException;
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.config.RequestConfig;
import org.apache.http.client.methods.HttpRequestBase;
//import org.apache.http.client.params.AllClientPNames;
//import org.apache.http.client.params.ClientPNames;
//import org.apache.http.client.params.HttpClientParams;
//import org.apache.http.conn.params.ConnRoutePNames;
//import org.apache.http.impl.client.CloseableHttpClient;
//import org.apache.http.impl.client.DefaultHttpClient;
//import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.DefaultProxyRoutePlanner;
import com.identityx.auth.def.IApiKey;
import com.identityx.auth.def.ITokenKey;
import com.identityx.auth.def.IRequest;
import com.identityx.auth.def.IRequestAuthenticator;
import com.identityx.auth.def.IRequestAuthenticatorFactory;
import com.identityx.auth.def.IRequestExecutor;
import com.identityx.auth.def.IResponse;
import com.identityx.auth.def.IResponseAuthenticator;
import com.identityx.auth.def.IResponseAuthenticatorFactory;
import com.identityx.auth.def.IResponseVerifier;
import com.identityx.auth.def.IResponseVerifierFactory;
import com.identityx.auth.impl.AuthenticationScheme;
import com.identityx.auth.impl.DefaultRequestAuthenticatorFactory;
import com.identityx.auth.impl.DefaultResponseAuthenticatorFactory;
import com.identityx.auth.impl.DefaultResponseVerifierFactory;
import com.identityx.auth.impl.HttpHeaders;
import com.identityx.auth.impl.MediaType;
import com.identityx.auth.impl.Proxy;
import com.identityx.auth.impl.QueryString;
import com.identityx.auth.lang.Assert;
import com.identityx.auth.support.BackoffStrategy;
import com.identityx.auth.support.DefaultRequest;
import com.identityx.auth.support.DefaultResponse;
import com.identityx.auth.support.RestException;
import com.identityx.auth.util.StringInputStream;
/**
* {@code RequestExecutor} implementation that uses the
* Apache HttpClient implementation to
* execute http requests.
*
* @since 0.1
*/
public class HttpClientRequestExecutor implements IRequestExecutor {
private static final Log log = LogFactory.getLog(HttpClientRequestExecutor.class);
/**
* Default HTTP connection timeout.
*/
private int CONNECTION_TIMEOUT = 50000; //10,000 millis = 10 seconds
/**
* Maximum exponential back-off time before retrying a request
*/
private static final int MAX_BACKOFF_IN_MILLISECONDS = 20 * 1000;
private static final int DEFAULT_MAX_RETRIES = 1;
private int maxConnPerRoute = 20;
private int maxConnTotal = 50;
private int numRetries = DEFAULT_MAX_RETRIES;
private final IApiKey apiKey;
private final IRequestAuthenticator requestAuthenticator;
private final IResponseAuthenticator responseAuthenticator;
private final IResponseVerifier responseVerifier;
private HttpClient httpClient;
private BackoffStrategy backoffStrategy;
private HttpClientRequestFactory httpClientRequestFactory;
private final IRequestAuthenticatorFactory requestAuthenticatorFactory = new DefaultRequestAuthenticatorFactory();
private final IResponseAuthenticatorFactory responseAuthenticatorFactory = new DefaultResponseAuthenticatorFactory();
private final IResponseVerifierFactory responseVerifierFactory = new DefaultResponseVerifierFactory();
//doesn't need to be SecureRandom: only used in backoff strategy, not for crypto:
private final Random random = new Random();
/**
* Creates a new {@code HttpClientRequestExecutor} using the specified {@code ApiKey} and optional {@code Proxy}
* configuration.
* @param apiKey the account API Key
* @param proxy the HTTP proxy
* @param authenticationScheme the HTTP authentication
*/
public HttpClientRequestExecutor(IApiKey apiKey, Proxy proxy, AuthenticationScheme authenticationScheme, SSLConnectionSocketFactory sslsf) {
Assert.notNull(apiKey, "apiKey argument is required.");
this.apiKey = apiKey;
this.requestAuthenticator = requestAuthenticatorFactory.create(authenticationScheme);
this.responseAuthenticator = responseAuthenticatorFactory.create(authenticationScheme);
this.responseVerifier = responseVerifierFactory.create(authenticationScheme);
this.httpClientRequestFactory = new HttpClientRequestFactory();
/*
PoolingHttpClientConnectionManager cConnMgr = new PoolingHttpClientConnectionManager();
cConnMgr.setDefaultMaxPerRoute(maxConnPerRoute);
*/
RequestConfig.Builder requestBuilder = RequestConfig.custom();
requestBuilder = requestBuilder.setConnectTimeout(CONNECTION_TIMEOUT).setConnectionRequestTimeout(CONNECTION_TIMEOUT).setSocketTimeout(CONNECTION_TIMEOUT).setCookieSpec("IGNORE_COOKIES");
requestBuilder = requestBuilder.setRedirectsEnabled(false);
HttpClientContext context = HttpClientContext.create();
context.setAttribute("http.protocol.version", HttpVersion.HTTP_1_1);
ConnectionConfig.Builder connBuilder = ConnectionConfig.custom();
connBuilder.setCharset(Charset.forName("UTF-8"));
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create().disableRedirectHandling();
if (sslsf != null) {
httpClientBuilder.setSSLSocketFactory(sslsf);
}
httpClientBuilder.setMaxConnPerRoute(maxConnPerRoute).setMaxConnTotal(maxConnTotal);
if (proxy != null) {
HttpHost aProxy = new HttpHost(proxy.getHost(), proxy.getPort());
DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(aProxy);
httpClientBuilder.setRoutePlanner(routePlanner);
if (proxy.isAuthenticationRequired()) {
CredentialsProvider credentialProvider = new BasicCredentialsProvider();
credentialProvider.setCredentials(new AuthScope(proxy.getHost(), proxy.getPort()), new UsernamePasswordCredentials(proxy.getUsername(), proxy.getPassword()));
httpClientBuilder.setDefaultCredentialsProvider(credentialProvider);
}
}
httpClientBuilder.setDefaultRequestConfig(requestBuilder.build());
this.httpClient = httpClientBuilder.build();
/*
PoolingClientConnectionManager connMgr = new PoolingClientConnectionManager();
connMgr.setDefaultMaxPerRoute(maxConnPerRoute);
this.httpClient = new DefaultHttpClient(connMgr);
httpClient.getParams().setParameter(AllClientPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1);
httpClient.getParams().setParameter(AllClientPNames.SO_TIMEOUT, CONNECTION_TIMEOUT);
httpClient.getParams().setParameter(AllClientPNames.CONNECTION_TIMEOUT, CONNECTION_TIMEOUT);
httpClient.getParams().setParameter(ClientPNames.HANDLE_REDIRECTS, false);
httpClient.getParams().setParameter("http.protocol.content-charset", "UTF-8");
if (proxy != null) {
//We have some proxy setting to use!
HttpHost httpProxyHost = new HttpHost(proxy.getHost(), proxy.getPort());
httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, httpProxyHost);
if (proxy.isAuthenticationRequired()) {
httpClient.getCredentialsProvider().setCredentials(
new AuthScope(proxy.getHost(), proxy.getPort()),
new UsernamePasswordCredentials(proxy.getUsername(), proxy.getPassword()));
}
}
*/
}
public int getNumRetries() {
return numRetries;
}
public void setNumRetries(int numRetries) {
this.numRetries = numRetries;
}
/**
* @since 0.3
*/
public BackoffStrategy getBackoffStrategy() {
return this.backoffStrategy;
}
public void setBackoffStrategy(BackoffStrategy backoffStrategy) {
this.backoffStrategy = backoffStrategy;
}
public void setHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
public IResponse executeRequest(IRequest request) throws RestException, UnsupportedEncodingException {
return executeRequest(request, true);
}
public IResponse executeRequest(IRequest request, boolean verifyResponse) throws RestException, UnsupportedEncodingException {
Assert.notNull(request, "Request argument cannot be null.");
/*if (requestLog.isDebugEnabled()) {
requestLog.debug("Sending Request: " + request.toString());
}*/
int retryCount = 0;
URI redirectUri = null;
HttpEntity entity = null;
RestException exception = null;
// Make a copy of the original request params and headers so that we can
// permute them in the loop and start over with the original every time.
QueryString originalQuery = new QueryString();
originalQuery.putAll(request.getQueryString());
HttpHeaders originalHeaders = new HttpHeaders();
originalHeaders.putAll(request.getHeaders());
while (true) {
if (redirectUri != null) {
request = new DefaultRequest(
request.getMethod(),
redirectUri.toString(),
null,
null,
request.getBody(),
request.getHeaders().getContentLength()
);
}
if (retryCount > 0) {
request.setQueryString(originalQuery);
request.setHeaders(originalHeaders);
}
String nonce = UUID.randomUUID().toString();
// Sign the request
if (this.apiKey != null) {
this.requestAuthenticator.authenticate(request, this.apiKey, nonce);
}
HttpRequestBase httpRequest = this.httpClientRequestFactory.createHttpClientRequest(request, entity);
if (httpRequest instanceof HttpEntityEnclosingRequest) {
entity = ((HttpEntityEnclosingRequest) httpRequest).getEntity();
}
HttpResponse httpResponse = null;
int currentStatus = 0;
try {
if (retryCount > 0) {
pauseExponentially(retryCount, exception);
if (entity != null) {
InputStream content = entity.getContent();
if (content.markSupported()) {
content.reset();
}
}
}
exception = null;
retryCount++;
//long start = System.currentTimeMillis();
httpResponse = httpClient.execute(httpRequest);
//long end = System.currentTimeMillis();
//executionContext.getTimingInfo().addSubMeasurement(HTTP_REQUEST_TIME, new TimingInfo(start, end));
if (isRedirect(httpResponse)) {
Header[] locationHeaders = httpResponse.getHeaders("Location");
String location = locationHeaders[0].getValue();
log.debug("Redirecting to: " + location);
redirectUri = URI.create(location);
httpRequest.setURI(redirectUri);
} else {
IResponse response = toSdkResponse(httpResponse);
currentStatus = response.getHttpStatus();
/*
if (currentStatus == 429) {
throw new RestException("Too Many Requests. Exceeded request rate limit in the allotted amount of time.", null, 429);
}
if (currentStatus == 401) {
throw new RestException("Failed to authenticate", null, 401);
}
*/
if (verifyResponse && responseVerifier != null) {
if (apiKey instanceof ITokenKey) {
responseVerifier.verify(response, responseAuthenticator, (ITokenKey)apiKey, nonce);
}
}
// if (!response.isServerError() || retryCount > this.numRetries) {
return response;
// }
//otherwise allow the loop to continue to execute a retry request
}
} catch (Throwable t) {
log.warn("Unable to execute HTTP request: " + t.getMessage());
if (t instanceof RestException) {
exception = (RestException)t;
}
if (!shouldRetry(httpRequest, t, retryCount)) {
String msg = "Unable to execute HTTP request, server response code attached." +
"A cause of type DigestVerificationFailedException signifies the IDX services were either not reached or the response was tampered with." +
"Common causes for not reaching the IDX services are 401 (Unauthorized ) or 404 (Resource not found)";
throw new RestException(msg, t, currentStatus);
}
} finally {
try {
httpResponse.getEntity().getContent().close();
} catch (Throwable ignored) {
}
}
}
}
public static String toString(InputStream is) throws IOException
{
if (is == null)
{
return null;
}
try {
return new java.util.Scanner(is, "UTF-8").useDelimiter("\\A").next();
} catch (java.util.NoSuchElementException e) {
if (log.isDebugEnabled()) {
log.debug(MessageFormat.format("Failed to parse SDK response", new Object[] {}), e);
}
return null;
}
}
private boolean isRedirect(org.apache.http.HttpResponse response) {
int status = response.getStatusLine().getStatusCode();
return (status == HttpStatus.SC_MOVED_PERMANENTLY ||
status == HttpStatus.SC_MOVED_TEMPORARILY ||
status == HttpStatus.SC_TEMPORARY_REDIRECT) &&
response.getHeaders("Location") != null &&
response.getHeaders("Location").length > 0;
}
/*private boolean isRequestSuccessful(org.apache.http.HttpResponse response) {
int status = response.getStatusLine().getStatusCode();
return status >= 200 && status < 300;
}*/
/**
* Exponential sleep on failed request to avoid flooding a service with
* retries.
*
* @param retries Current retry count.
* @param previousException Exception information for the previous attempt, if any.
*/
private void pauseExponentially(int retries, RestException previousException) {
long delay;
if (backoffStrategy != null) {
delay = this.backoffStrategy.getDelayMillis(retries);
} else {
long scaleFactor = 300;
if (previousException != null && isThrottlingException(previousException)) {
scaleFactor = 500 + random.nextInt(100);
}
delay = (long) (Math.pow(2, retries) * scaleFactor);
}
delay = Math.min(delay, MAX_BACKOFF_IN_MILLISECONDS);
if (log.isDebugEnabled()) {
log.debug("Retryable condition detected, will retry in " + delay + "ms, attempt number: " + retries);
}
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RestException(e.getMessage(), e);
}
}
/**
* Returns true if a failed request should be retried.
*
* @param method The current HTTP method being executed.
* @param t The throwable from the failed request.
* @param retries The number of times the current request has been attempted.
* @return True if the failed request should be retried.
*/
private boolean shouldRetry(HttpRequestBase method, Throwable t, int retries) {
if (retries > this.numRetries) {
return false;
}
if (method instanceof HttpEntityEnclosingRequest) {
HttpEntity entity = ((HttpEntityEnclosingRequest) method).getEntity();
if (entity != null && !entity.isRepeatable()) {
return false;
}
}
if (t instanceof NoHttpResponseException ||
t instanceof SocketException ||
t instanceof SocketTimeoutException) {
if (log.isDebugEnabled()) {
log.debug("Retrying on " + t.getClass().getName()
+ ": " + t.getMessage());
}
return true;
}
if (t instanceof RestException) {
RestException re = (RestException) t;
/*
* Throttling is reported as a 429 error. To try
* and smooth out an occasional throttling error, we'll pause and
* retry, hoping that the pause is long enough for the request to
* get through the next time.
*/
if (isThrottlingException(re)) return true;
}
return false;
}
/**
* Returns {@code true} if the exception resulted from a throttling error, {@code false} otherwise.
*
* @param re The exception to test.
* @return {@code true} if the exception resulted from a throttling error, {@code false} otherwise.
*/
private boolean isThrottlingException(RestException re) {
String msg = re.getMessage();
return msg != null && msg.contains("HTTP 429");
}
public static IResponse toSdkResponse(HttpResponse httpResponse) throws IOException
{
int httpStatus = httpResponse.getStatusLine().getStatusCode();
HttpHeaders headers = getHeaders(httpResponse);
MediaType mediaType = headers.getContentType();
HttpEntity entity = httpResponse.getEntity();
InputStream body = entity != null ? entity.getContent() : null;
long contentLength = entity != null ? entity.getContentLength() : -1;
//ensure that the content has been fully acquired before closing the http stream
if (body != null) {
String stringBody = toString(body);
if (stringBody != null) {
body = new StringInputStream(stringBody);
}
}
IResponse result = new DefaultResponse(httpStatus, mediaType, body, contentLength);
result.setHeaders(headers);
return result;
}
public static HttpHeaders getHeaders(HttpResponse response)
{
HttpHeaders headers = new HttpHeaders();
Header[] httpHeaders = response.getAllHeaders();
if (httpHeaders != null) {
for (Header httpHeader : httpHeaders) {
headers.add(httpHeader.getName(), httpHeader.getValue());
}
}
return headers;
}
public int getMaxConnPerRoute() {
return maxConnPerRoute;
}
public void setMaxConnPerRoute(int maxConnPerRoute) {
this.maxConnPerRoute = maxConnPerRoute;
}
public int getMaxConnTotal() {
return maxConnTotal;
}
public void setMaxConnTotal(int maxConnTotal) {
this.maxConnTotal = maxConnTotal;
}
public int getConnectionTimeout() {
return CONNECTION_TIMEOUT;
}
public void setConnectionTimeout(int connectionTimeout) {
CONNECTION_TIMEOUT = connectionTimeout;
}
}