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 953855afc9a..dd2c7bccdca 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 @@ -966,6 +966,7 @@ public class HttpClient extends ContainerLifeCycle engine.setUseClientMode(true); SslConnection sslConnection = newSslConnection(HttpClient.this, endPoint, engine); + sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed()); EndPoint appEndPoint = sslConnection.getDecryptedEndPoint(); HttpConnection connection = newHttpConnection(HttpClient.this, appEndPoint, destination); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java new file mode 100644 index 00000000000..67c99310cc8 --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java @@ -0,0 +1,369 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 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.client.ssl; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.SocketTimeoutException; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.FutureResponseListener; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class SslBytesClientTest extends SslBytesTest +{ + private ExecutorService threadPool; + private HttpClient client; + private SslContextFactory sslContextFactory; + private SSLServerSocket acceptor; + private SimpleProxy proxy; + + @Before + public void init() throws Exception + { + threadPool = Executors.newCachedThreadPool(); + + client = new HttpClient(new SslContextFactory(true)); + client.setMaxConnectionsPerDestination(1); + File keyStore = MavenTestingUtils.getTestResourceFile("keystore.jks"); + sslContextFactory = client.getSslContextFactory(); + sslContextFactory.setKeyStorePath(keyStore.getAbsolutePath()); + sslContextFactory.setKeyStorePassword("storepwd"); + client.start(); + + SSLContext sslContext = sslContextFactory.getSslContext(); + acceptor = (SSLServerSocket)sslContext.getServerSocketFactory().createServerSocket(43191); + + int serverPort = acceptor.getLocalPort(); + + proxy = new SimpleProxy(threadPool, "localhost", serverPort); + proxy.start(); + logger.info(":{} <==> :{}", proxy.getPort(), serverPort); + } + + @After + public void destroy() throws Exception + { + if (acceptor != null) + acceptor.close(); + if (proxy != null) + proxy.stop(); + if (client != null) + client.stop(); + if (threadPool != null) + threadPool.shutdownNow(); + } + + @Test + public void testHandshake() throws Exception + { + Request request = client.newRequest("localhost", proxy.getPort()); + FutureResponseListener listener = new FutureResponseListener(request); + request.scheme(HttpScheme.HTTPS.asString()).send(listener); + + Assert.assertTrue(proxy.awaitClient(5, TimeUnit.SECONDS)); + + final SSLSocket server = (SSLSocket)acceptor.accept(); + server.setUseClientMode(false); + + Future handshake = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + server.startHandshake(); + return null; + } + }); + + // Client Hello + TLSRecord record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToServer(record); + + // Server Hello + Certificate + Server Done + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToClient(record); + + // Client Key Exchange + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToServer(record); + + // Change Cipher Spec + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType()); + proxy.flushToServer(record); + + // Client Done + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToServer(record); + + // Change Cipher Spec + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType()); + proxy.flushToClient(record); + + // Server Done + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToClient(record); + + Assert.assertNull(handshake.get(5, TimeUnit.SECONDS)); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + // Read request + BufferedReader reader = new BufferedReader(new InputStreamReader(server.getInputStream(), "UTF-8")); + String line = reader.readLine(); + Assert.assertTrue(line.startsWith("GET")); + while (line.length() > 0) + line = reader.readLine(); + + // Write response + OutputStream output = server.getOutputStream(); + output.write(("HTTP/1.1 200 OK\r\n" + + "Content-Length: 0\r\n" + + "\r\n").getBytes("UTF-8")); + output.flush(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + ContentResponse response = listener.get(5, TimeUnit.SECONDS); + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + + server.close(); + } + + @Test + public void testServerRenegotiation() throws Exception + { + Request request = client.newRequest("localhost", proxy.getPort()); + FutureResponseListener listener = new FutureResponseListener(request); + request.scheme(HttpScheme.HTTPS.asString()).send(listener); + + Assert.assertTrue(proxy.awaitClient(5, TimeUnit.SECONDS)); + + final SSLSocket server = (SSLSocket)acceptor.accept(); + server.setUseClientMode(false); + + Future handshake = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + server.startHandshake(); + return null; + } + }); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + Assert.assertNull(handshake.get(5, TimeUnit.SECONDS)); + + // Read request + InputStream serverInput = server.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput, "UTF-8")); + String line = reader.readLine(); + Assert.assertTrue(line.startsWith("GET")); + while (line.length() > 0) + line = reader.readLine(); + + OutputStream serverOutput = server.getOutputStream(); + byte[] data1 = new byte[1024]; + Arrays.fill(data1, (byte)'X'); + String content1 = new String(data1, "UTF-8"); + byte[] data2 = new byte[1024]; + Arrays.fill(data2, (byte)'Y'); + final String content2 = new String(data2, "UTF-8"); + // Write first part of the response + serverOutput.write(("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: " + (content1.length() + content2.length()) + "\r\n" + + "\r\n" + + content1).getBytes("UTF-8")); + serverOutput.flush(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + // Renegotiate + Future renegotiation = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + server.startHandshake(); + return null; + } + }); + + // Renegotiation Handshake + TLSRecord record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToClient(record); + + // Renegotiation Handshake + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToServer(record); + + // Trigger a read to have the server write the final renegotiation steps + server.setSoTimeout(100); + try + { + serverInput.read(); + Assert.fail(); + } + catch (SocketTimeoutException x) + { + // Expected + } + + // Renegotiation Handshake + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToClient(record); + + // Renegotiation Change Cipher + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType()); + proxy.flushToClient(record); + + // Renegotiation Handshake + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToClient(record); + + // Renegotiation Change Cipher + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType()); + proxy.flushToServer(record); + + // Renegotiation Handshake + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToServer(record); + + Assert.assertNull(renegotiation.get(5, TimeUnit.SECONDS)); + + // Complete the response + automaticProxyFlow = proxy.startAutomaticFlow(); + serverOutput.write(data2); + serverOutput.flush(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + ContentResponse response = listener.get(5, TimeUnit.SECONDS); + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + Assert.assertEquals(data1.length + data2.length, response.getContent().length); + + server.close(); + } + + @Test + public void testServerRenegotiationWhenRenegotiationIsForbidden() throws Exception + { + sslContextFactory.setRenegotiationAllowed(false); + + Request request = client.newRequest("localhost", proxy.getPort()); + FutureResponseListener listener = new FutureResponseListener(request); + request.scheme(HttpScheme.HTTPS.asString()).send(listener); + + Assert.assertTrue(proxy.awaitClient(5, TimeUnit.SECONDS)); + + final SSLSocket server = (SSLSocket)acceptor.accept(); + server.setUseClientMode(false); + + Future handshake = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + server.startHandshake(); + return null; + } + }); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + Assert.assertNull(handshake.get(5, TimeUnit.SECONDS)); + + // Read request + InputStream serverInput = server.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput, "UTF-8")); + String line = reader.readLine(); + Assert.assertTrue(line.startsWith("GET")); + while (line.length() > 0) + line = reader.readLine(); + + OutputStream serverOutput = server.getOutputStream(); + byte[] data1 = new byte[1024]; + Arrays.fill(data1, (byte)'X'); + String content1 = new String(data1, "UTF-8"); + byte[] data2 = new byte[1024]; + Arrays.fill(data2, (byte)'Y'); + final String content2 = new String(data2, "UTF-8"); + // Write first part of the response + serverOutput.write(("HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: " + (content1.length() + content2.length()) + "\r\n" + + "\r\n" + + content1).getBytes("UTF-8")); + serverOutput.flush(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + // Renegotiate + threadPool.submit(new Callable() + { + public Object call() throws Exception + { + server.startHandshake(); + return null; + } + }); + + // Renegotiation Handshake + TLSRecord record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToClient(record); + + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); + proxy.flushToServer(record); + + record = proxy.readFromClient(); + Assert.assertNull(record); + proxy.flushToServer(record); + + server.close(); + } +} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java similarity index 95% rename from jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslBytesServerTest.java rename to jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java index 4a8945673ef..1faca02d882 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslBytesServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.server.ssl; +package org.eclipse.jetty.client.ssl; import java.io.BufferedReader; import java.io.EOFException; @@ -84,7 +84,7 @@ public class SslBytesServerTest extends SslBytesTest private final int idleTimeout = 2000; private ExecutorService threadPool; private Server server; - private int serverPort; + private SslContextFactory sslContextFactory; private SSLContext sslContext; private SimpleProxy proxy; @@ -94,11 +94,10 @@ public class SslBytesServerTest extends SslBytesTest threadPool = Executors.newCachedThreadPool(); server = new Server(); - File keyStore = MavenTestingUtils.getTestResourceFile("keystore"); - SslContextFactory sslContextFactory = new SslContextFactory(); + File keyStore = MavenTestingUtils.getTestResourceFile("keystore.jks"); + sslContextFactory = new SslContextFactory(); sslContextFactory.setKeyStorePath(keyStore.getAbsolutePath()); sslContextFactory.setKeyStorePassword("storepwd"); - sslContextFactory.setKeyManagerPassword("keypwd"); HttpConnectionFactory httpFactory = new HttpConnectionFactory() { @@ -204,7 +203,7 @@ public class SslBytesServerTest extends SslBytesTest } }); server.start(); - serverPort = connector.getLocalPort(); + int serverPort = connector.getLocalPort(); sslContext = sslContextFactory.getSslContext(); @@ -1277,6 +1276,98 @@ public class SslBytesServerTest extends SslBytesTest closeClient(client); } + @Test + public void testRequestWithContentWithRenegotiationInMiddleOfContentWhenRenegotiationIsForbidden() throws Exception + { + assumeJavaVersionSupportsTLSRenegotiations(); + + sslContextFactory.setRenegotiationAllowed(false); + + final SSLSocket client = newClient(); + final OutputStream clientOutput = client.getOutputStream(); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + client.startHandshake(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + byte[] data1 = new byte[1024]; + Arrays.fill(data1, (byte)'X'); + String content1 = new String(data1, "UTF-8"); + byte[] data2 = new byte[1024]; + Arrays.fill(data2, (byte)'Y'); + final String content2 = new String(data2, "UTF-8"); + + // Write only part of the body + automaticProxyFlow = proxy.startAutomaticFlow(); + clientOutput.write(("" + + "POST / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: " + (content1.length() + content2.length()) + "\r\n" + + "\r\n" + + content1).getBytes("UTF-8")); + clientOutput.flush(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + // Renegotiate + threadPool.submit(new Callable() + { + @Override + public Object call() throws Exception + { + client.startHandshake(); + return null; + } + }); + + // Renegotiation Handshake + TLSRecord record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + proxy.flushToServer(record); + + // Renegotiation now allowed, server has closed + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); + proxy.flushToClient(record); + + record = proxy.readFromServer(); + Assert.assertNull(record); + + // Write the rest of the request + threadPool.submit(new Callable() + { + @Override + public Object call() throws Exception + { + clientOutput.write(content2.getBytes("UTF-8")); + clientOutput.flush(); + return null; + } + }); + + // Trying to write more application data results in an exception since the server closed + record = proxy.readFromClient(); + proxy.flushToServer(record); + try + { + record = proxy.readFromClient(); + Assert.assertNotNull(record); + proxy.flushToServer(record); + Assert.fail(); + } + catch (IOException expected) + { + } + + // Check that we did not spin + TimeUnit.MILLISECONDS.sleep(500); + Assert.assertThat(sslFills.get(), Matchers.lessThan(50)); + Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); + Assert.assertThat(httpParses.get(), Matchers.lessThan(50)); + + client.close(); + } + @Test public void testRequestWithBigContentWithRenegotiationInMiddleOfContent() throws Exception { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslBytesTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesTest.java similarity index 98% rename from jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslBytesTest.java rename to jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesTest.java index a3b4952fec6..c7fe7447ddc 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslBytesTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesTest.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.server.ssl; +package org.eclipse.jetty.client.ssl; import java.io.EOFException; import java.io.IOException; @@ -115,8 +115,8 @@ public abstract class SslBytesTest public void start() throws Exception { -// serverSocket = new ServerSocket(5871); - serverSocket = new ServerSocket(0); + serverSocket = new ServerSocket(47009); +// serverSocket = new ServerSocket(0); Thread acceptor = new Thread(this); acceptor.start(); server = new Socket(serverHost, serverPort); diff --git a/jetty-client/src/test/resources/keystore b/jetty-client/src/test/resources/keystore deleted file mode 100644 index 3a15d1b603d..00000000000 Binary files a/jetty-client/src/test/resources/keystore and /dev/null differ 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 9d13a1a8d0b..301970d681c 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 @@ -99,6 +99,7 @@ public class SslConnection extends AbstractConnection _decryptedEndPoint.getWriteFlusher().completeWrite(); } }; + private boolean _renegotiationAllowed; public SslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine sslEngine) { @@ -137,6 +138,16 @@ public class SslConnection extends AbstractConnection return _decryptedEndPoint; } + public boolean isRenegotiationAllowed() + { + return _renegotiationAllowed; + } + + public void setRenegotiationAllowed(boolean renegotiationAllowed) + { + this._renegotiationAllowed = renegotiationAllowed; + } + @Override public void onOpen() { @@ -242,6 +253,7 @@ public class SslConnection extends AbstractConnection private boolean _fillRequiresFlushToProgress; private boolean _flushRequiresFillToProgress; private boolean _cannotAcceptMoreAppDataToFlush; + private boolean _handshaken; private boolean _underFlown; private final Callback _writeCallback = new Callback() @@ -493,15 +505,19 @@ public class SslConnection extends AbstractConnection if (DEBUG) LOG.debug("{} unwrap {}", SslConnection.this, unwrapResult); + Status unwrapResultStatus = unwrapResult.getStatus(); + HandshakeStatus unwrapHandshakeStatus = unwrapResult.getHandshakeStatus(); + HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus(); + // and deal with the results - switch (unwrapResult.getStatus()) + switch (unwrapResultStatus) { case BUFFER_OVERFLOW: throw new IllegalStateException(); case CLOSED: // Dang! we have to care about the handshake state specially for close - switch (_sslEngine.getHandshakeStatus()) + switch (handshakeStatus) { case NOT_HANDSHAKING: // We were not handshaking, so just tell the app we are closed @@ -521,10 +537,28 @@ public class SslConnection extends AbstractConnection throw new IllegalStateException(); default: - if (unwrapResult.getStatus()==Status.BUFFER_UNDERFLOW) - _underFlown=true; + if (unwrapHandshakeStatus == HandshakeStatus.FINISHED && !_handshaken) + { + _handshaken = true; + if (DEBUG) + LOG.debug("{} handshake completed client-side", SslConnection.this); + } - // if we produced bytes, we don't care about the handshake state for now and it can be dealt with on another call to fill or flush + // Check whether renegotiation is allowed + if (_handshaken && handshakeStatus != HandshakeStatus.NOT_HANDSHAKING && !isRenegotiationAllowed()) + { + if (DEBUG) + LOG.debug("{} renegotiation denied", SslConnection.this); + closeInbound(); + return -1; + } + + if (unwrapResultStatus == Status.BUFFER_UNDERFLOW) + _underFlown = true; + + // If bytes were produced, don't bother with the handshake status; + // pass the decrypted data to the application, which will perform + // another call to fill() or flush(). if (unwrapResult.bytesProduced() > 0) { if (app_in == buffer) @@ -533,7 +567,7 @@ public class SslConnection extends AbstractConnection } // Dang! we have to care about the handshake state - switch (_sslEngine.getHandshakeStatus()) + switch (handshakeStatus) { case NOT_HANDSHAKING: // we just didn't read anything. @@ -553,7 +587,7 @@ public class SslConnection extends AbstractConnection // we need to send some handshake data // if we are called from flush - if (buffer==__FLUSH_CALLED_FILL) + if (buffer == __FLUSH_CALLED_FILL) return 0; // let it do the wrapping _fillRequiresFlushToProgress = true; @@ -661,7 +695,6 @@ public class SslConnection extends AbstractConnection while (true) { - // do the funky SSL thang! // We call sslEngine.wrap to try to take bytes from appOut buffers and encrypt them into the _netOut buffer BufferUtil.compact(_encryptedOutput); int pos = BufferUtil.flipToFill(_encryptedOutput); @@ -672,18 +705,20 @@ public class SslConnection extends AbstractConnection if (wrapResult.bytesConsumed()>0) consumed+=wrapResult.bytesConsumed(); - boolean all_consumed=true; + boolean allConsumed=true; // clear empty buffers to prevent position creeping up the buffer for (ByteBuffer b : appOuts) { if (BufferUtil.isEmpty(b)) BufferUtil.clear(b); else - all_consumed=false; + allConsumed=false; } + Status wrapResultStatus = wrapResult.getStatus(); + // and deal with the results returned from the sslEngineWrap - switch (wrapResult.getStatus()) + switch (wrapResultStatus) { case CLOSED: // The SSL engine has close, but there may be close handshake that needs to be written @@ -692,32 +727,52 @@ public class SslConnection extends AbstractConnection _cannotAcceptMoreAppDataToFlush = true; getEndPoint().flush(_encryptedOutput); // If we failed to flush the close handshake then we will just pretend that - // the write has progressed normally and let a subsequent call to flush (or WriteFlusher#onIncompleteFlushed) - // to finish writing the close handshake. The caller will find out about the close on a subsequent flush or fill. + // the write has progressed normally and let a subsequent call to flush + // (or WriteFlusher#onIncompleteFlushed) to finish writing the close handshake. + // The caller will find out about the close on a subsequent flush or fill. if (BufferUtil.hasContent(_encryptedOutput)) return false; } // otherwise we have written, and the caller will close the underlying connection - return all_consumed; + return allConsumed; case BUFFER_UNDERFLOW: throw new IllegalStateException(); default: if (DEBUG) - LOG.debug("{} {} {}", this, wrapResult.getStatus(), BufferUtil.toDetailString(_encryptedOutput)); + LOG.debug("{} {} {}", this, wrapResultStatus, BufferUtil.toDetailString(_encryptedOutput)); + + if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED && !_handshaken) + { + _handshaken = true; + if (DEBUG) + LOG.debug("{} handshake completed server-side", SslConnection.this); + } + + HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus(); + + // Check whether renegotiation is allowed + if (_handshaken && handshakeStatus != HandshakeStatus.NOT_HANDSHAKING && !isRenegotiationAllowed()) + { + if (DEBUG) + LOG.debug("{} renegotiation denied", SslConnection.this); + shutdownOutput(); + BufferUtil.clear(_encryptedOutput); + return allConsumed; + } // if we have net bytes, let's try to flush them if (BufferUtil.hasContent(_encryptedOutput)) getEndPoint().flush(_encryptedOutput); // But we also might have more to do for the handshaking state. - switch (_sslEngine.getHandshakeStatus()) + switch (handshakeStatus) { case NOT_HANDSHAKING: // Return with the number of bytes consumed (which may be 0) - return all_consumed&&BufferUtil.isEmpty(_encryptedOutput); + return allConsumed && BufferUtil.isEmpty(_encryptedOutput); case NEED_TASK: // run the task and continue @@ -737,14 +792,13 @@ public class SslConnection extends AbstractConnection _flushRequiresFillToProgress = true; fill(__FLUSH_CALLED_FILL); // Check if after the fill() we need to wrap again - if (_sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) + if (handshakeStatus == HandshakeStatus.NEED_WRAP) continue; } - return all_consumed&&BufferUtil.isEmpty(_encryptedOutput); + return allConsumed&&BufferUtil.isEmpty(_encryptedOutput); case FINISHED: throw new IllegalStateException(); - } } } diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointSslTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointSslTest.java index 6f956a18e4b..77aea8b0081 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointSslTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointSslTest.java @@ -74,10 +74,9 @@ public class SelectChannelEndPointSslTest extends SelectChannelEndPointTest SSLEngine engine = __sslCtxFactory.newSSLEngine(); engine.setUseClientMode(false); SslConnection sslConnection = new SslConnection(__byteBufferPool, _threadPool, endpoint, engine); - + sslConnection.setRenegotiationAllowed(__sslCtxFactory.isRenegotiationAllowed()); Connection appConnection = super.newConnection(channel,sslConnection.getDecryptedEndPoint()); sslConnection.getDecryptedEndPoint().setConnection(appConnection); - return sslConnection; } diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java index 506d50bd5bc..c02bd000cae 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java @@ -30,7 +30,6 @@ import java.nio.channels.SocketChannel; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; - import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSocket; @@ -80,10 +79,9 @@ public class SslConnectionTest SSLEngine engine = __sslCtxFactory.newSSLEngine(); engine.setUseClientMode(false); SslConnection sslConnection = new SslConnection(__byteBufferPool, getExecutor(), endpoint, engine); - + sslConnection.setRenegotiationAllowed(__sslCtxFactory.isRenegotiationAllowed()); Connection appConnection = new TestConnection(sslConnection.getDecryptedEndPoint()); sslConnection.getDecryptedEndPoint().setConnection(appConnection); - return sslConnection; } 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 da83f1f96da..d3e9fd6c78a 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 @@ -78,6 +78,7 @@ public class SslConnectionFactory extends AbstractConnectionFactory engine.setUseClientMode(false); SslConnection sslConnection = newSslConnection(connector, endPoint, engine); + sslConnection.setRenegotiationAllowed(_sslContextFactory.isRenegotiationAllowed()); configure(sslConnection, connector, endPoint); ConnectionFactory next = connector.getConnectionFactory(_nextProtocol); diff --git a/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/SPDYClient.java b/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/SPDYClient.java index 2a23c5f2c1f..2456a2228fc 100644 --- a/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/SPDYClient.java +++ b/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/SPDYClient.java @@ -307,6 +307,7 @@ public class SPDYClient { final SSLEngine engine = client.newSSLEngine(sslContextFactory, channel); SslConnection sslConnection = new SslConnection(bufferPool, getExecutor(), endPoint, engine); + sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed()); DecryptedEndPoint sslEndPoint = sslConnection.getDecryptedEndPoint(); NextProtoNegoClientConnection connection = new NextProtoNegoClientConnection(channel, sslEndPoint, attachment, getExecutor(), client); sslEndPoint.setConnection(connection); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java index 857d199a818..f0ff187bb0e 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -41,6 +41,9 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.net.ssl.CertPathTrustManagerParameters; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; @@ -196,8 +199,12 @@ public class SslContextFactory extends AbstractLifeCycle /** EndpointIdentificationAlgorithm - when set to "HTTPS" hostname verification will be enabled */ private String _endpointIdentificationAlgorithm = null; + /** Whether to blindly trust certificates */ private boolean _trustAll; + /** Whether TLS renegotiation is allowed */ + private boolean _renegotiationAllowed = true; + /** * Construct an instance of SslContextFactory * Default constructor for use in XmlConfiguration files @@ -369,6 +376,7 @@ public class SslContextFactory extends AbstractLifeCycle } /** + * You can either use the exact cipher suite name or a a regular expression. * @param cipherSuites * The array of cipher suite names to exclude from * {@link SSLEngine#setEnabledCipherSuites(String[])} @@ -399,6 +407,7 @@ public class SslContextFactory extends AbstractLifeCycle } /** + * You can either use the exact cipher suite name or a a regular expression. * @param cipherSuites * The array of cipher suite names to include in * {@link SSLEngine#setEnabledCipherSuites(String[])} @@ -759,6 +768,22 @@ public class SslContextFactory extends AbstractLifeCycle _trustManagerFactoryAlgorithm = algorithm; } + /** + * @return whether TLS renegotiation is allowed (true by default) + */ + public boolean isRenegotiationAllowed() + { + return _renegotiationAllowed; + } + + /** + * @param renegotiationAllowed whether TLS renegotiation is allowed + */ + public void setRenegotiationAllowed(boolean renegotiationAllowed) + { + _renegotiationAllowed = renegotiationAllowed; + } + /** * @return Path to file that contains Certificate Revocation List */ @@ -1035,25 +1060,47 @@ public class SslContextFactory extends AbstractLifeCycle */ public String[] selectCipherSuites(String[] enabledCipherSuites, String[] supportedCipherSuites) { - Set selected_ciphers = new LinkedHashSet<>(); + Set selected_ciphers = new CopyOnWriteArraySet<>(); // Set the starting ciphers - either from the included or enabled list if (_includeCipherSuites!=null) - { - // Use only the supported included ciphers - for (String cipherSuite : _includeCipherSuites) - if(Arrays.asList(supportedCipherSuites).contains(cipherSuite)) - selected_ciphers.add(cipherSuite); - } + processIncludeCipherSuites(supportedCipherSuites, selected_ciphers); else selected_ciphers.addAll(Arrays.asList(enabledCipherSuites)); + removeExcludedCipherSuites(selected_ciphers); - // Remove any excluded ciphers - selected_ciphers.removeAll(_excludeCipherSuites); return selected_ciphers.toArray(new String[selected_ciphers.size()]); } + private void processIncludeCipherSuites(String[] supportedCipherSuites, Set selected_ciphers) + { + for (String cipherSuite : _includeCipherSuites) + { + Pattern p = Pattern.compile(cipherSuite); + for (String supportedCipherSuite : supportedCipherSuites) + { + Matcher m = p.matcher(supportedCipherSuite); + if (m.matches()) + selected_ciphers.add(supportedCipherSuite); + } + } + } + + private void removeExcludedCipherSuites(Set selected_ciphers) + { + for (String excludeCipherSuite : _excludeCipherSuites) + { + Pattern excludeCipherPattern = Pattern.compile(excludeCipherSuite); + for (String selectedCipherSuite : selected_ciphers) + { + Matcher m = excludeCipherPattern.matcher(selectedCipherSuite); + if (m.matches()) + selected_ciphers.remove(selectedCipherSuite); + } + } + } + /** * Check if the lifecycle has been started and throw runtime exception */ diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java index 6122dfd0244..980640c1081 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java @@ -18,15 +18,12 @@ package org.eclipse.jetty.util.ssl; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; +import javax.net.ssl.SSLEngine; + import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.StdErrLog; @@ -35,6 +32,12 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + public class SslContextFactoryTest { @@ -189,6 +192,30 @@ public class SslContextFactoryTest } } + @Test + public void testSetExcludeCipherSuitesRegex() throws Exception + { + cf.setExcludeCipherSuites(".*RC4.*"); + cf.start(); + SSLEngine sslEngine = cf.newSSLEngine(); + String[] enabledCipherSuites = sslEngine.getEnabledCipherSuites(); + assertThat("At least 1 cipherSuite is enabled", enabledCipherSuites.length, greaterThan(0)); + for (String enabledCipherSuite : enabledCipherSuites) + assertThat("CipherSuite does not contain RC4", enabledCipherSuite.contains("RC4"), is(false)); + } + + @Test + public void testSetIncludeCipherSuitesRegex() throws Exception + { + cf.setIncludeCipherSuites(".*RC4.*"); + cf.start(); + SSLEngine sslEngine = cf.newSSLEngine(); + String[] enabledCipherSuites = sslEngine.getEnabledCipherSuites(); + assertThat("At least 1 cipherSuite is enabled", enabledCipherSuites.length, greaterThan(0)); + for (String enabledCipherSuite : enabledCipherSuites) + assertThat("CipherSuite contains RC4", enabledCipherSuite.contains("RC4"), is(true)); + } + @Test public void testSetIncludeCipherSuitesPreservesOrder() { diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientSelectorManager.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientSelectorManager.java index 21c76954ff4..e3f1669a54e 100644 --- a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientSelectorManager.java +++ b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientSelectorManager.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.util.concurrent.Executor; - import javax.net.ssl.SSLEngine; import org.eclipse.jetty.io.ByteBufferPool; @@ -82,6 +81,7 @@ public class WebSocketClientSelectorManager extends SelectorManager { SSLEngine engine = newSSLEngine(sslContextFactory,channel); SslConnection sslConnection = new SslConnection(bufferPool,getExecutor(),endPoint,engine); + sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed()); EndPoint sslEndPoint = sslConnection.getDecryptedEndPoint(); Connection connection = newUpgradeConnection(channel,sslEndPoint,connectPromise); diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketCloseTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketCloseTest.java index cb2770d24d6..b7e80d3f258 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketCloseTest.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketCloseTest.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.server; -import static org.hamcrest.Matchers.*; - import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -45,11 +43,15 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; +import static org.hamcrest.Matchers.is; + /** * Tests various close scenarios */ +@Ignore public class WebSocketCloseTest { @SuppressWarnings("serial")