001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2015 ForgeRock AS.
015 */
016
017package org.forgerock.http.apache.async;
018
019import static java.util.concurrent.TimeUnit.MILLISECONDS;
020import static org.forgerock.http.handler.HttpClientHandler.OPTION_CONNECT_TIMEOUT;
021import static org.forgerock.http.handler.HttpClientHandler.OPTION_HOSTNAME_VERIFIER;
022import static org.forgerock.http.handler.HttpClientHandler.OPTION_KEY_MANAGERS;
023import static org.forgerock.http.handler.HttpClientHandler.OPTION_MAX_CONNECTIONS;
024import static org.forgerock.http.handler.HttpClientHandler.OPTION_REUSE_CONNECTIONS;
025import static org.forgerock.http.handler.HttpClientHandler.OPTION_SO_TIMEOUT;
026import static org.forgerock.http.handler.HttpClientHandler.OPTION_SSLCONTEXT_ALGORITHM;
027import static org.forgerock.http.handler.HttpClientHandler.OPTION_SSL_CIPHER_SUITES;
028import static org.forgerock.http.handler.HttpClientHandler.OPTION_SSL_ENABLED_PROTOCOLS;
029import static org.forgerock.http.handler.HttpClientHandler.OPTION_TEMPORARY_STORAGE;
030import static org.forgerock.http.handler.HttpClientHandler.OPTION_TRUST_MANAGERS;
031import static org.forgerock.http.util.Lists.asArrayOrNull;
032
033import java.security.GeneralSecurityException;
034import java.util.List;
035
036import javax.net.ssl.HostnameVerifier;
037import javax.net.ssl.SSLContext;
038
039import org.apache.http.HttpRequest;
040import org.apache.http.HttpResponse;
041import org.apache.http.ProtocolException;
042import org.apache.http.client.RedirectStrategy;
043import org.apache.http.client.methods.HttpUriRequest;
044import org.apache.http.config.Registry;
045import org.apache.http.config.RegistryBuilder;
046import org.apache.http.conn.ssl.DefaultHostnameVerifier;
047import org.apache.http.conn.ssl.NoopHostnameVerifier;
048import org.apache.http.impl.NoConnectionReuseStrategy;
049import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
050import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
051import org.apache.http.impl.nio.client.HttpAsyncClients;
052import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
053import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
054import org.apache.http.impl.nio.reactor.IOReactorConfig;
055import org.apache.http.nio.conn.NoopIOSessionStrategy;
056import org.apache.http.nio.conn.SchemeIOSessionStrategy;
057import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
058import org.apache.http.nio.reactor.ConnectingIOReactor;
059import org.apache.http.nio.reactor.IOReactorException;
060import org.apache.http.protocol.HttpContext;
061import org.forgerock.http.HttpApplicationException;
062import org.forgerock.http.apache.NoAuthenticationStrategy;
063import org.forgerock.http.io.Buffer;
064import org.forgerock.http.spi.HttpClient;
065import org.forgerock.http.spi.HttpClientProvider;
066import org.forgerock.util.Factory;
067import org.forgerock.util.Option;
068import org.forgerock.util.Options;
069import org.forgerock.util.time.Duration;
070
071/**
072 * Creates and configures a {@link HttpClient} instance built around Apache HTTP Async Client component.
073 *
074 * @see <a href="https://hc.apache.org/httpcomponents-asyncclient-dev/index.html">Apache HTTP Async Client</a>
075 */
076public class AsyncHttpClientProvider implements HttpClientProvider {
077
078    /**
079     * Specify the number of worker threads. If not set, the async client implementation manages this setting itself
080     * (by default this is number of CPU + 1).
081     */
082    public static final Option<Integer> OPTION_WORKER_THREADS = Option.of(Integer.class, null);
083
084    /**
085     * A redirect strategy that never performs a redirect.
086     */
087    private static final RedirectStrategy DISABLE_REDIRECT = new RedirectStrategy() {
088        @Override
089        public boolean isRedirected(final HttpRequest request, final HttpResponse response,
090                final HttpContext context) throws ProtocolException {
091            return false;
092        }
093
094        @Override
095        public HttpUriRequest getRedirect(final HttpRequest request, final HttpResponse response,
096                final HttpContext context) throws ProtocolException {
097            return null;
098        }
099    };
100
101    @Override
102    public HttpClient newHttpClient(final Options options) throws HttpApplicationException {
103
104        final Factory<Buffer> storage = options.get(OPTION_TEMPORARY_STORAGE);
105
106        // SSL
107        final SSLContext sslContext;
108        try {
109            sslContext = SSLContext.getInstance(options.get(OPTION_SSLCONTEXT_ALGORITHM));
110            sslContext.init(options.get(OPTION_KEY_MANAGERS),
111                            options.get(OPTION_TRUST_MANAGERS), null);
112        } catch (final GeneralSecurityException e) {
113            throw new HttpApplicationException("Can't create SSL Context", e);
114        }
115
116        HostnameVerifier verifier = new DefaultHostnameVerifier();
117        switch (options.get(OPTION_HOSTNAME_VERIFIER)) {
118        case ALLOW_ALL:
119            verifier = NoopHostnameVerifier.INSTANCE;
120            break;
121        }
122
123        List<String> protocols = options.get(OPTION_SSL_ENABLED_PROTOCOLS);
124        List<String> ciphers = options.get(OPTION_SSL_CIPHER_SUITES);
125
126        // Create a registry of custom connection session strategies for supported protocol schemes
127        Registry<SchemeIOSessionStrategy> registry =
128                RegistryBuilder.<SchemeIOSessionStrategy>create()
129                        .register("http", NoopIOSessionStrategy.INSTANCE)
130                        .register("https", new SSLIOSessionStrategy(sslContext, asArrayOrNull(protocols),
131                                asArrayOrNull(ciphers), verifier))
132                        .build();
133
134        // Timeouts
135        final Duration soTimeout = options.get(OPTION_SO_TIMEOUT);
136        final Duration connectTimeout = options.get(OPTION_CONNECT_TIMEOUT);
137        // FIXME GSA Can we support requestConnectTimeout ?
138
139        // Create I/O reactor configuration
140        IOReactorConfig.Builder reactorBuilder = IOReactorConfig.custom();
141
142        if (!connectTimeout.isUnlimited()) {
143            reactorBuilder.setConnectTimeout((int) connectTimeout.to(MILLISECONDS));
144        }
145        if (!soTimeout.isUnlimited()) {
146            reactorBuilder.setSoTimeout((int) soTimeout.to(MILLISECONDS));
147        }
148        Integer threadCount = options.get(OPTION_WORKER_THREADS);
149        if (threadCount != null) {
150            reactorBuilder.setIoThreadCount(threadCount);
151        }
152        IOReactorConfig ioReactorConfig = reactorBuilder.build();
153
154        // Create a custom I/O reactor
155        ConnectingIOReactor reactor;
156        try {
157            reactor = new DefaultConnectingIOReactor(ioReactorConfig);
158        } catch (IOReactorException e) {
159            throw new HttpApplicationException("Cannot create I/O Reactor", e);
160        }
161
162        // Create a connection manager with custom configuration.
163        PoolingNHttpClientConnectionManager manager = new PoolingNHttpClientConnectionManager(reactor, registry);
164
165        // Connection pooling
166        final int maxConnections = options.get(OPTION_MAX_CONNECTIONS);
167        manager.setMaxTotal(maxConnections);
168        manager.setDefaultMaxPerRoute(maxConnections);
169
170        // FIXME GSA Couldn't find how to configure retries in async http client
171        //if (!options.get(OPTION_RETRY_REQUESTS)) {
172        //    builder.disableAutomaticRetries();
173        //}
174
175        // Create a client with the given custom dependencies and configuration.
176        HttpAsyncClientBuilder builder = HttpAsyncClients.custom();
177
178        if (!options.get(OPTION_REUSE_CONNECTIONS)) {
179            builder.setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE);
180        }
181
182        // TODO Uncomment when we'll have a user-agent Option
183        // builder.setUserAgent("CHF/1.0");
184
185        CloseableHttpAsyncClient client = builder.setConnectionManager(manager)
186                .disableCookieManagement()
187                .setRedirectStrategy(DISABLE_REDIRECT)
188                .setTargetAuthenticationStrategy(NoAuthenticationStrategy.INSTANCE)
189                .setProxyAuthenticationStrategy(NoAuthenticationStrategy.INSTANCE)
190                .build();
191        client.start();
192        return new AsyncHttpClient(client, storage);
193    }
194}