Java tutorial
/* * 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; } } }