org.forgerock.openig.http.HttpClient.java Source code

Java tutorial

Introduction

Here is the source code for org.forgerock.openig.http.HttpClient.java

Source

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2009 Sun Microsystems Inc.
 * Portions Copyright 20102011 ApexIdentity Inc.
 * Portions Copyright 2011-2014 ForgeRock AS.
 */

package org.forgerock.openig.http;

import static java.lang.String.format;
import static org.forgerock.util.Utils.closeSilently;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Arrays;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

import org.apache.http.Header;
import org.apache.http.HeaderIterator;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.client.protocol.RequestAddCookies;
import org.apache.http.client.protocol.RequestProxyAuthentication;
import org.apache.http.client.protocol.RequestTargetAuthentication;
import org.apache.http.client.protocol.ResponseProcessCookies;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.params.ConnPerRouteBean;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.NoConnectionReuseStrategy;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.openig.header.ConnectionHeader;
import org.forgerock.openig.header.ContentEncodingHeader;
import org.forgerock.openig.header.ContentLengthHeader;
import org.forgerock.openig.header.ContentTypeHeader;
import org.forgerock.openig.heap.HeapException;
import org.forgerock.openig.heap.NestedHeaplet;
import org.forgerock.openig.io.BranchingStreamWrapper;
import org.forgerock.openig.io.TemporaryStorage;
import org.forgerock.openig.log.Logger;
import org.forgerock.openig.util.CaseInsensitiveSet;
import org.forgerock.openig.util.NoRetryHttpRequestRetryHandler;

/**
 * Submits requests to remote servers. In this implementation, requests are
 * dispatched through the <a href="http://hc.apache.org/">Apache
 * HttpComponents</a> client.
 * <p>
 * <pre>
 *   {
 *     "name": "HttpClient",
 *     "type": "HttpClient",
 *     "config": {
 *       "connections": 64,
 *       "disableReuseConnection": true,
 *       "disableRetries": true,
 *       "keystore": {
 *           "file": "/path/to/keystore.jks",
 *           "password": "changeit"
 *       },
 *       "truststore": {
 *           "file": "/path/to/keystore.jks",
 *           "password": "changeit"
 *       }
 *     }
 *   }
 * </pre>
 * <p>
 * <strong>Note:</strong> This implementation does not verify hostnames for
 * outgoing SSL connections. This is because the gateway will usually access the
 * SSL endpoint using a raw IP address rather than a fully-qualified hostname.
 */
public class HttpClient {

    /**
     * Key to retrieve an {@link HttpClient} instance from the {@link org.forgerock.openig.heap.Heap}.
     */
    public static final String HTTP_CLIENT_HEAP_KEY = "HttpClient";

    /** Reuse of Http connection is disabled by default. */
    public static final boolean DISABLE_CONNECTION_REUSE = false;

    /** Http connection retries are disabled by default. */
    public static final boolean DISABLE_RETRIES = false;

    /** Default maximum number of collections through HTTP client. */
    public static final int DEFAULT_CONNECTIONS = 64;

    /** A request that encloses an entity. */
    private static class EntityRequest extends HttpEntityEnclosingRequestBase {
        private final String method;

        public EntityRequest(final Request request) {
            this.method = request.method;
            final InputStreamEntity entity = new InputStreamEntity(request.entity,
                    new ContentLengthHeader(request).getLength());
            entity.setContentType(new ContentTypeHeader(request).toString());
            entity.setContentEncoding(new ContentEncodingHeader(request).toString());
            setEntity(entity);
        }

        @Override
        public String getMethod() {
            return method;
        }
    }

    /** A request that does not enclose an entity. */
    private static class NonEntityRequest extends HttpRequestBase {
        private final String method;

        public NonEntityRequest(final Request request) {
            this.method = request.method;
            final Header[] contentLengthHeader = getHeaders(ContentLengthHeader.NAME);
            if ((contentLengthHeader == null || contentLengthHeader.length == 0)
                    && ("PUT".equals(method) || "POST".equals(method) || "PROPFIND".equals(method))) {
                setHeader(ContentLengthHeader.NAME, "0");
            }
        }

        @Override
        public String getMethod() {
            return method;
        }
    }

    /** Headers that are suppressed in request. */
    // FIXME: How should the the "Expect" header be handled?
    private static final CaseInsensitiveSet SUPPRESS_REQUEST_HEADERS = new CaseInsensitiveSet(Arrays.asList(
            // populated in outgoing request by EntityRequest (HttpEntityEnclosingRequestBase):
            "Content-Encoding", "Content-Length", "Content-Type",
            // hop-to-hop headers, not forwarded by proxies, per RFC 2616 13.5.1:
            "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailers",
            "Transfer-Encoding", "Upgrade"));

    /** Headers that are suppressed in response. */
    private static final CaseInsensitiveSet SUPPRESS_RESPONSE_HEADERS = new CaseInsensitiveSet(Arrays.asList(
            // hop-to-hop headers, not forwarded by proxies, per RFC 2616 13.5.1:
            "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailers",
            "Transfer-Encoding", "Upgrade"));

