You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2022/04/01 15:12:17 UTC

[httpcomponents-core] 01/01: HTTPCORE-710: In case of some TLS handshake failures (protocol version mismatch) the local TLS engine quietly closes the stream instead of throwing a handshake exception

This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch HTTPCORE-710
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 1736fe414f5f27a4199d4752b4a49e847e7079fc
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Fri Apr 1 17:08:01 2022 +0200

    HTTPCORE-710: In case of some TLS handshake failures (protocol version mismatch) the local TLS engine quietly closes the stream instead of throwing a handshake exception
---
 .../hc/core5/testing/nio/H2TLSIntegrationTest.java | 392 ---------------------
 .../testing/nio/NoopIOEventHandlerFactory.java     |  68 ++++
 .../hc/core5/testing/nio/TLSIntegrationTest.java   | 330 +++++++++++++++++
 .../testing/nio/TestDefaultListeningIOReactor.java |  39 --
 .../apache/hc/core5/reactor/ssl/SSLIOSession.java  |  13 +-
 5 files changed, 407 insertions(+), 435 deletions(-)

diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2TLSIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2TLSIntegrationTest.java
deleted file mode 100644
index 2eb38e8..0000000
--- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2TLSIntegrationTest.java
+++ /dev/null
@@ -1,392 +0,0 @@
-/*
- * ====================================================================
- * 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
- * <http://www.apache.org/>.
- *
- */
-
-package org.apache.hc.core5.testing.nio;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicReference;
-
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLSession;
-
-import org.apache.hc.core5.http.ContentType;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpResponse;
-import org.apache.hc.core5.http.HttpStatus;
-import org.apache.hc.core5.http.Message;
-import org.apache.hc.core5.http.Method;
-import org.apache.hc.core5.http.ProtocolVersion;
-import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.http.impl.bootstrap.AsyncServerBootstrap;
-import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester;
-import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer;
-import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
-import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
-import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy;
-import org.apache.hc.core5.http.nio.ssl.BasicServerTlsStrategy;
-import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
-import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
-import org.apache.hc.core5.http.protocol.UriPatternMatcher;
-import org.apache.hc.core5.http.ssl.TLS;
-import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap;
-import org.apache.hc.core5.io.CloseMode;
-import org.apache.hc.core5.reactor.IOReactorConfig;
-import org.apache.hc.core5.reactor.ListenerEndpoint;
-import org.apache.hc.core5.ssl.SSLContexts;
-import org.apache.hc.core5.testing.SSLTestContexts;
-import org.apache.hc.core5.testing.classic.LoggingConnPoolListener;
-import org.apache.hc.core5.util.Timeout;
-import org.hamcrest.CoreMatchers;
-import org.junit.Rule;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.api.extension.Extensions;
-import org.junit.jupiter.migrationsupport.rules.ExternalResourceSupport;
-import org.junit.rules.ExternalResource;
-
-@Extensions({@ExtendWith({ExternalResourceSupport.class})})
-public class H2TLSIntegrationTest {
-
-    private static final Timeout TIMEOUT = Timeout.ofSeconds(30);
-
-    private HttpAsyncServer server;
-
-    @Rule
-    public ExternalResource serverResource = new ExternalResource() {
-
-        @Override
-        protected void after() {
-            if (server != null) {
-                try {
-                    server.close(CloseMode.IMMEDIATE);
-                } catch (final Exception ignore) {
-                }
-            }
-        }
-
-    };
-
-    private HttpAsyncRequester requester;
-
-    @Rule
-    public ExternalResource clientResource = new ExternalResource() {
-
-        @Override
-        protected void after() {
-            if (requester != null) {
-                try {
-                    requester.close(CloseMode.GRACEFUL);
-                } catch (final Exception ignore) {
-                }
-            }
-        }
-
-    };
-
-    @Test
-    public void testTLSSuccess() throws Exception {
-        server = AsyncServerBootstrap.bootstrap()
-                .setLookupRegistry(new UriPatternMatcher<>())
-                .setIOReactorConfig(
-                        IOReactorConfig.custom()
-                                .setSoTimeout(TIMEOUT)
-                                .build())
-                .setTlsStrategy(new BasicServerTlsStrategy(SSLTestContexts.createServerSSLContext()))
-                .setStreamListener(LoggingHttp1StreamListener.INSTANCE_SERVER)
-                .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
-                .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
-                .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
-                .register("*", () -> new EchoHandler(2048))
-                .create();
-        server.start();
-
-        final AtomicReference<SSLSession> sslSessionRef = new AtomicReference<>();
-
-        requester = H2RequesterBootstrap.bootstrap()
-                .setIOReactorConfig(IOReactorConfig.custom()
-                        .setSoTimeout(TIMEOUT)
-                        .build())
-                .setTlsStrategy(new BasicClientTlsStrategy(
-                        SSLTestContexts.createClientSSLContext(),
-                        (endpoint, sslEngine) -> {
-                            sslSessionRef.set(sslEngine.getSession());
-                            return null;
-                        }))
-                .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
-                .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
-                .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
-                .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
-                .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
-                .create();
-
-        server.start();
-        final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTPS);
-        final ListenerEndpoint listener = future.get();
-        final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
-        requester.start();
-
-        final HttpHost target = new HttpHost(URIScheme.HTTPS.id, "localhost", address.getPort());
-        final Future<Message<HttpResponse, String>> resultFuture1 = requester.execute(
-                new BasicRequestProducer(Method.POST, target, "/stuff",
-                        new StringAsyncEntityProducer("some stuff", ContentType.TEXT_PLAIN)),
-                new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), TIMEOUT, null);
-        final Message<HttpResponse, String> message1 = resultFuture1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
-        assertThat(message1, CoreMatchers.notNullValue());
-        final HttpResponse response1 = message1.getHead();
-        assertThat(response1.getCode(), CoreMatchers.equalTo(HttpStatus.SC_OK));
-        final String body1 = message1.getBody();
-        assertThat(body1, CoreMatchers.equalTo("some stuff"));
-
-        final SSLSession sslSession = sslSessionRef.getAndSet(null);
-        final ProtocolVersion tlsVersion = TLS.parse(sslSession.getProtocol());
-        assertThat(tlsVersion.greaterEquals(TLS.V_1_2.version), CoreMatchers.equalTo(true));
-        assertThat(sslSession.getPeerPrincipal().getName(),
-                CoreMatchers.equalTo("CN=localhost,OU=Apache HttpComponents,O=Apache Software Foundation"));
-    }
-
-    @Test
-    public void testTLSTrustFailure() throws Exception {
-        server = AsyncServerBootstrap.bootstrap()
-                .setLookupRegistry(new UriPatternMatcher<>())
-                .setIOReactorConfig(
-                        IOReactorConfig.custom()
-                                .setSoTimeout(TIMEOUT)
-                                .build())
-                .setTlsStrategy(new BasicServerTlsStrategy(SSLTestContexts.createServerSSLContext()))
-                .setStreamListener(LoggingHttp1StreamListener.INSTANCE_SERVER)
-                .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
-                .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
-                .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
-                .register("*", () -> new EchoHandler(2048))
-                .create();
-        server.start();
-
-        requester = H2RequesterBootstrap.bootstrap()
-                .setIOReactorConfig(IOReactorConfig.custom()
-                        .setSoTimeout(TIMEOUT)
-                        .build())
-                .setTlsStrategy(new BasicClientTlsStrategy(SSLContexts.createDefault()))
-                .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
-                .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
-                .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
-                .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
-                .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
-                .create();
-
-        server.start();
-        final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTPS);
-        final ListenerEndpoint listener = future.get();
-        final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
-        requester.start();
-
-        final HttpHost target = new HttpHost(URIScheme.HTTPS.id, "localhost", address.getPort());
-        final Future<Message<HttpResponse, String>> resultFuture1 = requester.execute(
-                new BasicRequestProducer(Method.POST, target, "/stuff",
-                        new StringAsyncEntityProducer("some stuff", ContentType.TEXT_PLAIN)),
-                new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), TIMEOUT, null);
-        final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
-                resultFuture1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
-        final Throwable cause = exception.getCause();
-        assertThat(cause, CoreMatchers.instanceOf(SSLHandshakeException.class));
-    }
-
-    @Test
-    public void testTLSClientAuthFailure() throws Exception {
-        server = AsyncServerBootstrap.bootstrap()
-                .setLookupRegistry(new UriPatternMatcher<>())
-                .setIOReactorConfig(
-                        IOReactorConfig.custom()
-                                .setSoTimeout(TIMEOUT)
-                                .build())
-                .setTlsStrategy(new BasicServerTlsStrategy(
-                        SSLTestContexts.createServerSSLContext(),
-                        (endpoint, sslEngine) -> sslEngine.setNeedClientAuth(true),
-                        null))
-                .setStreamListener(LoggingHttp1StreamListener.INSTANCE_SERVER)
-                .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
-                .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
-                .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
-                .register("*", () -> new EchoHandler(2048))
-                .create();
-        server.start();
-
-        requester = H2RequesterBootstrap.bootstrap()
-                .setIOReactorConfig(IOReactorConfig.custom()
-                        .setSoTimeout(TIMEOUT)
-                        .build())
-                .setTlsStrategy(new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
-                .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
-                .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
-                .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
-                .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
-                .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
-                .create();
-
-        server.start();
-        final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTPS);
-        final ListenerEndpoint listener = future.get();
-        final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
-        requester.start();
-
-        final HttpHost target = new HttpHost(URIScheme.HTTPS.id, "localhost", address.getPort());
-        final Future<Message<HttpResponse, String>> resultFuture1 = requester.execute(
-                new BasicRequestProducer(Method.POST, target, "/stuff",
-                        new StringAsyncEntityProducer("some stuff", ContentType.TEXT_PLAIN)),
-                new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), TIMEOUT, null);
-        final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
-                resultFuture1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
-        final Throwable cause = exception.getCause();
-        assertThat(cause, CoreMatchers.instanceOf(IOException.class));
-    }
-
-    @Test
-    public void testSSLDisabledByDefault() throws Exception {
-        server = AsyncServerBootstrap.bootstrap()
-                .setLookupRegistry(new UriPatternMatcher<>())
-                .setIOReactorConfig(
-                        IOReactorConfig.custom()
-                                .setSoTimeout(TIMEOUT)
-                                .build())
-                .setTlsStrategy(new BasicServerTlsStrategy(
-                        SSLTestContexts.createServerSSLContext(),
-                        (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[]{"SSLv3"}),
-                        null))
-                .setStreamListener(LoggingHttp1StreamListener.INSTANCE_SERVER)
-                .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
-                .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
-                .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
-                .register("*", () -> new EchoHandler(2048))
-                .create();
-        server.start();
-
-        requester = H2RequesterBootstrap.bootstrap()
-                .setIOReactorConfig(IOReactorConfig.custom()
-                        .setSoTimeout(TIMEOUT)
-                        .build())
-                .setTlsStrategy(new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
-                .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
-                .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
-                .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
-                .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
-                .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
-                .create();
-
-        server.start();
-        final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTPS);
-        final ListenerEndpoint listener = future.get();
-        final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
-        requester.start();
-
-        final HttpHost target = new HttpHost(URIScheme.HTTPS.id, "localhost", address.getPort());
-        final Future<Message<HttpResponse, String>> resultFuture1 = requester.execute(
-                new BasicRequestProducer(Method.POST, target, "/stuff",
-                        new StringAsyncEntityProducer("some stuff", ContentType.TEXT_PLAIN)),
-                new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), TIMEOUT, null);
-        final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
-                resultFuture1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
-        final Throwable cause = exception.getCause();
-        assertThat(cause, CoreMatchers.instanceOf(IOException.class));
-    }
-
-    @Test
-    public void testWeakCiphersDisabledByDefault() throws Exception {
-        requester = H2RequesterBootstrap.bootstrap()
-                .setIOReactorConfig(IOReactorConfig.custom()
-                        .setSoTimeout(TIMEOUT)
-                        .build())
-                .setTlsStrategy(new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext()))
-                .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
-                .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
-                .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
-                .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
-                .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
-                .create();
-        requester.start();
-
-        final String[] weakCiphersSuites = {
-                "SSL_RSA_WITH_RC4_128_SHA",
-                "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
-                "TLS_DH_anon_WITH_AES_128_CBC_SHA",
-                "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA",
-                "SSL_RSA_WITH_NULL_SHA",
-                "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
-                "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
-                "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA",
-                "TLS_DH_anon_WITH_AES_256_GCM_SHA384",
-                "TLS_ECDH_anon_WITH_AES_256_CBC_SHA",
-                "TLS_RSA_WITH_NULL_SHA256",
-                "SSL_RSA_EXPORT_WITH_RC4_40_MD5",
-                "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5",
-                "TLS_KRB5_EXPORT_WITH_RC4_40_SHA",
-                "SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5"
-        };
-
-        for (final String cipherSuite : weakCiphersSuites) {
-            server = AsyncServerBootstrap.bootstrap()
-                    .setLookupRegistry(new UriPatternMatcher<>())
-                    .setIOReactorConfig(
-                            IOReactorConfig.custom()
-                                    .setSoTimeout(TIMEOUT)
-                                    .build())
-                    .setTlsStrategy(new BasicServerTlsStrategy(
-                            SSLTestContexts.createServerSSLContext(),
-                            (endpoint, sslEngine) -> sslEngine.setEnabledCipherSuites(new String[]{cipherSuite}),
-                            null))
-                    .setStreamListener(LoggingHttp1StreamListener.INSTANCE_SERVER)
-                    .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
-                    .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
-                    .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
-                    .register("*", () -> new EchoHandler(2048))
-                    .create();
-            try {
-                server.start();
-                final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTPS);
-                final ListenerEndpoint listener = future.get();
-                final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
-
-                final HttpHost target = new HttpHost(URIScheme.HTTPS.id, "localhost", address.getPort());
-                final Future<Message<HttpResponse, String>> resultFuture1 = requester.execute(
-                        new BasicRequestProducer(Method.POST, target, "/stuff",
-                                new StringAsyncEntityProducer("some stuff", ContentType.TEXT_PLAIN)),
-                        new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), TIMEOUT, null);
-                final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
-                        resultFuture1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
-                final Throwable cause = exception.getCause();
-                assertThat(cause, CoreMatchers.instanceOf(IOException.class));
-            } finally {
-                server.close(CloseMode.IMMEDIATE);
-            }
-        }
-    }
-
-}
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/NoopIOEventHandlerFactory.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/NoopIOEventHandlerFactory.java
new file mode 100644
index 0000000..1caefc5
--- /dev/null
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/NoopIOEventHandlerFactory.java
@@ -0,0 +1,68 @@
+/*
+ * ====================================================================
+ * 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
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.core5.testing.nio;
+
+import java.nio.ByteBuffer;
+
+import org.apache.hc.core5.reactor.IOEventHandler;
+import org.apache.hc.core5.reactor.IOEventHandlerFactory;
+import org.apache.hc.core5.reactor.IOSession;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.apache.hc.core5.util.Timeout;
+
+class NoopIOEventHandlerFactory implements IOEventHandlerFactory {
+
+    @Override
+    public IOEventHandler createHandler(final ProtocolIOSession ioSession, final Object attachment) {
+        return new IOEventHandler() {
+
+            @Override
+            public void connected(final IOSession session) {
+            }
+
+            @Override
+            public void inputReady(final IOSession session, final ByteBuffer src) {
+            }
+
+            @Override
+            public void outputReady(final IOSession session) {
+            }
+
+            @Override
+            public void timeout(final IOSession session, final Timeout timeout) {
+            }
+
+            @Override
+            public void exception(final IOSession session, final Exception cause) {
+            }
+
+            @Override
+            public void disconnected(final IOSession session) {
+            }
+        };
+    }
+}
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSIntegrationTest.java
new file mode 100644
index 0000000..175dc4f
--- /dev/null
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSIntegrationTest.java
@@ -0,0 +1,330 @@
+/*
+ * ====================================================================
+ * 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
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.testing.nio;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSession;
+
+import org.apache.hc.core5.concurrent.BasicFuture;
+import org.apache.hc.core5.concurrent.DefaultThreadFactory;
+import org.apache.hc.core5.concurrent.FutureContribution;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.ProtocolVersion;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.impl.bootstrap.AsyncServerBootstrap;
+import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer;
+import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy;
+import org.apache.hc.core5.http.nio.ssl.BasicServerTlsStrategy;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.UriPatternMatcher;
+import org.apache.hc.core5.http.ssl.TLS;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.reactor.DefaultConnectingIOReactor;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.reactor.IOSession;
+import org.apache.hc.core5.reactor.ListenerEndpoint;
+import org.apache.hc.core5.reactor.ssl.TlsDetails;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.ssl.SSLContexts;
+import org.apache.hc.core5.testing.SSLTestContexts;
+import org.apache.hc.core5.util.Timeout;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.Rule;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.Extensions;
+import org.junit.jupiter.migrationsupport.rules.ExternalResourceSupport;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.rules.ExternalResource;
+
+@Extensions({@ExtendWith({ExternalResourceSupport.class})})
+public class TLSIntegrationTest {
+
+    private static final Timeout TIMEOUT = Timeout.ofSeconds(30);
+
+    private HttpAsyncServer server;
+
+    @Rule
+    public ExternalResource serverResource = new ExternalResource() {
+
+        @Override
+        protected void after() {
+            if (server != null) {
+                try {
+                    server.close(CloseMode.IMMEDIATE);
+                } catch (final Exception ignore) {
+                }
+            }
+        }
+
+    };
+
+    private DefaultConnectingIOReactor client;
+
+    @Rule
+    public ExternalResource clientResource = new ExternalResource() {
+
+        @Override
+        protected void after() {
+            if (client != null) {
+                try {
+                    client.close(CloseMode.IMMEDIATE);
+                } catch (final Exception ignore) {
+                }
+            }
+        }
+
+    };
+
+    HttpAsyncServer createServer(final TlsStrategy tlsStrategy) {
+        return AsyncServerBootstrap.bootstrap()
+                .setLookupRegistry(new UriPatternMatcher<>())
+                .setIOReactorConfig(
+                        IOReactorConfig.custom()
+                                .setSoTimeout(TIMEOUT)
+                                .setIoThreadCount(1)
+                                .build())
+                .setTlsStrategy(tlsStrategy)
+                .setStreamListener(LoggingHttp1StreamListener.INSTANCE_SERVER)
+                .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
+                .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
+                .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
+                .register("*", () -> new EchoHandler(2048))
+                .create();
+    }
+
+    DefaultConnectingIOReactor createClient() {
+        return new DefaultConnectingIOReactor(
+                new NoopIOEventHandlerFactory(),
+                IOReactorConfig.custom()
+                        .setSoTimeout(TIMEOUT)
+                        .setIoThreadCount(1)
+                        .build(),
+                new DefaultThreadFactory("test-client"),
+                LoggingIOSessionDecorator.INSTANCE,
+                LoggingExceptionCallback.INSTANCE,
+                LoggingIOSessionListener.INSTANCE,
+                null);
+    }
+
+    Future<TlsDetails> executeTlsHandshake(final TlsStrategy clientTlsStrategy) throws Exception {
+        final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTPS);
+        final ListenerEndpoint listener = future.get();
+        final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
+
+        final BasicFuture<TlsDetails> tlsSessionFuture = new BasicFuture<>(null);
+        final HttpHost target = new HttpHost(URIScheme.HTTPS.id, "localhost", address.getPort());
+        client.connect(
+                target,
+                null, null, TIMEOUT, null,
+                new FutureContribution<IOSession>(tlsSessionFuture) {
+
+                    @Override
+                    public void completed(final IOSession ioSession) {
+                        clientTlsStrategy.upgrade(
+                                (TransportSecurityLayer) ioSession,
+                                target, null, TIMEOUT,
+                                new FutureContribution<TransportSecurityLayer>(tlsSessionFuture) {
+
+                                    @Override
+                                    public void completed(final TransportSecurityLayer tls) {
+                                        tlsSessionFuture.completed(tls.getTlsDetails());
+                                    }
+
+                                });
+                    }
+
+                });
+        return tlsSessionFuture;
+    }
+
+    @ParameterizedTest(name = "TLS protocol {0}")
+    @EnumSource(value = TLS.class, names = {"V_1_0", "V_1_1", "V_1_2"})
+    public void testTLSSuccess(final TLS tlsProtocol) throws Exception {
+        final TlsStrategy serverTlsStrategy = new BasicServerTlsStrategy(
+                SSLTestContexts.createServerSSLContext(),
+                (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[]{tlsProtocol.id}),
+                null);
+        server = createServer(serverTlsStrategy);
+        server.start();
+
+        final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext(),
+                (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[]{tlsProtocol.id}),
+                null);
+        client = createClient();
+        client.start();
+
+        final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake(clientTlsStrategy);
+
+        final TlsDetails tlsDetails = tlsSessionFuture.get();
+        Assertions.assertNotNull(tlsDetails);
+        final SSLSession tlsSession = tlsDetails.getSSLSession();
+        final ProtocolVersion tlsVersion = TLS.parse(tlsSession.getProtocol());
+        MatcherAssert.assertThat(tlsVersion.greaterEquals(tlsProtocol.version), CoreMatchers.equalTo(true));
+        MatcherAssert.assertThat(tlsSession.getPeerPrincipal().getName(),
+                CoreMatchers.equalTo("CN=localhost,OU=Apache HttpComponents,O=Apache Software Foundation"));
+    }
+
+    @Test
+    public void testTLSTrustFailure() throws Exception {
+        final TlsStrategy serverTlsStrategy = new BasicServerTlsStrategy(SSLTestContexts.createServerSSLContext());
+        server = createServer(serverTlsStrategy);
+        server.start();
+
+        final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(SSLContexts.createDefault());
+        client = createClient();
+        client.start();
+
+        final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake(clientTlsStrategy);
+
+        final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
+                tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
+        final Throwable cause = exception.getCause();
+        Assertions.assertInstanceOf(SSLHandshakeException.class, cause);
+    }
+
+    @Test
+    public void testTLSClientAuthFailure() throws Exception {
+        final TlsStrategy serverTlsStrategy = new BasicServerTlsStrategy(
+                SSLTestContexts.createServerSSLContext(),
+                (endpoint, sslEngine) -> sslEngine.setNeedClientAuth(true),
+                null);
+        server = createServer(serverTlsStrategy);
+        server.start();
+
+        final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext());
+        client = createClient();
+        client.start();
+
+        final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake(clientTlsStrategy);
+
+        final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
+                tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
+        final Throwable cause = exception.getCause();
+        Assertions.assertInstanceOf(SSLHandshakeException.class, cause);
+    }
+
+    @Test
+    public void testSSLDisabledByDefault() throws Exception {
+        final TlsStrategy serverTlsStrategy = new BasicServerTlsStrategy(
+                SSLTestContexts.createServerSSLContext(),
+                (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[]{"SSLv3"}),
+                null);
+        server = createServer(serverTlsStrategy);
+        server.start();
+
+        final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext());
+        client = createClient();
+        client.start();
+
+        final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake(clientTlsStrategy);
+
+        final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
+                tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
+        final Throwable cause = exception.getCause();
+        Assertions.assertInstanceOf(IOException.class, cause);
+    }
+
+    @ParameterizedTest(name = "cipher {0}")
+    @ValueSource(strings = {
+            "SSL_RSA_WITH_RC4_128_SHA",
+            "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
+            "TLS_DH_anon_WITH_AES_128_CBC_SHA",
+            "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA",
+            "SSL_RSA_WITH_NULL_SHA",
+            "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
+            "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
+            "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA",
+            "TLS_DH_anon_WITH_AES_256_GCM_SHA384",
+            "TLS_ECDH_anon_WITH_AES_256_CBC_SHA",
+            "TLS_RSA_WITH_NULL_SHA256",
+            "SSL_RSA_EXPORT_WITH_RC4_40_MD5",
+            "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5",
+            "TLS_KRB5_EXPORT_WITH_RC4_40_SHA",
+            "SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5"
+    })
+    public void testWeakCipherDisabledByDefault(final String cipher) throws Exception {
+        final TlsStrategy serverTlsStrategy = new BasicServerTlsStrategy(
+                SSLTestContexts.createServerSSLContext(),
+                (endpoint, sslEngine) -> sslEngine.setEnabledCipherSuites(new String[]{cipher}),
+                null);
+        server = createServer(serverTlsStrategy);
+        server.start();
+
+        final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext());
+        client = createClient();
+        client.start();
+
+        final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake(clientTlsStrategy);
+
+        final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
+                tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
+        final Throwable cause = exception.getCause();
+        Assertions.assertInstanceOf(IOException.class, cause);
+    }
+
+    @Test
+    public void testTLSVersionMismatch() throws Exception {
+        final TlsStrategy serverTlsStrategy = new BasicServerTlsStrategy(
+                SSLTestContexts.createServerSSLContext(),
+                (endpoint, sslEngine) -> {
+                    sslEngine.setEnabledProtocols(new String[]{ TLS.V_1_0.id });
+                    sslEngine.setEnabledCipherSuites(new String[]{
+                            "TLS_RSA_WITH_AES_256_CBC_SHA",
+                            "TLS_RSA_WITH_AES_128_CBC_SHA",
+                            "TLS_RSA_WITH_3DES_EDE_CBC_SHA"});
+                },
+                null);
+        server = createServer(serverTlsStrategy);
+        server.start();
+
+        final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(
+                SSLTestContexts.createClientSSLContext(),
+                (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[] { TLS.V_1_2.id, TLS.V_1_3.id }),
+                null);
+        client = createClient();
+        client.start();
+
+        final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake(clientTlsStrategy);
+
+        final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
+                tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
+        final Throwable cause = exception.getCause();
+        Assertions.assertInstanceOf(SSLHandshakeException.class, cause);
+    }
+
+}
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TestDefaultListeningIOReactor.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TestDefaultListeningIOReactor.java
index 945d023..1c25995 100644
--- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TestDefaultListeningIOReactor.java
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TestDefaultListeningIOReactor.java
@@ -28,22 +28,16 @@
 package org.apache.hc.core5.testing.nio;
 
 import java.net.InetSocketAddress;
-import java.nio.ByteBuffer;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 
 import org.apache.hc.core5.io.CloseMode;
 import org.apache.hc.core5.reactor.DefaultListeningIOReactor;
-import org.apache.hc.core5.reactor.IOEventHandler;
-import org.apache.hc.core5.reactor.IOEventHandlerFactory;
 import org.apache.hc.core5.reactor.IOReactorConfig;
 import org.apache.hc.core5.reactor.IOReactorStatus;
-import org.apache.hc.core5.reactor.IOSession;
 import org.apache.hc.core5.reactor.ListenerEndpoint;
-import org.apache.hc.core5.reactor.ProtocolIOSession;
 import org.apache.hc.core5.util.TimeValue;
-import org.apache.hc.core5.util.Timeout;
 import org.junit.jupiter.api.AfterEach;;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
@@ -56,39 +50,6 @@ public class TestDefaultListeningIOReactor {
 
     private DefaultListeningIOReactor ioReactor;
 
-    private static class NoopIOEventHandlerFactory implements IOEventHandlerFactory {
-
-        @Override
-        public IOEventHandler createHandler(final ProtocolIOSession ioSession, final Object attachment) {
-            return new IOEventHandler() {
-
-                @Override
-                public void connected(final IOSession session) {
-                }
-
-                @Override
-                public void inputReady(final IOSession session, final ByteBuffer src) {
-                }
-
-                @Override
-                public void outputReady(final IOSession session) {
-                }
-
-                @Override
-                public void timeout(final IOSession session, final Timeout timeout) {
-                }
-
-                @Override
-                public void exception(final IOSession session, final Exception cause) {
-                }
-
-                @Override
-                public void disconnected(final IOSession session) {
-                }
-            };
-        }
-    }
-
     @BeforeEach
     public void setup() throws Exception {
         final IOReactorConfig reactorConfig = IOReactorConfig.custom()
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLIOSession.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLIOSession.java
index 2d8f94c..ce7ef69 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLIOSession.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLIOSession.java
@@ -43,6 +43,7 @@ import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLEngineResult;
 import javax.net.ssl.SSLEngineResult.HandshakeStatus;
 import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.SSLSession;
 
 import org.apache.hc.core5.annotation.Contract;
@@ -225,6 +226,10 @@ public class SSLIOSession implements IOSession {
 
             @Override
             public void exception(final IOSession protocolSession, final Exception cause) {
+                final FutureCallback<SSLSession> resultCallback = handshakeCallbackRef.getAndSet(null);
+                if (resultCallback != null) {
+                    resultCallback.failed(cause);
+                }
                 final IOEventHandler handler = session.getHandler();
                 if (handshakeStateRef.get() != TLSHandShakeState.COMPLETE) {
                     session.close(CloseMode.GRACEFUL);
@@ -233,10 +238,6 @@ public class SSLIOSession implements IOSession {
                 if (handler != null) {
                     handler.exception(protocolSession, cause);
                 }
-                final FutureCallback<SSLSession> resultCallback = handshakeCallbackRef.getAndSet(null);
-                if (resultCallback != null) {
-                    resultCallback.failed(cause);
-                }
             }
 
             @Override
@@ -447,6 +448,10 @@ public class SSLIOSession implements IOSession {
             if (this.status == Status.ACTIVE
                     && (this.endOfStream || this.sslEngine.isInboundDone())) {
                 this.status = Status.CLOSING;
+                final FutureCallback<SSLSession> resultCallback = handshakeCallbackRef.getAndSet(null);
+                if (resultCallback != null) {
+                    resultCallback.failed(new SSLHandshakeException("TLS handshake failed"));
+                }
             }
             if (this.status == Status.CLOSING && !this.outEncrypted.hasData()) {
                 this.sslEngine.closeOutbound();