View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.core5.testing.nio;
29  
30  import java.io.IOException;
31  import java.net.InetSocketAddress;
32  import java.net.SocketAddress;
33  import java.util.concurrent.ExecutionException;
34  import java.util.concurrent.Future;
35  import java.util.stream.Stream;
36  
37  import javax.net.ssl.SSLContext;
38  import javax.net.ssl.SSLHandshakeException;
39  import javax.net.ssl.SSLSession;
40  
41  import org.apache.hc.core5.concurrent.BasicFuture;
42  import org.apache.hc.core5.concurrent.FutureCallback;
43  import org.apache.hc.core5.concurrent.FutureContribution;
44  import org.apache.hc.core5.http.ContentType;
45  import org.apache.hc.core5.http.HttpHost;
46  import org.apache.hc.core5.http.HttpResponse;
47  import org.apache.hc.core5.http.Message;
48  import org.apache.hc.core5.http.Method;
49  import org.apache.hc.core5.http.ProtocolVersion;
50  import org.apache.hc.core5.http.URIScheme;
51  import org.apache.hc.core5.http.impl.bootstrap.AsyncRequesterBootstrap;
52  import org.apache.hc.core5.http.impl.bootstrap.AsyncServerBootstrap;
53  import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester;
54  import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer;
55  import org.apache.hc.core5.http.nio.AsyncClientEndpoint;
56  import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
57  import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
58  import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy;
59  import org.apache.hc.core5.http.nio.ssl.BasicServerTlsStrategy;
60  import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
61  import org.apache.hc.core5.http.nio.ssl.TlsSupport;
62  import org.apache.hc.core5.http.nio.ssl.TlsUpgradeCapable;
63  import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
64  import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
65  import org.apache.hc.core5.http.protocol.UriPatternMatcher;
66  import org.apache.hc.core5.http.ssl.TLS;
67  import org.apache.hc.core5.io.CloseMode;
68  import org.apache.hc.core5.net.NamedEndpoint;
69  import org.apache.hc.core5.reactor.IOReactorConfig;
70  import org.apache.hc.core5.reactor.ListenerEndpoint;
71  import org.apache.hc.core5.reactor.ProtocolIOSession;
72  import org.apache.hc.core5.reactor.ssl.SSLBufferMode;
73  import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer;
74  import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier;
75  import org.apache.hc.core5.reactor.ssl.TlsDetails;
76  import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
77  import org.apache.hc.core5.ssl.SSLContexts;
78  import org.apache.hc.core5.testing.SSLTestContexts;
79  import org.apache.hc.core5.testing.classic.LoggingConnPoolListener;
80  import org.apache.hc.core5.util.Args;
81  import org.apache.hc.core5.util.ReflectionUtils;
82  import org.apache.hc.core5.util.Timeout;
83  import org.hamcrest.CoreMatchers;
84  import org.hamcrest.MatcherAssert;
85  import org.junit.jupiter.api.Assertions;
86  import org.junit.jupiter.api.Test;
87  import org.junit.jupiter.api.extension.AfterEachCallback;
88  import org.junit.jupiter.api.extension.ExtensionContext;
89  import org.junit.jupiter.api.extension.RegisterExtension;
90  import org.junit.jupiter.params.ParameterizedTest;
91  import org.junit.jupiter.params.provider.Arguments;
92  import org.junit.jupiter.params.provider.ArgumentsProvider;
93  import org.junit.jupiter.params.provider.ArgumentsSource;
94  import org.junit.jupiter.params.provider.ValueSource;
95  
96  public class TLSIntegrationTest {
97  
98      private static final Timeout TIMEOUT = Timeout.ofSeconds(30);
99  
100     private HttpAsyncServer server;
101 
102     @RegisterExtension
103     public final AfterEachCallback serverCleanup = new AfterEachCallback() {
104 
105         @Override
106         public void afterEach(final ExtensionContext context) throws Exception {
107             if (server != null) {
108                 try {
109                     server.close(CloseMode.IMMEDIATE);
110                 } catch (final Exception ignore) {
111                 }
112             }
113         }
114 
115     };
116 
117     private HttpAsyncRequester client;
118 
119     @RegisterExtension
120     public final AfterEachCallback clientCleanup = new AfterEachCallback() {
121 
122         @Override
123         public void afterEach(final ExtensionContext context) throws Exception {
124             if (client != null) {
125                 try {
126                     client.close(CloseMode.GRACEFUL);
127                 } catch (final Exception ignore) {
128                 }
129             }
130         }
131 
132     };
133 
134     HttpAsyncServer createServer(final TlsStrategy tlsStrategy) {
135         return AsyncServerBootstrap.bootstrap()
136                 .setLookupRegistry(new UriPatternMatcher<>())
137                 .setIOReactorConfig(
138                         IOReactorConfig.custom()
139                                 .setSoTimeout(TIMEOUT)
140                                 .setIoThreadCount(1)
141                                 .build())
142                 .setTlsStrategy(tlsStrategy)
143                 .setStreamListener(LoggingHttp1StreamListener.INSTANCE_SERVER)
144                 .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
145                 .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
146                 .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
147                 .register("*", () -> new EchoHandler(2048))
148                 .create();
149     }
150 
151     HttpAsyncRequester createClient(final TlsStrategy tlsStrategy) {
152         return AsyncRequesterBootstrap.bootstrap()
153                 .setIOReactorConfig(IOReactorConfig.custom()
154                         .setSoTimeout(TIMEOUT)
155                         .build())
156                 .setTlsStrategy(tlsStrategy)
157                 .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
158                 .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
159                 .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
160                 .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
161                 .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
162                 .create();
163     }
164 
165     Future<TlsDetails> executeTlsHandshake() throws Exception {
166         final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTPS);
167         final ListenerEndpoint listener = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
168         final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
169 
170         final HttpHost target = new HttpHost(URIScheme.HTTPS.id, "localhost", address.getPort());
171 
172         final BasicFuture<TlsDetails> tlsFuture = new BasicFuture<>(null);
173         client.connect(
174                 new HttpHost(URIScheme.HTTP.id, "localhost", address.getPort()),
175                 TIMEOUT, null,
176                 new FutureContribution<AsyncClientEndpoint>(tlsFuture) {
177 
178                     @Override
179                     public void completed(final AsyncClientEndpoint clientEndpoint) {
180                         try {
181                             ((TlsUpgradeCapable) clientEndpoint).tlsUpgrade(
182                                     target,
183                                     new FutureContribution<ProtocolIOSession>(tlsFuture) {
184 
185                                         @Override
186                                         public void completed(final ProtocolIOSession protocolIOSession) {
187                                             tlsFuture.completed(protocolIOSession.getTlsDetails());
188                                         }
189 
190                                     });
191                         } catch (final Exception ex) {
192                             tlsFuture.failed(ex);
193                         }
194                     }
195 
196                 });
197         return tlsFuture;
198     }
199 
200     @ParameterizedTest(name = "TLS protocol {0}")
201     @ArgumentsSource(SupportedTLSProtocolProvider.class)
202     public void testTLSSuccess(final TLS tlsProtocol) throws Exception {
203         final TlsStrategy serverTlsStrategy = new TestTlsStrategy(
204                 SSLTestContexts.createServerSSLContext(),
205                 (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[]{tlsProtocol.getId()}),
206                 null);
207         server = createServer(serverTlsStrategy);
208         server.start();
209 
210         final TlsStrategy clientTlsStrategy = new TestTlsStrategy(SSLTestContexts.createClientSSLContext(),
211                 (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[]{tlsProtocol.getId()}),
212                 null);
213         client = createClient(clientTlsStrategy);
214         client.start();
215 
216         final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake();
217 
218         final TlsDetails tlsDetails = tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
219         Assertions.assertNotNull(tlsDetails);
220         final SSLSession tlsSession = tlsDetails.getSSLSession();
221         final ProtocolVersion tlsVersion = TLS.parse(tlsSession.getProtocol());
222         MatcherAssert.assertThat(tlsVersion.greaterEquals(tlsProtocol.version), CoreMatchers.equalTo(true));
223         MatcherAssert.assertThat(tlsSession.getPeerPrincipal().getName(),
224                 CoreMatchers.equalTo("CN=localhost,OU=Apache HttpComponents,O=Apache Software Foundation"));
225     }
226 
227     @Test
228     public void testTLSTrustFailure() throws Exception {
229         final TlsStrategy serverTlsStrategy = new BasicServerTlsStrategy(SSLTestContexts.createServerSSLContext());
230         server = createServer(serverTlsStrategy);
231         server.start();
232 
233         final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(SSLContexts.createDefault());
234         client = createClient(clientTlsStrategy);
235         client.start();
236 
237         final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake();
238 
239         final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
240                 tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
241         final Throwable cause = exception.getCause();
242         Assertions.assertInstanceOf(SSLHandshakeException.class, cause);
243     }
244 
245     @Test
246     public void testTLSClientAuthFailure() throws Exception {
247         final TlsStrategy serverTlsStrategy = new BasicServerTlsStrategy(
248                 SSLTestContexts.createServerSSLContext(),
249                 (endpoint, sslEngine) -> sslEngine.setNeedClientAuth(true),
250                 null);
251         server = createServer(serverTlsStrategy);
252         server.start();
253 
254         final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext());
255         client = createClient(clientTlsStrategy);
256         client.start();
257 
258         final Future<ListenerEndpoint> future = server.listen(new InetSocketAddress(0), URIScheme.HTTPS);
259         final ListenerEndpoint listener = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
260         final InetSocketAddress address = (InetSocketAddress) listener.getAddress();
261 
262         final HttpHost target = new HttpHost(URIScheme.HTTPS.id, "localhost", address.getPort());
263 
264         final Future<Message<HttpResponse, String>> resultFuture = client.execute(
265                 new BasicRequestProducer(Method.POST, target, "/stuff",
266                         new StringAsyncEntityProducer("some stuff", ContentType.TEXT_PLAIN)),
267                 new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), TIMEOUT, null);
268 
269         final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
270                 resultFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
271         final Throwable cause = exception.getCause();
272         Assertions.assertInstanceOf(IOException.class, cause);
273     }
274 
275     @Test
276     public void testSSLDisabledByDefault() throws Exception {
277         final TlsStrategy serverTlsStrategy = new TestTlsStrategy(
278                 SSLTestContexts.createServerSSLContext(),
279                 (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[]{"SSLv3"}),
280                 null);
281         server = createServer(serverTlsStrategy);
282         server.start();
283 
284         final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext());
285         client = createClient(clientTlsStrategy);
286         client.start();
287 
288         final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake();
289 
290         Assertions.assertThrows(ExecutionException.class, () ->
291                 tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
292     }
293 
294     @ParameterizedTest(name = "cipher {0}")
295     @ValueSource(strings = {
296             "SSL_RSA_WITH_RC4_128_SHA",
297             "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
298             "TLS_DH_anon_WITH_AES_128_CBC_SHA",
299             "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA",
300             "SSL_RSA_WITH_NULL_SHA",
301             "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
302             "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
303             "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA",
304             "TLS_DH_anon_WITH_AES_256_GCM_SHA384",
305             "TLS_ECDH_anon_WITH_AES_256_CBC_SHA",
306             "TLS_RSA_WITH_NULL_SHA256",
307             "SSL_RSA_EXPORT_WITH_RC4_40_MD5",
308             "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5",
309             "TLS_KRB5_EXPORT_WITH_RC4_40_SHA",
310             "SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5"
311     })
312     public void testWeakCipherDisabledByDefault(final String cipher) throws Exception {
313         final TlsStrategy serverTlsStrategy = new TestTlsStrategy(
314                 SSLTestContexts.createServerSSLContext(),
315                 (endpoint, sslEngine) -> sslEngine.setEnabledCipherSuites(new String[]{cipher}),
316                 null);
317         server = createServer(serverTlsStrategy);
318         server.start();
319 
320         final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext());
321         client = createClient(clientTlsStrategy);
322         client.start();
323 
324         final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake();
325 
326         Assertions.assertThrows(ExecutionException.class, () ->
327                 tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
328     }
329 
330     @Test
331     public void testTLSVersionMismatch() throws Exception {
332         final TlsStrategy serverTlsStrategy = new TestTlsStrategy(
333                 SSLTestContexts.createServerSSLContext(),
334                 (endpoint, sslEngine) -> {
335                     sslEngine.setEnabledProtocols(new String[]{TLS.V_1_0.getId()});
336                     sslEngine.setEnabledCipherSuites(new String[]{
337                             "TLS_RSA_WITH_AES_256_CBC_SHA",
338                             "TLS_RSA_WITH_AES_128_CBC_SHA",
339                             "TLS_RSA_WITH_3DES_EDE_CBC_SHA"});
340                 },
341                 null);
342         server = createServer(serverTlsStrategy);
343         server.start();
344 
345         final TlsStrategy clientTlsStrategy = new BasicClientTlsStrategy(
346                 SSLTestContexts.createClientSSLContext(),
347                 (endpoint, sslEngine) -> sslEngine.setEnabledProtocols(new String[]{TLS.V_1_2.getId()}),
348                 null);
349         client = createClient(clientTlsStrategy);
350         client.start();
351 
352         final Future<TlsDetails> tlsSessionFuture = executeTlsHandshake();
353 
354         final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () ->
355                 tlsSessionFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()));
356         final Throwable cause = exception.getCause();
357         Assertions.assertInstanceOf(IOException.class, cause);
358     }
359 
360     static class SupportedTLSProtocolProvider implements ArgumentsProvider {
361 
362         int javaVere = ReflectionUtils.determineJRELevel();
363 
364         @Override
365         public Stream<? extends Arguments> provideArguments(final ExtensionContext context) {
366             if (javaVere >= 11) {
367                 return Stream.of(Arguments.of(TLS.V_1_2), Arguments.of(TLS.V_1_3));
368             } else {
369                 return Stream.of(Arguments.of(TLS.V_1_2));
370             }
371         }
372     }
373 
374     static class TestTlsStrategy implements TlsStrategy {
375 
376         private final SSLContext sslContext;
377         private final SSLSessionInitializer initializer;
378         private final SSLSessionVerifier verifier;
379 
380         public TestTlsStrategy(
381                 final SSLContext sslContext,
382                 final SSLSessionInitializer initializer,
383                 final SSLSessionVerifier verifier) {
384             this.sslContext = Args.notNull(sslContext, "SSL context");
385             this.initializer = initializer;
386             this.verifier = verifier;
387         }
388 
389         @Override
390         public void upgrade(
391                 final TransportSecurityLayer tlsSession,
392                 final NamedEndpoint endpoint,
393                 final Object attachment,
394                 final Timeout handshakeTimeout,
395                 final FutureCallback<TransportSecurityLayer> callback) {
396             tlsSession.startTls(sslContext, endpoint, SSLBufferMode.STATIC,
397                     TlsSupport.enforceStrongSecurity(initializer), verifier, handshakeTimeout, callback);
398         }
399 
400         /**
401          * @deprecated do not use.
402          */
403         @Deprecated
404         @Override
405         public boolean upgrade(
406                 final TransportSecurityLayer tlsSession,
407                 final HttpHost host,
408                 final SocketAddress localAddress,
409                 final SocketAddress remoteAddress,
410                 final Object attachment,
411                 final Timeout handshakeTimeout) {
412             tlsSession.startTls(sslContext, host, SSLBufferMode.STATIC,
413                     TlsSupport.enforceStrongSecurity(initializer), verifier, handshakeTimeout, null);
414             return true;
415         }
416 
417     }
418 
419 }