    /**
     * Returns a new SSL socket factory that does not perform hostname verification.
     *
     * @param keyManagerFactory
     *         Provides Keys/Certificates in case of SSL/TLS connections
     * @param trustManagerFactory
     *         Provides TrustManagers in case of SSL/TLS connections
     * @throws GeneralSecurityException
     *         if the SSL algorithm is unsupported or if an error occurs during SSL configuration
     */
    private static SSLSocketFactory newSSLSocketFactory(final KeyManagerFactory keyManagerFactory,
            final TrustManagerFactory trustManagerFactory) throws GeneralSecurityException {
        SSLContext context = SSLContext.getInstance("TLS");
        context.init((keyManagerFactory == null) ? null : keyManagerFactory.getKeyManagers(),
                (trustManagerFactory == null) ? null : trustManagerFactory.getTrustManagers(), null);
        SSLSocketFactory factory = new SSLSocketFactory(context);
        factory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
        return factory;
    }

    /** The HTTP client to transmit requests through. */
    private final DefaultHttpClient httpClient;
    /**
     * Allocates temporary buffers for caching streamed content during request
     * processing.
     */
    private final TemporaryStorage storage;

    /**
     * Creates a new client handler which will cache at most 64 connections.
     *
     * @param storage the TemporaryStorage to use
     * @throws GeneralSecurityException
     *         if the SSL algorithm is unsupported or if an error occurs during SSL configuration
     */
    public HttpClient(final TemporaryStorage storage) throws GeneralSecurityException {
        this(storage, DEFAULT_CONNECTIONS, null, null);
    }

    /**
     * Creates a new client handler with the specified maximum number of cached connections.
     *
     * @param storage the {@link TemporaryStorage} to use
     * @param connections the maximum number of connections to open.
     * @param keyManagerFactory Provides Keys/Certificates in case of SSL/TLS connections
     * @param trustManagerFactory Provides TrustManagers in case of SSL/TLS connections
     * @throws GeneralSecurityException
     *         if the SSL algorithm is unsupported or if an error occurs during SSL configuration
     */
    public HttpClient(final TemporaryStorage storage, final int connections,
            final KeyManagerFactory keyManagerFactory, final TrustManagerFactory trustManagerFactory)
            throws GeneralSecurityException {
        this.storage = storage;

        final BasicHttpParams parameters = new BasicHttpParams();
        final int maxConnections = connections <= 0 ? DEFAULT_CONNECTIONS : connections;
        ConnManagerParams.setMaxTotalConnections(parameters, maxConnections);
        ConnManagerParams.setMaxConnectionsPerRoute(parameters, new ConnPerRouteBean(maxConnections));
        HttpProtocolParams.setVersion(parameters, HttpVersion.HTTP_1_1);
        HttpClientParams.setRedirecting(parameters, false);

        final SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        registry.register(new Scheme("https", newSSLSocketFactory(keyManagerFactory, trustManagerFactory), 443));
        final ClientConnectionManager connectionManager = new ThreadSafeClientConnManager(parameters, registry);

        httpClient = new DefaultHttpClient(connectionManager, parameters);
        httpClient.removeRequestInterceptorByClass(RequestAddCookies.class);
        httpClient.removeRequestInterceptorByClass(RequestProxyAuthentication.class);
        httpClient.removeRequestInterceptorByClass(RequestTargetAuthentication.class);
        httpClient.removeResponseInterceptorByClass(ResponseProcessCookies.class);
        // TODO: set timeout to drop stalled connections?
    }

    /**
     * Disables connection caching.
     *
     * @return this HTTP client.
     */
    public HttpClient disableConnectionReuse() {
        httpClient.setReuseStrategy(new NoConnectionReuseStrategy());
        return this;
    }

    /**
     * Disables automatic retrying of failed requests.
     *
     * @param logger a logger which should be used for logging the reason that a
     * request failed.
     * @return this HTTP client.
     */
    public HttpClient disableRetries(final Logger logger) {
        httpClient.setHttpRequestRetryHandler(new NoRetryHttpRequestRetryHandler(logger));
        return this;
    }

    /**
     * Submits the exchange request to the remote server. Creates and populates
     * the exchange response from that provided by the remote server.
     *
     * @param exchange The HTTP exchange containing the request to send and where the
     * response will be placed.
     * @throws IOException If an IO error occurred while performing the request.
     */
    public void execute(final Exchange exchange) throws IOException {
        // recover any previous response connection, if present
        if (exchange.response != null && exchange.response.entity != null) {
            exchange.response.entity.close();
        }
        exchange.response = execute(exchange.request);
    }

