All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.identityx.auth.client.HttpClientRequestExecutor Maven / Gradle / Ivy

Go to download

Client library used for adding authentication to http messages as required by IdentityX Rest Services

There is a newer version: 5.6.0.2
Show newest version
/*
* 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;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy