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/07 12:46:07 UTC
[httpcomponents-core] branch master updated: 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 master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git
The following commit(s) were added to refs/heads/master by this push:
new e556a2906 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
e556a2906 is described below
commit e556a29067b8deabbe86bbf6681ef18b6b2b61ac
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 | 419 +++++++++++++++++++++
.../testing/nio/TestDefaultListeningIOReactor.java | 39 --
.../apache/hc/core5/reactor/ssl/SSLIOSession.java | 13 +-
5 files changed, 496 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 2eb38e833..000000000
--- 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 000000000..1caefc564
--- /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 000000000..b66da40e8
--- /dev/null
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSIntegrationTest.java
@@ -0,0 +1,419 @@
+/*
+ * ====================================================================
+ * 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.net.SocketAddress;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.stream.Stream;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSession;
+
+import org.apache.hc.core5.concurrent.BasicFuture;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.concurrent.FutureContribution;
+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.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.AsyncRequesterBootstrap;
+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.AsyncClientEndpoint;
+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.ssl.TlsStrategy;
+import org.apache.hc.core5.http.nio.ssl.TlsSupport;
+import org.apache.hc.core5.http.nio.ssl.TlsUpgradeCapable;
+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.io.CloseMode;
+import org.apache.hc.core5.net.NamedEndpoint;
+import org.apache.hc.core5.reactor.IOReactorConfig;
+import org.apache.hc.core5.reactor.ListenerEndpoint;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.apache.hc.core5.reactor.ssl.SSLBufferMode;
+import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer;
+import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier;
+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.testing.classic.LoggingConnPoolListener;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.ReflectionUtils;
+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.ExtensionContext;
+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.Arguments;
+import org.junit.jupiter.params.provider.ArgumentsProvider;
+import org.junit.jupiter.params.provider.ArgumentsSource;
+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 HttpAsyncRequester 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();
+ }
+
+ HttpAsyncRequester createClient(final TlsStrategy tlsStrategy) {
+ return AsyncRequesterBootstrap.bootstrap()
+ .setIOReactorConfig(IOReactorConfig.custom()
+ .setSoTimeout(TIMEOUT)
+ .build())
+ .setTlsStrategy(tlsStrategy)
+ .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
+ .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
+ .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
+ .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
+ .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
+ .create();
+ }
+
+ Future<TlsDetails> executeTlsHandshake() throws Exception {
+ final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTPS);
+ final ListenerEndpoint listener = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
+
+ final HttpHost target = new HttpHost(URIScheme.HTTPS.id, "localhost", address.getPort());
+
+ final BasicFuture<TlsDetails> tlsFuture = new BasicFuture<>(null);
+ client.connect(
+ new HttpHost(URIScheme.HTTP.id, "localhost", address.getPort()),
+ TIMEOUT, null,
+ new FutureContribution<AsyncClientEndpoint>(tlsFuture) {
+
+ @Override
+ public void completed(final AsyncClientEndpoint clientEndpoint) {
+ try {
+ ((TlsUpgradeCapable) clientEndpoint).tlsUpgrade(
+ target,
+ new FutureContribution<ProtocolIOSession>(tlsFuture) {
+
+ @Override
+ public void completed(final ProtocolIOSession protocolIOSession) {
+ tlsFuture.completed(protocolIOSession.getTlsDetails());
+ }
+
+ });
+ } catch (final Exception ex) {
+ tlsFuture.failed(ex);
+ }
+ }
+
+ });
+ return tlsFuture;
+ }
+
+ @ParameterizedTest(name = "TLS protocol {0}")
+ @ArgumentsSource(SupportedTLSProtocolProvider.class)
+ public void testTLSSuccess(final TLS tlsProtocol) throws Exception {
+ final TlsStrategy serverTlsStrategy = new TestTlsStrategy(
+ SSLTestContexts.createServerSSLContext(),
+ (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[]{tlsProtocol.id}),
+ null);
+ server = createServer(serverTlsStrategy);
+ server.start();
+
+ final TlsStrategy clientTlsStrategy = new TestTlsStrategy(SSLTestContexts.createClientSSLContext(),
+ (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[]{tlsProtocol.id}),
+ null);
+ client = createClient(clientTlsStrategy);
+ client.start();
+
+ final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake();
+
+ final TlsDetails tlsDetails = tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ 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(clientTlsStrategy);
+ client.start();
+
+ final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake();
+
+ 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(clientTlsStrategy);
+ client.start();
+
+ final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTPS);
+ final ListenerEndpoint listener = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
+
+ final HttpHost target = new HttpHost(URIScheme.HTTPS.id, "localhost", address.getPort());
+
+ final Future<Message<HttpResponse, String>> resultFuture = client.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, () ->
+ resultFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
+ final Throwable cause = exception.getCause();
+ Assertions.assertInstanceOf(IOException.class, cause);
+ }
+
+ @Test
+ public void testSSLDisabledByDefault() throws Exception {
+ final TlsStrategy serverTlsStrategy = new TestTlsStrategy(
+ SSLTestContexts.createServerSSLContext(),
+ (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[]{"SSLv3"}),
+ null);
+ server = createServer(serverTlsStrategy);
+ server.start();
+
+ final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext());
+ client = createClient(clientTlsStrategy);
+ client.start();
+
+ final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake();
+
+ Assertions.assertThrows(ExecutionException.class, () ->
+ tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
+ }
+
+ @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 TestTlsStrategy(
+ SSLTestContexts.createServerSSLContext(),
+ (endpoint, sslEngine) -> sslEngine.setEnabledCipherSuites(new String[]{cipher}),
+ null);
+ server = createServer(serverTlsStrategy);
+ server.start();
+
+ final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext());
+ client = createClient(clientTlsStrategy);
+ client.start();
+
+ final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake();
+
+ Assertions.assertThrows(ExecutionException.class, () ->
+ tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
+ }
+
+ @Test
+ public void testTLSVersionMismatch() throws Exception {
+ final TlsStrategy serverTlsStrategy = new TestTlsStrategy(
+ 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}),
+ null);
+ client = createClient(clientTlsStrategy);
+ client.start();
+
+ final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake();
+
+ final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
+ tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
+ final Throwable cause = exception.getCause();
+ Assertions.assertInstanceOf(SSLHandshakeException.class, cause);
+ }
+
+ static class SupportedTLSProtocolProvider implements ArgumentsProvider {
+
+ int javaVere = ReflectionUtils.determineJRELevel();
+
+ @Override
+ public Stream<? extends Arguments> provideArguments(final ExtensionContext context) {
+ if (javaVere >= 11) {
+ return Stream.of(Arguments.of(TLS.V_1_2), Arguments.of(TLS.V_1_3));
+ } else {
+ return Stream.of(Arguments.of(TLS.V_1_2));
+ }
+ }
+ }
+
+ static class TestTlsStrategy implements TlsStrategy {
+
+ private final SSLContext sslContext;
+ private final SSLSessionInitializer initializer;
+ private final SSLSessionVerifier verifier;
+
+ public TestTlsStrategy(
+ final SSLContext sslContext,
+ final SSLSessionInitializer initializer,
+ final SSLSessionVerifier verifier) {
+ this.sslContext = Args.notNull(sslContext, "SSL context");
+ this.initializer = initializer;
+ this.verifier = verifier;
+ }
+
+ @Override
+ public void upgrade(
+ final TransportSecurityLayer tlsSession,
+ final NamedEndpoint endpoint,
+ final Object attachment,
+ final Timeout handshakeTimeout,
+ final FutureCallback<TransportSecurityLayer> callback) {
+ tlsSession.startTls(sslContext, endpoint, SSLBufferMode.STATIC,
+ TlsSupport.enforceStrongSecurity(initializer), verifier, handshakeTimeout, callback);
+ }
+
+ @Override
+ public boolean upgrade(
+ final TransportSecurityLayer tlsSession,
+ final HttpHost host,
+ final SocketAddress localAddress,
+ final SocketAddress remoteAddress,
+ final Object attachment,
+ final Timeout handshakeTimeout) {
+ tlsSession.startTls(sslContext, host, SSLBufferMode.STATIC,
+ TlsSupport.enforceStrongSecurity(initializer), verifier, handshakeTimeout, null);
+ return true;
+ }
+
+ }
+
+}
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 945d023bc..1c25995a2 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 4d8e48b16..5e5210eb8 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
@@ -448,6 +449,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();