    /**
     * Submits the request to the remote server. Creates and populates the
     * response from that provided by the remote server.
     *
     * @param request The HTTP request to send.
     * @return The HTTP response.
     * @throws IOException If an IO error occurred while performing the request.
     */
    public Response execute(final Request request) throws IOException {
        final HttpRequestBase clientRequest = request.entity != null ? new EntityRequest(request)
                : new NonEntityRequest(request);
        clientRequest.setURI(request.uri);
        // connection headers to suppress
        final CaseInsensitiveSet suppressConnection = new CaseInsensitiveSet();
        // parse request connection headers to be suppressed in request
        suppressConnection.addAll(new ConnectionHeader(request).getTokens());
        // request headers
        for (final String name : request.headers.keySet()) {
            if (!SUPPRESS_REQUEST_HEADERS.contains(name) && !suppressConnection.contains(name)) {
                for (final String value : request.headers.get(name)) {
                    clientRequest.addHeader(name, value);
                }
            }
        }
        // send request
        final HttpResponse clientResponse = httpClient.execute(clientRequest);
        final Response response = new Response();
        // response entity
        final HttpEntity clientResponseEntity = clientResponse.getEntity();
        if (clientResponseEntity != null) {
            response.entity = new BranchingStreamWrapper(clientResponseEntity.getContent(), storage);
        }
        // response status line
        final StatusLine statusLine = clientResponse.getStatusLine();
        response.version = statusLine.getProtocolVersion().toString();
        response.status = statusLine.getStatusCode();
        response.reason = statusLine.getReasonPhrase();
        // parse response connection headers to be suppressed in response
        suppressConnection.clear();
        suppressConnection.addAll(new ConnectionHeader(response).getTokens());
        // response headers
        for (final HeaderIterator i = clientResponse.headerIterator(); i.hasNext();) {
            final Header header = i.nextHeader();
            final String name = header.getName();
            if (!SUPPRESS_RESPONSE_HEADERS.contains(name) && !suppressConnection.contains(name)) {
                response.headers.add(name, header.getValue());
            }
        }
        // TODO: decide if need to try-finally to call httpRequest.abort?
        return response;
    }

    /**
     * Creates and initializes a http client object in a heap environment.
     */
    public static class Heaplet extends NestedHeaplet {

        @Override
        public Object create() throws HeapException {
            // optional, default to DEFAULT_CONNECTIONS number of connections
            Integer connections = config.get("connections").defaultTo(DEFAULT_CONNECTIONS).asInteger();
            // determines if connections should be reused, disables keep-alive
            Boolean disableReuseConnection = config.get("disableReuseConnection")
                    .defaultTo(DISABLE_CONNECTION_REUSE).asBoolean();
            // determines if requests should be retried on failure
            Boolean disableRetries = config.get("disableRetries").defaultTo(DISABLE_RETRIES).asBoolean();

            // Build an optional KeyManagerFactory
            KeyManagerFactory keyManagerFactory = null;
            if (config.isDefined("keystore")) {
                JsonValue store = config.get("keystore");
                File keystoreFile = store.get("file").required().asFile();
                String password = store.get("password").required().asString();

                keyManagerFactory = buildKeyManagerFactory(keystoreFile, password);
            }

            // Build an optional TrustManagerFactory
            TrustManagerFactory trustManagerFactory = null;
            if (config.isDefined("truststore")) {
                JsonValue store = config.get("truststore");
                File truststoreFile = store.get("file").required().asFile();
                String password = store.get("password").asString();

                trustManagerFactory = buildTrustManagerFactory(truststoreFile, password);
            }

            // Create the HttpClient instance
            try {
                HttpClient client = new HttpClient(storage, connections, keyManagerFactory, trustManagerFactory);

                if (disableRetries) {
                    client.disableRetries(logger);
                }
                if (disableReuseConnection) {
                    client.disableConnectionReuse();
                }

                return client;
            } catch (GeneralSecurityException e) {
                throw new HeapException(format("Cannot build HttpClient named '%s'", name), e);
            }
        }

        private TrustManagerFactory buildTrustManagerFactory(final File truststoreFile, final String password)
                throws HeapException {
            try {
                TrustManagerFactory factory = TrustManagerFactory.getInstance("SunX509");
                KeyStore store = buildJksKeyStore(truststoreFile, password);
                factory.init(store);
                return factory;
            } catch (Exception e) {
                throw new HeapException(
                        format("Cannot build TrustManagerFactory from trust store: %s", truststoreFile), e);
            }
        }

        private KeyManagerFactory buildKeyManagerFactory(final File keystoreFile, final String password)
                throws HeapException {
            try {
                KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
                KeyStore keyStore = buildJksKeyStore(keystoreFile, password);
                keyManagerFactory.init(keyStore, password.toCharArray());
                return keyManagerFactory;
            } catch (Exception e) {
                throw new HeapException(format("Cannot build KeyManagerFactory from key store: %s", keystoreFile),
                        e);
            }
        }

        private KeyStore buildJksKeyStore(final File keystoreFile, final String password) throws Exception {
            KeyStore keyStore = KeyStore.getInstance("JKS");
            InputStream keyInput = null;
            try {
                keyInput = new FileInputStream(keystoreFile);
                char[] credentials = (password == null) ? null : password.toCharArray();
                keyStore.load(keyInput, credentials);
            } finally {
                closeSilently(keyInput);
            }
            return keyStore;
        }
    }

}