diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index 06cac36fb14..ddb42150772 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -57,6 +57,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.Jetty; @@ -544,6 +545,7 @@ public class HttpClient extends ContainerLifeCycle public void succeeded(List socketAddresses) { Map context = new HashMap<>(); + context.put(ClientConnectionFactory.CONNECTOR_CONTEXT_KEY, HttpClient.this); context.put(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY, destination); connect(socketAddresses, 0, context); } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java index 43559aa2184..894c78db134 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java @@ -18,10 +18,17 @@ package org.eclipse.jetty.client; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -86,10 +93,30 @@ public class HttpClientTLSTest serverTLSFactory.setIncludeProtocols("TLSv1.2"); startServer(serverTLSFactory, new EmptyServerHandler()); + CountDownLatch serverLatch = new CountDownLatch(1); + connector.addBean(new SslHandshakeListener() + { + @Override + public void handshakeFailed(Event event, Throwable failure) + { + serverLatch.countDown(); + } + }); + SslContextFactory clientTLSFactory = createSslContextFactory(); clientTLSFactory.setIncludeProtocols("TLSv1.1"); startClient(clientTLSFactory); + CountDownLatch clientLatch = new CountDownLatch(1); + client.addBean(new SslHandshakeListener() + { + @Override + public void handshakeFailed(Event event, Throwable failure) + { + clientLatch.countDown(); + } + }); + try { client.newRequest("localhost", connector.getLocalPort()) @@ -102,6 +129,9 @@ public class HttpClientTLSTest { // Expected. } + + Assert.assertTrue(serverLatch.await(1, TimeUnit.SECONDS)); + Assert.assertTrue(clientLatch.await(1, TimeUnit.SECONDS)); } @Test @@ -111,10 +141,30 @@ public class HttpClientTLSTest serverTLSFactory.setIncludeCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA"); startServer(serverTLSFactory, new EmptyServerHandler()); + CountDownLatch serverLatch = new CountDownLatch(1); + connector.addBean(new SslHandshakeListener() + { + @Override + public void handshakeFailed(Event event, Throwable failure) + { + serverLatch.countDown(); + } + }); + SslContextFactory clientTLSFactory = createSslContextFactory(); clientTLSFactory.setExcludeCipherSuites(".*_SHA$"); startClient(clientTLSFactory); + CountDownLatch clientLatch = new CountDownLatch(1); + client.addBean(new SslHandshakeListener() + { + @Override + public void handshakeFailed(Event event, Throwable failure) + { + clientLatch.countDown(); + } + }); + try { client.newRequest("localhost", connector.getLocalPort()) @@ -127,6 +177,9 @@ public class HttpClientTLSTest { // Expected. } + + Assert.assertTrue(serverLatch.await(1, TimeUnit.SECONDS)); + Assert.assertTrue(clientLatch.await(1, TimeUnit.SECONDS)); } @Test @@ -138,9 +191,29 @@ public class HttpClientTLSTest serverTLSFactory.setIncludeCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); startServer(serverTLSFactory, new EmptyServerHandler()); + CountDownLatch serverLatch = new CountDownLatch(1); + connector.addBean(new SslHandshakeListener() + { + @Override + public void handshakeFailed(Event event, Throwable failure) + { + serverLatch.countDown(); + } + }); + SslContextFactory clientTLSFactory = createSslContextFactory(); startClient(clientTLSFactory); + CountDownLatch clientLatch = new CountDownLatch(1); + client.addBean(new SslHandshakeListener() + { + @Override + public void handshakeFailed(Event event, Throwable failure) + { + clientLatch.countDown(); + } + }); + try { client.newRequest("localhost", connector.getLocalPort()) @@ -153,6 +226,9 @@ public class HttpClientTLSTest { // Expected. } + + Assert.assertTrue(serverLatch.await(1, TimeUnit.SECONDS)); + Assert.assertTrue(clientLatch.await(1, TimeUnit.SECONDS)); } @Test @@ -161,12 +237,32 @@ public class HttpClientTLSTest SslContextFactory serverTLSFactory = createSslContextFactory(); startServer(serverTLSFactory, new EmptyServerHandler()); + CountDownLatch serverLatch = new CountDownLatch(1); + connector.addBean(new SslHandshakeListener() + { + @Override + public void handshakeFailed(Event event, Throwable failure) + { + serverLatch.countDown(); + } + }); + SslContextFactory clientTLSFactory = createSslContextFactory(); // TLS 1.1 protocol, but only TLS 1.2 ciphers. clientTLSFactory.setIncludeProtocols("TLSv1.1"); clientTLSFactory.setIncludeCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); startClient(clientTLSFactory); + CountDownLatch clientLatch = new CountDownLatch(1); + client.addBean(new SslHandshakeListener() + { + @Override + public void handshakeFailed(Event event, Throwable failure) + { + clientLatch.countDown(); + } + }); + try { client.newRequest("localhost", connector.getLocalPort()) @@ -179,5 +275,121 @@ public class HttpClientTLSTest { // Expected. } + + Assert.assertTrue(serverLatch.await(1, TimeUnit.SECONDS)); + Assert.assertTrue(clientLatch.await(1, TimeUnit.SECONDS)); + } + + @Test + public void testHandshakeSucceeded() throws Exception + { + SslContextFactory serverTLSFactory = createSslContextFactory(); + startServer(serverTLSFactory, new EmptyServerHandler()); + + CountDownLatch serverLatch = new CountDownLatch(1); + connector.addBean(new SslHandshakeListener() + { + @Override + public void handshakeSucceeded(Event event) + { + serverLatch.countDown(); + } + }); + + SslContextFactory clientTLSFactory = createSslContextFactory(); + startClient(clientTLSFactory); + + CountDownLatch clientLatch = new CountDownLatch(1); + client.addBean(new SslHandshakeListener() + { + @Override + public void handshakeSucceeded(Event event) + { + clientLatch.countDown(); + } + }); + + ContentResponse response = client.GET("https://localhost:" + connector.getLocalPort()); + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + + Assert.assertTrue(serverLatch.await(1, TimeUnit.SECONDS)); + Assert.assertTrue(clientLatch.await(1, TimeUnit.SECONDS)); + } + + @Test + public void testHandshakeSucceededWithSessionResumption() throws Exception + { + SslContextFactory serverTLSFactory = createSslContextFactory(); + startServer(serverTLSFactory, new EmptyServerHandler()); + + AtomicReference serverSession = new AtomicReference<>(); + connector.addBean(new SslHandshakeListener() + { + @Override + public void handshakeSucceeded(Event event) + { + serverSession.set(event.getSSLEngine().getSession().getId()); + } + }); + + SslContextFactory clientTLSFactory = createSslContextFactory(); + startClient(clientTLSFactory); + + AtomicReference clientSession = new AtomicReference<>(); + client.addBean(new SslHandshakeListener() + { + @Override + public void handshakeSucceeded(Event event) + { + clientSession.set(event.getSSLEngine().getSession().getId()); + } + }); + + // First request primes the TLS session. + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(HttpScheme.HTTPS.asString()) + .header(HttpHeader.CONNECTION, "close") + .timeout(5, TimeUnit.SECONDS) + .send(); + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + + Assert.assertNotNull(serverSession.get()); + Assert.assertNotNull(clientSession.get()); + + connector.removeBean(connector.getBean(SslHandshakeListener.class)); + client.removeBean(client.getBean(SslHandshakeListener.class)); + + CountDownLatch serverLatch = new CountDownLatch(1); + connector.addBean(new SslHandshakeListener() + { + @Override + public void handshakeSucceeded(Event event) + { + if (Arrays.equals(serverSession.get(), event.getSSLEngine().getSession().getId())) + serverLatch.countDown(); + } + }); + + CountDownLatch clientLatch = new CountDownLatch(1); + client.addBean(new SslHandshakeListener() + { + @Override + public void handshakeSucceeded(Event event) + { + if (Arrays.equals(clientSession.get(), event.getSSLEngine().getSession().getId())) + clientLatch.countDown(); + } + }); + + // Second request should have the same session ID. + response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(HttpScheme.HTTPS.asString()) + .header(HttpHeader.CONNECTION, "close") + .timeout(5, TimeUnit.SECONDS) + .send(); + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + + Assert.assertTrue(serverLatch.await(1, TimeUnit.SECONDS)); + Assert.assertTrue(clientLatch.await(1, TimeUnit.SECONDS)); } } diff --git a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java index 75006fba0ee..12aecb6260c 100644 --- a/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java +++ b/jetty-http2/http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2Client.java @@ -374,6 +374,7 @@ public class HTTP2Client extends ContainerLifeCycle context.put(SslClientConnectionFactory.SSL_CONTEXT_FACTORY_CONTEXT_KEY, sslContextFactory); context.put(SslClientConnectionFactory.SSL_PEER_HOST_CONTEXT_KEY, address.getHostString()); context.put(SslClientConnectionFactory.SSL_PEER_PORT_CONTEXT_KEY, address.getPort()); + context.putIfAbsent(ClientConnectionFactory.CONNECTOR_CONTEXT_KEY, this); return context; } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java index ee028e14251..d2d265efaaf 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java @@ -26,6 +26,8 @@ import java.util.Map; */ public interface ClientConnectionFactory { + public static final String CONNECTOR_CONTEXT_KEY = "client.connector"; + /** * * @param endPoint the {@link org.eclipse.jetty.io.EndPoint} to link the newly created connection to diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java index 77e3409266f..ef7d47bc81d 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java @@ -27,6 +27,7 @@ import javax.net.ssl.SSLEngine; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; public class SslClientConnectionFactory implements ClientConnectionFactory @@ -61,6 +62,10 @@ public class SslClientConnectionFactory implements ClientConnectionFactory SslConnection sslConnection = newSslConnection(byteBufferPool, executor, endPoint, engine); sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed()); endPoint.setConnection(sslConnection); + + ContainerLifeCycle connector = (ContainerLifeCycle)context.get(ClientConnectionFactory.CONNECTOR_CONTEXT_KEY); + connector.getBeans(SslHandshakeListener.class).forEach(sslConnection::addHandshakeListener); + EndPoint appEndPoint = sslConnection.getDecryptedEndPoint(); appEndPoint.setConnection(connectionFactory.newConnection(appEndPoint, context)); diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java index 945fffff7ba..75249693558 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Executor; import javax.net.ssl.SSLEngine; @@ -80,6 +82,8 @@ public class SslConnection extends AbstractConnection private static final Logger LOG = Log.getLogger(SslConnection.class); private static final ByteBuffer __FILL_CALLED_FLUSH= BufferUtil.allocate(0); private static final ByteBuffer __FLUSH_CALLED_FILL= BufferUtil.allocate(0); + + private final List handshakeListeners = new ArrayList<>(); private final ByteBufferPool _bufferPool; private final SSLEngine _sslEngine; private final DecryptedEndPoint _decryptedEndPoint; @@ -116,6 +120,16 @@ public class SslConnection extends AbstractConnection this._decryptedEndPoint = newDecryptedEndPoint(); } + public void addHandshakeListener(SslHandshakeListener listener) + { + handshakeListeners.add(listener); + } + + public boolean removeHandshakeListener(SslHandshakeListener listener) + { + return handshakeListeners.remove(listener); + } + protected DecryptedEndPoint newDecryptedEndPoint() { return new DecryptedEndPoint(); @@ -593,6 +607,7 @@ public class SslConnection extends AbstractConnection LOG.debug("{} {} handshake succeeded {}/{}", SslConnection.this, _sslEngine.getUseClientMode() ? "client" : "resumed server", _sslEngine.getSession().getProtocol(),_sslEngine.getSession().getCipherSuite()); + notifyHandshakeSucceeded(_sslEngine); } // Check whether renegotiation is allowed @@ -671,13 +686,17 @@ public class SslConnection extends AbstractConnection } catch (SSLHandshakeException x) { + notifyHandshakeFailed(_sslEngine, x); close(x); throw x; } catch (SSLException x) { if (!_handshaken) + { x = (SSLException)new SSLHandshakeException(x.getMessage()).initCause(x); + notifyHandshakeFailed(_sslEngine, x); + } close(x); throw x; } @@ -811,7 +830,10 @@ public class SslConnection extends AbstractConnection { _handshaken = true; if (LOG.isDebugEnabled()) - LOG.debug("{} server handshake succeeded {}/{}", SslConnection.this, _sslEngine.getSession().getProtocol(),_sslEngine.getSession().getCipherSuite()); + LOG.debug("{} {} handshake succeeded {}/{}", SslConnection.this, + _sslEngine.getUseClientMode() ? "resumed client" : "server", + _sslEngine.getSession().getProtocol(),_sslEngine.getSession().getCipherSuite()); + notifyHandshakeSucceeded(_sslEngine); } HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus(); @@ -875,6 +897,7 @@ public class SslConnection extends AbstractConnection } catch (SSLHandshakeException x) { + notifyHandshakeFailed(_sslEngine, x); close(x); throw x; } @@ -961,6 +984,42 @@ public class SslConnection extends AbstractConnection return _sslEngine.isInboundDone(); } + private void notifyHandshakeSucceeded(SSLEngine sslEngine) + { + SslHandshakeListener.Event event = null; + for (SslHandshakeListener listener : handshakeListeners) + { + if (event == null) + event = new SslHandshakeListener.Event(sslEngine); + try + { + listener.handshakeSucceeded(event); + } + catch (Throwable x) + { + LOG.info("Exception while notifying listener " + listener, x); + } + } + } + + private void notifyHandshakeFailed(SSLEngine sslEngine, Throwable failure) + { + SslHandshakeListener.Event event = null; + for (SslHandshakeListener listener : handshakeListeners) + { + if (event == null) + event = new SslHandshakeListener.Event(sslEngine); + try + { + listener.handshakeFailed(event, failure); + } + catch (Throwable x) + { + LOG.info("Exception while notifying listener " + listener, x); + } + } + } + @Override public String toString() { diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java new file mode 100644 index 00000000000..3b592b7ca42 --- /dev/null +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java @@ -0,0 +1,71 @@ +// +// ======================================================================== +// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io.ssl; + +import java.util.EventListener; +import java.util.EventObject; + +import javax.net.ssl.SSLEngine; + +/** + *

Implementations of this interface are notified of TLS handshake events.

+ *

Similar to {@link javax.net.ssl.HandshakeCompletedListener}, but for {@link SSLEngine}.

+ *

Typical usage if to add instances of this class as beans to a server connector, or + * to a client connector.

+ */ +public interface SslHandshakeListener extends EventListener +{ + /** + *

Callback method invoked when the TLS handshake succeeds.

+ * + * @param event the event object carrying information about the TLS handshake event + */ + default void handshakeSucceeded(Event event) + { + } + + /** + *

Callback method invoked when the TLS handshake fails.

+ * + * @param event the event object carrying information about the TLS handshake event + * @param failure the failure that caused the TLS handshake to fail + */ + default void handshakeFailed(Event event, Throwable failure) + { + } + + /** + *

The event object carrying information about TLS handshake events.

+ */ + public static class Event extends EventObject + { + public Event(Object source) + { + super(source); + } + + /** + * @return the SSLEngine associated to the TLS handshake event + */ + public SSLEngine getSSLEngine() + { + return (SSLEngine)getSource(); + } + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java index 680288c238e..61e473e0910 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java @@ -24,10 +24,13 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSession; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ssl.SslConnection; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; import org.eclipse.jetty.util.annotation.Name; +import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; public class SslConnectionFactory extends AbstractConnectionFactory @@ -94,6 +97,22 @@ public class SslConnectionFactory extends AbstractConnectionFactory return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine); } + @Override + protected AbstractConnection configure(AbstractConnection connection, Connector connector, EndPoint endPoint) + { + if (connection instanceof SslConnection) + { + SslConnection sslConnection = (SslConnection)connection; + if (connector instanceof ContainerLifeCycle) + { + ContainerLifeCycle container = (ContainerLifeCycle)connector; + container.getBeans(SslHandshakeListener.class).forEach(sslConnection::addHandshakeListener); + } + getBeans(SslHandshakeListener.class).forEach(sslConnection::addHandshakeListener); + } + return super.configure(connection, connector, endPoint); + } + @Override public String toString() {