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