From 113d40ef3957833a9c488306909a59baf99863fb Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Thu, 23 Feb 2017 14:32:43 +0000 Subject: [PATCH] New non-blocking connection management APIs git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1784139 13f79535-47bb-0310-9956-ffa450edef68 --- httpclient5/pom.xml | 5 + .../hc/client5/http/impl/ComplexFuture.java | 81 ++++ .../hc/client5/http/impl/ConnPoolSupport.java | 2 +- .../nio/AsyncClientConnectionOperator.java | 172 +++++++ .../nio/ManagedAsyncClientConnection.java | 186 ++++++++ .../PoolingAsyncClientConnectionManager.java | 445 ++++++++++++++++++ ...ngAsyncClientConnectionManagerBuilder.java | 203 ++++++++ .../nio/AsyncClientConnectionManager.java | 127 +++++ .../http/nio/AsyncConnectionEndpoint.java | 97 ++++ .../hc/client5/http/ssl/H2TlsSupport.java | 377 +++++++++++++++ .../client5/http/ssl/SSLUpgradeStrategy.java | 231 +++++++++ pom.xml | 5 + 12 files changed, 1930 insertions(+), 1 deletion(-) create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/ComplexFuture.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/AsyncClientConnectionOperator.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/ManagedAsyncClientConnection.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManagerBuilder.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/nio/AsyncClientConnectionManager.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/nio/AsyncConnectionEndpoint.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/ssl/H2TlsSupport.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLUpgradeStrategy.java diff --git a/httpclient5/pom.xml b/httpclient5/pom.xml index 493c364a0..79c0046ea 100644 --- a/httpclient5/pom.xml +++ b/httpclient5/pom.xml @@ -42,6 +42,11 @@ httpcore5 compile + + org.apache.httpcomponents.core5 + httpcore5-h2 + compile + org.apache.logging.log4j log4j-api diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ComplexFuture.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ComplexFuture.java new file mode 100644 index 000000000..2a0151835 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ComplexFuture.java @@ -0,0 +1,81 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.impl; + +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hc.core5.concurrent.BasicFuture; +import org.apache.hc.core5.concurrent.Cancellable; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.util.Args; + +public final class ComplexFuture extends BasicFuture { + + private final AtomicReference dependencyRef; + + public ComplexFuture(final FutureCallback callback) { + super(callback); + this.dependencyRef = new AtomicReference<>(null); + } + + public void setDependency(final Cancellable dependency) { + Args.notNull(dependency, "dependency"); + if (isDone()) { + dependency.cancel(); + } else { + dependencyRef.set(dependency); + } + } + + public void setDependency(final Future dependency) { + Args.notNull(dependency, "dependency"); + setDependency(new Cancellable() { + + @Override + public boolean cancel() { + return dependency.cancel(true); + } + + }); + } + + public void clearDependency() { + dependencyRef.set(null); + } + + @Override + public boolean cancel(final boolean mayInterruptIfRunning) { + final boolean cancelled = super.cancel(mayInterruptIfRunning); + final Cancellable dependency = dependencyRef.getAndSet(null); + if (dependency != null) { + dependency.cancel(); + } + return cancelled; + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ConnPoolSupport.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ConnPoolSupport.java index f6ab7d1d3..1727b0b23 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ConnPoolSupport.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ConnPoolSupport.java @@ -40,7 +40,7 @@ public final class ConnPoolSupport { if (object instanceof Identifiable) { return ((Identifiable) object).getId(); } else { - return Integer.toHexString(System.identityHashCode(object)); + return object.getClass().getSimpleName() + "-" + Integer.toHexString(System.identityHashCode(object)); } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/AsyncClientConnectionOperator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/AsyncClientConnectionOperator.java new file mode 100644 index 000000000..c869d8850 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/AsyncClientConnectionOperator.java @@ -0,0 +1,172 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl.nio; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hc.client5.http.ConnectTimeoutException; +import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.HttpHostConnectException; +import org.apache.hc.client5.http.SchemePortResolver; +import org.apache.hc.client5.http.SystemDefaultDnsResolver; +import org.apache.hc.client5.http.UnsupportedSchemeException; +import org.apache.hc.client5.http.impl.ComplexFuture; +import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.config.Lookup; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.reactor.ConnectionInitiator; +import org.apache.hc.core5.reactor.IOSession; +import org.apache.hc.core5.reactor.SessionRequest; +import org.apache.hc.core5.reactor.SessionRequestCallback; +import org.apache.hc.core5.util.Args; + +final class AsyncClientConnectionOperator { + + private final SchemePortResolver schemePortResolver; + private final DnsResolver dnsResolver; + private final Lookup tlsStrategyLookup; + + AsyncClientConnectionOperator( + final SchemePortResolver schemePortResolver, + final DnsResolver dnsResolver, + final Lookup tlsStrategyLookup) { + this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE; + this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE; + this.tlsStrategyLookup = tlsStrategyLookup; + } + + public Future connect( + final ConnectionInitiator connectionInitiator, + final HttpHost host, + final SocketAddress localAddress, + final long timeout, + final TimeUnit timeUnit, + final FutureCallback callback) { + Args.notNull(connectionInitiator, "Connection initiator"); + Args.notNull(host, "Host"); + final ComplexFuture future = new ComplexFuture<>(callback); + final InetAddress[] remoteAddresses; + try { + remoteAddresses = dnsResolver.resolve(host.getHostName()); + } catch (UnknownHostException ex) { + future.failed(ex); + return future; + } + final int port; + try { + port = schemePortResolver.resolve(host); + } catch (UnsupportedSchemeException ex) { + future.failed(ex); + return future; + } + final TlsStrategy tlsStrategy = tlsStrategyLookup != null ? tlsStrategyLookup.lookup(host.getSchemeName()) : null; + final Runnable runnable = new Runnable() { + + private final AtomicInteger attempt = new AtomicInteger(0); + + void executeNext() { + final int index = attempt.getAndIncrement(); + final InetSocketAddress remoteAddress = new InetSocketAddress(remoteAddresses[index], port); + final SessionRequest sessionRequest = connectionInitiator.connect( + host, + // TODO: fix after upgrading to HttpCore 5.0a3 + // TODO: remoteAddress + localAddress, + null, new SessionRequestCallback() { + + @Override + public void completed(final SessionRequest request) { + final IOSession session = request.getSession(); + final ManagedAsyncClientConnection connection = new ManagedAsyncClientConnection(session); + if (tlsStrategy != null) { + tlsStrategy.upgrade( + connection, + host.getHostName(), + session.getLocalAddress(), + session.getRemoteAddress()); + } + future.completed(connection); + } + + @Override + public void failed(final SessionRequest request) { + if (attempt.get() >= remoteAddresses.length) { + future.failed(new HttpHostConnectException(request.getException(), host, remoteAddresses)); + } else { + executeNext(); + } + } + + @Override + public void timeout(final SessionRequest request) { + future.failed(new ConnectTimeoutException(new SocketException(), host, remoteAddresses)); + } + + @Override + public void cancelled(final SessionRequest request) { + future.cancel(); + } + + }); + future.setDependency(sessionRequest); + final int connectTimeout = (int) (timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS).toMillis(timeout); + sessionRequest.setConnectTimeout(connectTimeout); + } + + @Override + public void run() { + executeNext(); + } + + }; + runnable.run(); + return future; + } + + public void upgrade(final ManagedAsyncClientConnection connection, final HttpHost host) { + final TlsStrategy tlsStrategy = tlsStrategyLookup != null ? tlsStrategyLookup.lookup(host.getHostName()) : null; + if (tlsStrategy != null) { + tlsStrategy.upgrade( + connection, + // TODO: fix after upgrading to HttpCore 5.0a3 + host.getHostName(), + connection.getLocalAddress(), + connection.getRemoteAddress()); + } + + } +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/ManagedAsyncClientConnection.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/ManagedAsyncClientConnection.java new file mode 100644 index 000000000..d7d4c99ac --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/ManagedAsyncClientConnection.java @@ -0,0 +1,186 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl.nio; + +import java.io.IOException; +import java.net.SocketAddress; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; + +import org.apache.hc.client5.http.impl.ConnPoolSupport; +import org.apache.hc.client5.http.ssl.H2TlsSupport; +import org.apache.hc.client5.http.utils.Identifiable; +import org.apache.hc.core5.http.HttpConnection; +import org.apache.hc.core5.http.HttpConnectionMetrics; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.ProtocolVersion; +import org.apache.hc.core5.reactor.Command; +import org.apache.hc.core5.reactor.IOEventHandler; +import org.apache.hc.core5.reactor.IOSession; +import org.apache.hc.core5.reactor.ssl.SSLBufferManagement; +import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer; +import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier; +import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +final class ManagedAsyncClientConnection implements Identifiable, HttpConnection, TransportSecurityLayer { + + private final Logger log = LogManager.getLogger(getClass()); + + private final IOSession ioSession; + private final AtomicBoolean closed; + + public ManagedAsyncClientConnection(final IOSession ioSession) { + this.ioSession = ioSession; + this.closed = new AtomicBoolean(); + } + + @Override + public String getId() { + return ConnPoolSupport.getId(ioSession); + } + + @Override + public void shutdown() throws IOException { + if (this.closed.compareAndSet(false, true)) { + if (log.isDebugEnabled()) { + log.debug(getId() + ": Shutdown connection"); + } + ioSession.shutdown(); + } + } + + @Override + public void close() throws IOException { + if (this.closed.compareAndSet(false, true)) { + if (log.isDebugEnabled()) { + log.debug(getId() + ": Close connection"); + } + ioSession.close(); + } + } + + @Override + public boolean isOpen() { + return !ioSession.isClosed(); + } + + @Override + public void setSocketTimeout(final int timeout) { + ioSession.setSocketTimeout(timeout); + } + + @Override + public int getSocketTimeout() { + return ioSession.getSocketTimeout(); + } + + @Override + public SocketAddress getRemoteAddress() { + return ioSession.getRemoteAddress(); + } + + @Override + public SocketAddress getLocalAddress() { + return ioSession.getLocalAddress(); + } + + @Override + public HttpConnectionMetrics getMetrics() { + final IOEventHandler handler = ioSession.getHandler(); + if (handler instanceof HttpConnection) { + return ((HttpConnection) handler).getMetrics(); + } else { + return null; + } + } + + @Override + public ProtocolVersion getProtocolVersion() { + final IOEventHandler handler = ioSession.getHandler(); + if (handler instanceof HttpConnection) { + return ((HttpConnection) handler).getProtocolVersion(); + } else { + return HttpVersion.DEFAULT; + } + } + + @Override + public void start( + final SSLContext sslContext, + final SSLBufferManagement sslBufferManagement, + final SSLSessionInitializer initializer, + final SSLSessionVerifier verifier) throws UnsupportedOperationException { + if (log.isDebugEnabled()) { + log.debug(getId() + ": start TLS"); + } + if (ioSession instanceof TransportSecurityLayer) { + final IOEventHandler handler = ioSession.getHandler(); + final ProtocolVersion protocolVersion; + if (handler instanceof HttpConnection) { + protocolVersion = ((HttpConnection) handler).getProtocolVersion(); + } else { + protocolVersion = null; + } + ((TransportSecurityLayer) ioSession).start( + sslContext, + sslBufferManagement, + HttpVersion.HTTP_2.greaterEquals(protocolVersion) ? H2TlsSupport.decorateInitializer(initializer) : initializer, + verifier); + } else { + throw new UnsupportedOperationException(); + } + } + + @Override + public SSLSession getSSLSession() { + if (ioSession instanceof TransportSecurityLayer) { + return ((TransportSecurityLayer) ioSession).getSSLSession(); + } else { + return null; + } + } + + public void submitPriorityCommand(final Command command) { + if (log.isDebugEnabled()) { + log.debug(getId() + ": priority command " + command); + } + ioSession.addFirst(command); + } + + public void submitCommand(final Command command) { + if (log.isDebugEnabled()) { + log.debug(getId() + ": command " + command); + } + ioSession.addLast(command); + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java new file mode 100644 index 000000000..4822f6a72 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java @@ -0,0 +1,445 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.SchemePortResolver; +import org.apache.hc.client5.http.impl.ComplexFuture; +import org.apache.hc.client5.http.impl.ConnPoolSupport; +import org.apache.hc.client5.http.impl.ConnectionShutdownException; +import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; +import org.apache.hc.client5.http.nio.AsyncConnectionEndpoint; +import org.apache.hc.client5.http.utils.Identifiable; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Callback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.ProtocolVersion; +import org.apache.hc.core5.http.config.Lookup; +import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; +import org.apache.hc.core5.http.nio.command.ExecutionCommand; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http2.nio.command.PingCommand; +import org.apache.hc.core5.http2.nio.support.BasicPingHandler; +import org.apache.hc.core5.pool.ConnPoolControl; +import org.apache.hc.core5.pool.ConnPoolListener; +import org.apache.hc.core5.pool.ConnPoolPolicy; +import org.apache.hc.core5.pool.PoolEntry; +import org.apache.hc.core5.pool.PoolStats; +import org.apache.hc.core5.pool.StrictConnPool; +import org.apache.hc.core5.reactor.ConnectionInitiator; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.Asserts; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * {@code PoolingAsyncClientConnectionManager} maintains a pool of non-blocking + * {@link org.apache.hc.core5.http.HttpConnection}s and is able to service + * connection requests from multiple execution threads. Connections are pooled + * on a per route basis. A request for a route which already the manager has + * persistent connections for available in the pool will be services by leasing + * a connection from the pool rather than creating a new connection. + *

+ * {@code PoolingAsyncClientConnectionManager} maintains a maximum limit + * of connection on a per route basis and in total. Connection limits + * can be adjusted using {@link ConnPoolControl} methods. + *

+ * Total time to live (TTL) set at construction time defines maximum life span + * of persistent connections regardless of their expiration setting. No persistent + * connection will be re-used past its TTL value. + * + * @since 5.0 + */ +@Contract(threading = ThreadingBehavior.SAFE_CONDITIONAL) +public class PoolingAsyncClientConnectionManager implements AsyncClientConnectionManager, ConnPoolControl { + + private final Logger log = LogManager.getLogger(getClass()); + + private final AsyncClientConnectionOperator connectionOperator; + private final StrictConnPool pool; + private final AtomicBoolean closed; + + private volatile int validateAfterInactivity; + + public PoolingAsyncClientConnectionManager( + final Lookup tlsStrategyLookup, + final SchemePortResolver schemePortResolver, + final DnsResolver dnsResolver, + final long timeToLive, + final TimeUnit timeUnit, + final ConnPoolPolicy policy, + final ConnPoolListener connPoolListener) { + this.connectionOperator = new AsyncClientConnectionOperator(schemePortResolver, dnsResolver, tlsStrategyLookup); + this.pool = new StrictConnPool<>(20, 50, timeToLive, timeUnit, + policy != null ? policy : ConnPoolPolicy.LIFO, connPoolListener); + this.closed = new AtomicBoolean(false); + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + log.debug("Connection manager is shutting down"); + pool.shutdown(); + log.debug("Connection manager shut down"); + } + } + + private InternalConnectionEndpoint cast(final AsyncConnectionEndpoint endpoint) { + if (endpoint instanceof InternalConnectionEndpoint) { + return (InternalConnectionEndpoint) endpoint; + } else { + throw new IllegalStateException("Unexpected endpoint class: " + endpoint.getClass()); + } + } + + @Override + public Future lease( + final HttpRoute route, + final Object state, + final long timeout, + final TimeUnit timeUnit, + final FutureCallback callback) { + if (log.isDebugEnabled()) { + log.debug("Connection request: " + ConnPoolSupport.formatStats(null, route, state, pool)); + } + final ComplexFuture resultFuture = new ComplexFuture<>(callback); + final Future> leaseFuture = pool.lease( + route, state, timeout, timeUnit, new FutureCallback>() { + + void leaseCompleted(final PoolEntry poolEntry) { + resultFuture.completed(new InternalConnectionEndpoint(poolEntry)); + if (log.isDebugEnabled()) { + log.debug("Connection leased: " + ConnPoolSupport.formatStats(poolEntry.getConnection(), route, state, pool)); + } + } + + @Override + public void completed(final PoolEntry poolEntry) { + final ManagedAsyncClientConnection connection = poolEntry.getConnection(); + if (connection != null && validateAfterInactivity > 0 && + poolEntry.getUpdated() + validateAfterInactivity <= System.currentTimeMillis()) { + final ProtocolVersion protocolVersion = connection.getProtocolVersion(); + if (HttpVersion.HTTP_2_0.greaterEquals(protocolVersion)) { + connection.submitPriorityCommand(new PingCommand(new BasicPingHandler(new Callback() { + + @Override + public void execute(final Boolean result) { + if (result == Boolean.FALSE) { + if (log.isDebugEnabled()) { + log.debug("Connection " + ConnPoolSupport.getId(connection) + " is stale"); + } + poolEntry.discardConnection(); + } + leaseCompleted(poolEntry); + } + + }))); + } else { + leaseCompleted(poolEntry); + } + } else { + leaseCompleted(poolEntry); + } + } + + @Override + public void failed(final Exception ex) { + resultFuture.failed(ex); + } + + @Override + public void cancelled() { + resultFuture.cancel(); + } + + }); + resultFuture.setDependency(leaseFuture); + return resultFuture; + } + + @Override + public void release( + final AsyncConnectionEndpoint endpoint, + final Object state, + final long keepAlive, final TimeUnit timeUnit) { + Args.notNull(endpoint, "Managed endpoint"); + final PoolEntry entry = cast(endpoint).detach(); + if (entry == null) { + return; + } + final ManagedAsyncClientConnection connection = entry.getConnection(); + boolean reusable = connection != null && connection.isOpen(); + try { + if (reusable) { + final TimeUnit effectiveUnit = timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS; + entry.updateConnection(keepAlive, effectiveUnit, state); + if (log.isDebugEnabled()) { + final String s; + if (keepAlive > 0) { + s = "for " + (double) effectiveUnit.toMillis(keepAlive) / 1000 + " seconds"; + } else { + s = "indefinitely"; + } + log.debug("Connection " + ConnPoolSupport.getId(connection) + " can be kept alive " + s); + } + } + } catch (RuntimeException ex) { + reusable = false; + throw ex; + } finally { + pool.release(entry, reusable); + if (log.isDebugEnabled()) { + log.debug("Connection released: " + ConnPoolSupport.formatStats( + connection, entry.getRoute(), entry.getState(), pool)); + } + } + } + + @Override + public Future connect( + final AsyncConnectionEndpoint endpoint, + final ConnectionInitiator connectionInitiator, + final long timeout, + final TimeUnit timeUnit, + final HttpContext context, + final FutureCallback callback) { + Args.notNull(endpoint, "Endpoint"); + Args.notNull(connectionInitiator, "Connection initiator"); + final InternalConnectionEndpoint internalEndpoint = cast(endpoint); + final ComplexFuture resultFuture = new ComplexFuture<>(callback); + if (internalEndpoint.isConnected()) { + resultFuture.completed(endpoint); + return resultFuture; + } + final PoolEntry poolEntry = internalEndpoint.getPoolEntry(); + final HttpRoute route = poolEntry.getRoute(); + final HttpHost host; + if (route.getProxyHost() != null) { + host = route.getProxyHost(); + } else { + host = route.getTargetHost(); + } + final InetSocketAddress localAddress = route.getLocalSocketAddress(); + final Future connectFuture = connectionOperator.connect( + connectionInitiator, host, localAddress, timeout, timeUnit, new FutureCallback() { + + @Override + public void completed(final ManagedAsyncClientConnection connection) { + poolEntry.assignConnection(connection); + resultFuture.completed(new InternalConnectionEndpoint(poolEntry)); + } + + @Override + public void failed(final Exception ex) { + resultFuture.failed(ex); + } + + @Override + public void cancelled() { + resultFuture.cancel(); + } + + }); + resultFuture.setDependency(connectFuture); + return resultFuture; + } + + @Override + public void upgrade( + final AsyncConnectionEndpoint endpoint, + final HttpContext context) { + Args.notNull(endpoint, "Managed endpoint"); + final InternalConnectionEndpoint internalEndpoint = cast(endpoint); + final PoolEntry poolEntry = internalEndpoint.getValidatedPoolEntry(); + final HttpRoute route = poolEntry.getRoute(); + connectionOperator.upgrade(poolEntry.getConnection(), route.getTargetHost()); + } + + @Override + public void setMaxTotal(final int max) { + pool.setMaxTotal(max); + } + + @Override + public int getMaxTotal() { + return pool.getMaxTotal(); + } + + @Override + public void setDefaultMaxPerRoute(final int max) { + pool.setDefaultMaxPerRoute(max); + } + + @Override + public int getDefaultMaxPerRoute() { + return pool.getDefaultMaxPerRoute(); + } + + @Override + public void setMaxPerRoute(final HttpRoute route, final int max) { + pool.setMaxPerRoute(route, max); + } + + @Override + public int getMaxPerRoute(final HttpRoute route) { + return pool.getMaxPerRoute(route); + } + + @Override + public void closeIdle(final long idletime, final TimeUnit tunit) { + pool.closeIdle(idletime, tunit); + } + + @Override + public void closeExpired() { + pool.closeExpired(); + } + + @Override + public PoolStats getTotalStats() { + return pool.getTotalStats(); + } + + @Override + public PoolStats getStats(final HttpRoute route) { + return pool.getStats(route); + } + + public int getValidateAfterInactivity() { + return validateAfterInactivity; + } + + /** + * Defines period of inactivity in milliseconds after which persistent connections must + * be re-validated prior to being {@link #lease(HttpRoute, Object, long, TimeUnit, + * FutureCallback)} leased} to the consumer. Non-positive value passed + * to this method disables connection validation. This check helps detect connections + * that have become stale (half-closed) while kept inactive in the pool. + */ + public void setValidateAfterInactivity(final int ms) { + validateAfterInactivity = ms; + } + + class InternalConnectionEndpoint extends AsyncConnectionEndpoint implements Identifiable { + + private final AtomicReference> poolEntryRef; + + InternalConnectionEndpoint(final PoolEntry poolEntry) { + this.poolEntryRef = new AtomicReference<>(poolEntry); + } + + @Override + public String getId() { + final PoolEntry poolEntry = poolEntryRef.get(); + final ManagedAsyncClientConnection connection = poolEntry != null ? poolEntry.getConnection() : null; + return ConnPoolSupport.getId(connection); + } + + PoolEntry getPoolEntry() { + final PoolEntry poolEntry = poolEntryRef.get(); + if (poolEntry == null) { + throw new ConnectionShutdownException(); + } + return poolEntry; + } + + PoolEntry getValidatedPoolEntry() { + final PoolEntry poolEntry = getPoolEntry(); + final ManagedAsyncClientConnection connection = poolEntry.getConnection(); + Asserts.check(connection != null && connection.isOpen(), "Endpoint is not connected"); + return poolEntry; + } + + PoolEntry detach() { + return poolEntryRef.getAndSet(null); + } + + @Override + public void shutdown() throws IOException { + final PoolEntry poolEntry = poolEntryRef.get(); + if (poolEntry != null) { + final ManagedAsyncClientConnection connection = poolEntry.getConnection(); + poolEntry.discardConnection(); + if (connection != null) { + connection.shutdown(); + } + } + } + + @Override + public void close() throws IOException { + final PoolEntry poolEntry = poolEntryRef.get(); + if (poolEntry != null) { + poolEntry.discardConnection(); + } + } + + @Override + public boolean isConnected() { + final PoolEntry poolEntry = poolEntryRef.get(); + if (poolEntry == null) { + throw new ConnectionShutdownException(); + } + final ManagedAsyncClientConnection connection = poolEntry.getConnection(); + return connection != null && connection.isOpen(); + } + + @Override + public void setSocketTimeout(final int timeout) { + getValidatedPoolEntry().getConnection().setSocketTimeout(timeout); + } + + @Override + public void execute(final AsyncClientExchangeHandler exchangeHandler, final HttpContext context) { + getValidatedPoolEntry().getConnection().submitCommand(new ExecutionCommand(exchangeHandler, context)); + } + + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManagerBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManagerBuilder.java new file mode 100644 index 000000000..f42527ad8 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManagerBuilder.java @@ -0,0 +1,203 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl.nio; + +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.SchemePortResolver; +import org.apache.hc.client5.http.ssl.SSLUpgradeStrategy; +import org.apache.hc.core5.http.config.RegistryBuilder; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.pool.ConnPoolListener; +import org.apache.hc.core5.pool.ConnPoolPolicy; + +/** + * Builder for {@link PoolingAsyncClientConnectionManager} instances. + *

+ * When a particular component is not explicitly set this class will + * use its default implementation. System properties will be taken + * into account when configuring the default implementations when + * {@link #useSystemProperties()} method is called prior to calling + * {@link #build()}. + *

+ *
    + *
  • ssl.TrustManagerFactory.algorithm
  • + *
  • javax.net.ssl.trustStoreType
  • + *
  • javax.net.ssl.trustStore
  • + *
  • javax.net.ssl.trustStoreProvider
  • + *
  • javax.net.ssl.trustStorePassword
  • + *
  • ssl.KeyManagerFactory.algorithm
  • + *
  • javax.net.ssl.keyStoreType
  • + *
  • javax.net.ssl.keyStore
  • + *
  • javax.net.ssl.keyStoreProvider
  • + *
  • javax.net.ssl.keyStorePassword
  • + *
  • https.protocols
  • + *
  • https.cipherSuites
  • + *
+ * + * @since 5.0 + */ +public class PoolingAsyncClientConnectionManagerBuilder { + + private TlsStrategy tlsStrategy; + private SchemePortResolver schemePortResolver; + private DnsResolver dnsResolver; + private ConnPoolPolicy connPoolPolicy; + private ConnPoolListener connPoolListener; + + private boolean systemProperties; + + private int maxConnTotal = 0; + private int maxConnPerRoute = 0; + + private long connTimeToLive = -1; + private TimeUnit connTimeToLiveTimeUnit = TimeUnit.MILLISECONDS; + private int validateAfterInactivity = 2000; + + public static PoolingAsyncClientConnectionManagerBuilder create() { + return new PoolingAsyncClientConnectionManagerBuilder(); + } + + PoolingAsyncClientConnectionManagerBuilder() { + super(); + } + + /** + * Assigns {@link TlsStrategy} instance for TLS connections. + */ + public final PoolingAsyncClientConnectionManagerBuilder setTlsStrategy( + final TlsStrategy tlsStrategy) { + this.tlsStrategy = tlsStrategy; + return this; + } + + /** + * Assigns {@link DnsResolver} instance. + */ + public final PoolingAsyncClientConnectionManagerBuilder setDnsResolver(final DnsResolver dnsResolver) { + this.dnsResolver = dnsResolver; + return this; + } + + /** + * Assigns {@link SchemePortResolver} instance. + */ + public final PoolingAsyncClientConnectionManagerBuilder setSchemePortResolver(final SchemePortResolver schemePortResolver) { + this.schemePortResolver = schemePortResolver; + return this; + } + + /** + * Assigns {@link ConnPoolPolicy} value. + */ + public final PoolingAsyncClientConnectionManagerBuilder setConnPoolPolicy(final ConnPoolPolicy connPoolPolicy) { + this.connPoolPolicy = connPoolPolicy; + return this; + } + + /** + * Assigns {@link ConnPoolListener} instance. + */ + public final PoolingAsyncClientConnectionManagerBuilder setConnPoolListener(final ConnPoolListener connPoolListener) { + this.connPoolListener = connPoolListener; + return this; + } + + /** + * Assigns maximum total connection value. + */ + public final PoolingAsyncClientConnectionManagerBuilder setMaxConnTotal(final int maxConnTotal) { + this.maxConnTotal = maxConnTotal; + return this; + } + + /** + * Assigns maximum connection per route value. + */ + public final PoolingAsyncClientConnectionManagerBuilder setMaxConnPerRoute(final int maxConnPerRoute) { + this.maxConnPerRoute = maxConnPerRoute; + return this; + } + + /** + * Sets maximum time to live for persistent connections + */ + public final PoolingAsyncClientConnectionManagerBuilder setConnectionTimeToLive(final long connTimeToLive, final TimeUnit connTimeToLiveTimeUnit) { + this.connTimeToLive = connTimeToLive; + this.connTimeToLiveTimeUnit = connTimeToLiveTimeUnit; + return this; + } + + /** + * Sets period after inactivity in milliseconds after which persistent + * connections must be checked to ensure they are still valid. + * + * @see org.apache.hc.core5.http.io.HttpClientConnection#isStale() + */ + public final PoolingAsyncClientConnectionManagerBuilder setValidateAfterInactivity(final int validateAfterInactivity) { + this.validateAfterInactivity = validateAfterInactivity; + return this; + } + + /** + * Use system properties when creating and configuring default + * implementations. + */ + public final PoolingAsyncClientConnectionManagerBuilder useSystemProperties() { + this.systemProperties = true; + return this; + } + + public PoolingAsyncClientConnectionManager build() { + @SuppressWarnings("resource") + final PoolingAsyncClientConnectionManager poolingmgr = new PoolingAsyncClientConnectionManager( + RegistryBuilder.create() + .register("https", tlsStrategy != null ? tlsStrategy : + (systemProperties ? + SSLUpgradeStrategy.getSystemDefault() : + SSLUpgradeStrategy.getDefault())) + .build(), + schemePortResolver, + dnsResolver, + connTimeToLive, + connTimeToLiveTimeUnit != null ? connTimeToLiveTimeUnit : TimeUnit.MILLISECONDS, + connPoolPolicy, + connPoolListener); + poolingmgr.setValidateAfterInactivity(this.validateAfterInactivity); + if (maxConnTotal > 0) { + poolingmgr.setMaxTotal(maxConnTotal); + } + if (maxConnPerRoute > 0) { + poolingmgr.setDefaultMaxPerRoute(maxConnPerRoute); + } + return poolingmgr; + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/nio/AsyncClientConnectionManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/nio/AsyncClientConnectionManager.java new file mode 100644 index 000000000..d5b5f1f5b --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/nio/AsyncClientConnectionManager.java @@ -0,0 +1,127 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.nio; + +import java.io.Closeable; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.reactor.ConnectionInitiator; + +/** + * Represents a manager of persistent non-blocking client connections. + *

+ * The purpose of an HTTP connection manager is to serve as a factory for new + * HTTP connections, manage persistent connections and synchronize access to + * persistent connections making sure that only one thread of execution can + * have access to a connection at a time. + *

+ * Implementations of this interface must be thread-safe. Access to shared + * data must be synchronized as methods of this interface may be executed + * from multiple threads. + * + * @since 5.0 + */ +public interface AsyncClientConnectionManager extends Closeable { + + /** + * Returns a {@link Future} object which can be used to obtain + * an {@link AsyncConnectionEndpoint} or to cancel the request by calling + * {@link Future#cancel(boolean)}. + *

+ * Please note that newly allocated endpoints can be leased + * {@link AsyncConnectionEndpoint#isConnected() disconnected}. The consumer + * of the endpoint is responsible for fully establishing the route to + * the endpoint target by calling {@link #connect(AsyncConnectionEndpoint, + * ConnectionInitiator, long, TimeUnit, HttpContext, FutureCallback)} + * in order to connect directly to the target or to the first proxy hop, + * and optionally calling {@link #upgrade(AsyncConnectionEndpoint, HttpContext)} + * method to upgrade the underlying transport to Transport Layer Security + * after having executed a {@code CONNECT} method to all intermediate + * proxy hops. + * + * @param route HTTP route of the requested connection. + * @param state expected state of the connection or {@code null} + * if the connection is not expected to carry any state. + * @param timeout lease request timeout. + * @param timeUnit time unit. + * @param callback result callback. + */ + Future lease( + HttpRoute route, + Object state, + long timeout, + TimeUnit timeUnit, + FutureCallback callback); + + /** + * Releases the endpoint back to the manager making it potentially + * re-usable by other consumers. Optionally, the maximum period + * of how long the manager should keep the connection alive can be + * defined using {@code validDuration} and {@code timeUnit} + * parameters. + * + * @param endpoint the managed endpoint. + * @param newState the new connection state of {@code null} if state-less. + * @param validDuration the duration of time this connection is valid for reuse. + * @param timeUnit the time unit. + */ + void release(AsyncConnectionEndpoint endpoint, Object newState, long validDuration, TimeUnit timeUnit); + + /** + * Connects the endpoint to the initial hop (connection target in case + * of a direct route or to the first proxy hop in case of a route via a proxy + * or multiple proxies). + * + * @param endpoint the managed endpoint. + * @param connectTimeout connect timeout. + * @param timeUnit time unit. + * @param context the actual HTTP context. + * @param callback result callback. + */ + Future connect( + AsyncConnectionEndpoint endpoint, + ConnectionInitiator connectionInitiator, + long connectTimeout, + TimeUnit timeUnit, + HttpContext context, + FutureCallback callback); + + /** + * Upgrades the endpoint's underlying transport to Transport Layer Security. + * + * @param endpoint the managed endpoint. + * @param context the actual HTTP context. + */ + void upgrade( + AsyncConnectionEndpoint endpoint, + HttpContext context); + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/nio/AsyncConnectionEndpoint.java b/httpclient5/src/main/java/org/apache/hc/client5/http/nio/AsyncConnectionEndpoint.java new file mode 100644 index 000000000..47767aaba --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/nio/AsyncConnectionEndpoint.java @@ -0,0 +1,97 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.nio; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.Future; + +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.concurrent.BasicFuture; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.AsyncResponseConsumer; +import org.apache.hc.core5.http.nio.support.BasicClientExchangeHandler; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.protocol.HttpCoreContext; + +/** + * Client connection endpoint that can be used to execute message exchanges. + * + * @since 5.0 + */ +@Contract(threading = ThreadingBehavior.SAFE) +public abstract class AsyncConnectionEndpoint implements Closeable { + + public abstract void execute(AsyncClientExchangeHandler exchangeHandler, HttpContext context); + + public Future execute( + final AsyncRequestProducer requestProducer, + final AsyncResponseConsumer responseConsumer, + final HttpContext context, + final FutureCallback callback) { + final BasicFuture future = new BasicFuture<>(callback); + execute(new BasicClientExchangeHandler<>(requestProducer, responseConsumer, + new FutureCallback() { + + @Override + public void completed(final T result) { + future.completed(result); + } + + @Override + public void failed(final Exception ex) { + future.failed(ex); + } + + @Override + public void cancelled() { + future.cancel(); + } + + }), + context != null ? context : HttpCoreContext.create()); + return future; + } + + public Future execute( + final AsyncRequestProducer requestProducer, + final AsyncResponseConsumer responseConsumer, + final FutureCallback callback) { + return execute(requestProducer, responseConsumer, null, callback); + } + + public abstract boolean isConnected(); + + public abstract void setSocketTimeout(int timeout); + + public abstract void shutdown() throws IOException; + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/H2TlsSupport.java b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/H2TlsSupport.java new file mode 100644 index 000000000..aaf73b4d3 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/H2TlsSupport.java @@ -0,0 +1,377 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.ssl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer; + +// TODO: use class from HttpCore 5.0a3 +public final class H2TlsSupport { + + private final static String[] PROTOCOL_BLACKLIST = { + "TLSv1", + "TLSv1.1" + }; + + private final static String[] CIPHER_BLACKLIST = { + "TLS_NULL_WITH_NULL_NULL", + "TLS_RSA_WITH_NULL_MD5", + "TLS_RSA_WITH_NULL_SHA", + "TLS_RSA_EXPORT_WITH_RC4_40_MD5", + "TLS_RSA_WITH_RC4_128_MD5", + "TLS_RSA_WITH_RC4_128_SHA", + "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5", + "TLS_RSA_WITH_IDEA_CBC_SHA", + "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_RSA_WITH_DES_CBC_SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_DSS_WITH_DES_CBC_SHA", + "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_RSA_WITH_DES_CBC_SHA", + "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DHE_DSS_WITH_DES_CBC_SHA", + "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DHE_RSA_WITH_DES_CBC_SHA", + "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5", + "TLS_DH_anon_WITH_RC4_128_MD5", + "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_anon_WITH_DES_CBC_SHA", + "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA", + "TLS_KRB5_WITH_DES_CBC_SHA", + "TLS_KRB5_WITH_3DES_EDE_CBC_SHA", + "TLS_KRB5_WITH_RC4_128_SHA", + "TLS_KRB5_WITH_IDEA_CBC_SHA", + "TLS_KRB5_WITH_DES_CBC_MD5", + "TLS_KRB5_WITH_3DES_EDE_CBC_MD5", + "TLS_KRB5_WITH_RC4_128_MD5", + "TLS_KRB5_WITH_IDEA_CBC_MD5", + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA", + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA", + "TLS_KRB5_EXPORT_WITH_RC4_40_SHA", + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5", + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5", + "TLS_KRB5_EXPORT_WITH_RC4_40_MD5", + "TLS_PSK_WITH_NULL_SHA", + "TLS_DHE_PSK_WITH_NULL_SHA", + "TLS_RSA_PSK_WITH_NULL_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_DH_anon_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_DH_anon_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_NULL_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DH_anon_WITH_AES_128_CBC_SHA256", + "TLS_DH_anon_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA", + "TLS_PSK_WITH_RC4_128_SHA", + "TLS_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_PSK_WITH_AES_128_CBC_SHA", + "TLS_PSK_WITH_AES_256_CBC_SHA", + "TLS_DHE_PSK_WITH_RC4_128_SHA", + "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_RSA_PSK_WITH_RC4_128_SHA", + "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA", + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_SEED_CBC_SHA", + "TLS_DH_DSS_WITH_SEED_CBC_SHA", + "TLS_DH_RSA_WITH_SEED_CBC_SHA", + "TLS_DHE_DSS_WITH_SEED_CBC_SHA", + "TLS_DHE_RSA_WITH_SEED_CBC_SHA", + "TLS_DH_anon_WITH_SEED_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DH_DSS_WITH_AES_128_GCM_SHA256", + "TLS_DH_DSS_WITH_AES_256_GCM_SHA384", + "TLS_DH_anon_WITH_AES_128_GCM_SHA256", + "TLS_DH_anon_WITH_AES_256_GCM_SHA384", + "TLS_PSK_WITH_AES_128_GCM_SHA256", + "TLS_PSK_WITH_AES_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384", + "TLS_PSK_WITH_AES_128_CBC_SHA256", + "TLS_PSK_WITH_AES_256_CBC_SHA384", + "TLS_PSK_WITH_NULL_SHA256", + "TLS_PSK_WITH_NULL_SHA384", + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_NULL_SHA256", + "TLS_DHE_PSK_WITH_NULL_SHA384", + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_NULL_SHA256", + "TLS_RSA_PSK_WITH_NULL_SHA384", + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_EMPTY_RENEGOTIATION_INFO_SCSV", + "TLS_ECDH_ECDSA_WITH_NULL_SHA", + "TLS_ECDH_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_NULL_SHA", + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_RSA_WITH_NULL_SHA", + "TLS_ECDH_RSA_WITH_RC4_128_SHA", + "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_NULL_SHA", + "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_anon_WITH_NULL_SHA", + "TLS_ECDH_anon_WITH_RC4_128_SHA", + "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_128_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_PSK_WITH_RC4_128_SHA", + "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_PSK_WITH_NULL_SHA", + "TLS_ECDHE_PSK_WITH_NULL_SHA256", + "TLS_ECDHE_PSK_WITH_NULL_SHA384", + "TLS_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_PSK_WITH_ARIA_128_GCM_SHA256", + "TLS_PSK_WITH_ARIA_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_WITH_AES_128_CCM", + "TLS_RSA_WITH_AES_256_CCM", + "TLS_RSA_WITH_AES_128_CCM_8", + "TLS_RSA_WITH_AES_256_CCM_8", + "TLS_PSK_WITH_AES_128_CCM", + "TLS_PSK_WITH_AES_256_CCM", + "TLS_PSK_WITH_AES_128_CCM_8", + "TLS_PSK_WITH_AES_256_CCM_8" }; + + private final static Set BLACKLISED_PROTOCOLS = new CopyOnWriteArraySet<>(Arrays.asList(PROTOCOL_BLACKLIST)); + private final static Set BLACKLISED_CIPHERS = new CopyOnWriteArraySet<>(Arrays.asList(CIPHER_BLACKLIST)); + + public static String[] excludeBlacklistedProtocols(final String[] protocols) { + if (protocols == null) { + return null; + } + List enabledProtocols = null; + for (String protocol: protocols) { + if (!protocol.startsWith("SSL") && !BLACKLISED_PROTOCOLS.contains(protocol)) { + if (enabledProtocols == null) { + enabledProtocols = new ArrayList<>(); + } + enabledProtocols.add(protocol); + } + } + return enabledProtocols != null ? enabledProtocols.toArray(new String[enabledProtocols.size()]) : protocols; + } + + public static String[] excludeBlacklistedCiphers(final String[] ciphers) { + if (ciphers == null) { + return null; + } + List enabledCiphers = null; + for (String cipher: ciphers) { + if (!BLACKLISED_CIPHERS.contains(cipher)) { + if (enabledCiphers == null) { + enabledCiphers = new ArrayList<>(); + } + enabledCiphers.add(cipher); + } + } + return enabledCiphers != null ? enabledCiphers.toArray(new String[enabledCiphers.size()]) : ciphers; + } + + public static SSLSessionInitializer decorateInitializer(final SSLSessionInitializer initializer) { + return new SSLSessionInitializer() { + + @Override + public void initialize(final SSLEngine sslengine) throws SSLException { + sslengine.setEnabledProtocols(H2TlsSupport.excludeBlacklistedProtocols(sslengine.getEnabledProtocols())); + sslengine.setEnabledCipherSuites(H2TlsSupport.excludeBlacklistedCiphers(sslengine.getEnabledCipherSuites())); + if (initializer != null) { + initializer.initialize(sslengine); + } + } + + }; + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLUpgradeStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLUpgradeStrategy.java new file mode 100644 index 000000000..00a83ec56 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLUpgradeStrategy.java @@ -0,0 +1,231 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.ssl; + +import java.net.SocketAddress; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.security.auth.x500.X500Principal; + +import org.apache.hc.client5.http.psl.PublicSuffixMatcherLoader; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.reactor.IOSession; +import org.apache.hc.core5.reactor.ssl.SSLBufferManagement; +import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer; +import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier; +import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.TextUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Default SSL upgrade strategy for non-blocking connections. + * + * @since 5.0 + */ +public class SSLUpgradeStrategy implements TlsStrategy { + + private static String[] split(final String s) { + if (TextUtils.isBlank(s)) { + return null; + } + return s.split(" *, *"); + } + + public static HostnameVerifier getDefaultHostnameVerifier() { + return new DefaultHostnameVerifier(PublicSuffixMatcherLoader.getDefault()); + } + + public static TlsStrategy getDefault() { + return new SSLUpgradeStrategy( + SSLContexts.createDefault(), + getDefaultHostnameVerifier()); + } + + public static TlsStrategy getSystemDefault() { + return new SSLUpgradeStrategy( + SSLContexts.createSystemDefault(), + split(System.getProperty("https.protocols")), + split(System.getProperty("https.cipherSuites")), + SSLBufferManagement.STATIC, + getDefaultHostnameVerifier()); + } + + private final Logger log = LogManager.getLogger(getClass()); + + private final SSLContext sslContext; + private final String[] supportedProtocols; + private final String[] supportedCipherSuites; + private final SSLBufferManagement sslBufferManagement; + private final HostnameVerifier hostnameVerifier; + + public SSLUpgradeStrategy( + final SSLContext sslContext, + final String[] supportedProtocols, + final String[] supportedCipherSuites, + final SSLBufferManagement sslBufferManagement, + final HostnameVerifier hostnameVerifier) { + super(); + this.sslContext = Args.notNull(sslContext, "SSL context"); + this.supportedProtocols = supportedProtocols; + this.supportedCipherSuites = supportedCipherSuites; + this.sslBufferManagement = sslBufferManagement != null ? sslBufferManagement : SSLBufferManagement.STATIC; + this.hostnameVerifier = hostnameVerifier != null ? hostnameVerifier : getDefaultHostnameVerifier(); + } + + public SSLUpgradeStrategy( + final SSLContext sslcontext, + final HostnameVerifier hostnameVerifier) { + this(sslcontext, null, null, SSLBufferManagement.STATIC, hostnameVerifier); + } + + public SSLUpgradeStrategy(final SSLContext sslcontext) { + this(sslcontext, getDefaultHostnameVerifier()); + } + + @Override + public void upgrade( + final TransportSecurityLayer tlsSession, + final String hostname, + final SocketAddress localAddress, + final SocketAddress remoteAddress, + final String... parameters) { + tlsSession.start(sslContext, sslBufferManagement, new SSLSessionInitializer() { + + @Override + public void initialize(final SSLEngine sslengine) throws SSLException { + if (supportedProtocols != null) { + sslengine.setEnabledProtocols(supportedProtocols); + } else { + // If supported protocols are not explicitly set, remove all SSL protocol versions + final String[] allProtocols = sslengine.getEnabledProtocols(); + final List enabledProtocols = new ArrayList<>(allProtocols.length); + for (final String protocol: allProtocols) { + if (!protocol.startsWith("SSL")) { + enabledProtocols.add(protocol); + } + } + if (!enabledProtocols.isEmpty()) { + sslengine.setEnabledProtocols(enabledProtocols.toArray(new String[enabledProtocols.size()])); + } + } + if (supportedCipherSuites != null) { + sslengine.setEnabledCipherSuites(supportedCipherSuites); + } + initializeEngine(sslengine); + + if (log.isDebugEnabled()) { + log.debug("Enabled protocols: " + Arrays.asList(sslengine.getEnabledProtocols())); + log.debug("Enabled cipher suites:" + Arrays.asList(sslengine.getEnabledCipherSuites())); + } + } + + }, new SSLSessionVerifier() { + + @Override + public void verify(final IOSession iosession, final SSLSession sslsession) throws SSLException { + // TODO: fix after upgrading to HttpCore 5.0a3 + verifySession(hostname, sslsession); + } + + }); + } + + protected void initializeEngine(final SSLEngine engine) { + } + + protected void verifySession( + final String hostname, + final SSLSession sslsession) throws SSLException { + + if (log.isDebugEnabled()) { + log.debug("Secure session established"); + log.debug(" negotiated protocol: " + sslsession.getProtocol()); + log.debug(" negotiated cipher suite: " + sslsession.getCipherSuite()); + + try { + + final Certificate[] certs = sslsession.getPeerCertificates(); + final X509Certificate x509 = (X509Certificate) certs[0]; + final X500Principal peer = x509.getSubjectX500Principal(); + + log.debug(" peer principal: " + peer.toString()); + final Collection> altNames1 = x509.getSubjectAlternativeNames(); + if (altNames1 != null) { + final List altNames = new ArrayList<>(); + for (final List aC : altNames1) { + if (!aC.isEmpty()) { + altNames.add((String) aC.get(1)); + } + } + log.debug(" peer alternative names: " + altNames); + } + + final X500Principal issuer = x509.getIssuerX500Principal(); + log.debug(" issuer principal: " + issuer.toString()); + final Collection> altNames2 = x509.getIssuerAlternativeNames(); + if (altNames2 != null) { + final List altNames = new ArrayList<>(); + for (final List aC : altNames2) { + if (!aC.isEmpty()) { + altNames.add((String) aC.get(1)); + } + } + log.debug(" issuer alternative names: " + altNames); + } + } catch (final Exception ignore) { + } + } + + if (this.hostnameVerifier instanceof HttpClientHostnameVerifier) { + final Certificate[] certs = sslsession.getPeerCertificates(); + final X509Certificate x509 = (X509Certificate) certs[0]; + ((HttpClientHostnameVerifier) this.hostnameVerifier).verify(hostname, x509); + } else if (!this.hostnameVerifier.verify(hostname, sslsession)) { + final Certificate[] certs = sslsession.getPeerCertificates(); + final X509Certificate x509 = (X509Certificate) certs[0]; + final List subjectAlts = DefaultHostnameVerifier.getSubjectAltNames(x509); + throw new SSLPeerUnverifiedException("Certificate for <" + hostname + "> doesn't match any " + + "of the subject alternative names: " + subjectAlts); + } + } + +} diff --git a/pom.xml b/pom.xml index e2c81ead9..feab23f99 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,11 @@ httpcore5 ${httpcore.version} + + org.apache.httpcomponents.core5 + httpcore5-h2 + ${httpcore.version} + org.apache.logging.log4j log4j-api