From 97c711613f751cafabcb9a6cf13913f0e1495143 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 17 Nov 2011 14:08:46 +1100 Subject: [PATCH 01/13] added siege like load tester --- .../eclipse/jetty/embedded/LikeJettyXml.java | 13 +- .../java/org/eclipse/jetty/client/Curl.java | 32 ++- .../java/org/eclipse/jetty/client/Siege.java | 194 ++++++++++++++++++ 3 files changed, 231 insertions(+), 8 deletions(-) create mode 100644 jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java diff --git a/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java b/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java index 0cbb2b61afc..b9371e698c3 100644 --- a/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java +++ b/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java @@ -31,6 +31,7 @@ import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; +import org.eclipse.jetty.server.ssl.SslSocketConnector; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; @@ -63,7 +64,7 @@ public class LikeJettyXml connector.setPort(8080); connector.setMaxIdleTime(30000); connector.setConfidentialPort(8443); - connector.setStatsOn(true); + connector.setStatsOn(false); server.setConnectors(new Connector[] { connector }); @@ -86,11 +87,17 @@ public class LikeJettyXml "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA" }); - ssl_connector.setStatsOn(true); + ssl_connector.setStatsOn(false); server.addConnector(ssl_connector); ssl_connector.open(); - + SslSocketConnector ssl2_connector = new SslSocketConnector(cf); + ssl2_connector.setPort(8444); + ssl2_connector.setStatsOn(false); + server.addConnector(ssl2_connector); + ssl2_connector.open(); + + /* Ajp13SocketConnector ajp = new Ajp13SocketConnector(); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/Curl.java b/jetty-client/src/test/java/org/eclipse/jetty/client/Curl.java index 1e7c1da3913..be0368b75c3 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/Curl.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/Curl.java @@ -1,6 +1,8 @@ package org.eclipse.jetty.client; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; @@ -33,11 +35,26 @@ public class Curl client.start(); boolean async=true; boolean dump= false; + boolean verbose= false; - final CountDownLatch latch = new CountDownLatch(args.length); + + int urls=0; + for (String arg : args) + { + if (!arg.startsWith("-")) + urls++; + } + + final CountDownLatch latch = new CountDownLatch(urls); for (String arg : args) { + if ("--verbose".equals(arg)) + { + verbose=true; + continue; + } + if ("--sync".equals(arg)) { async=false; @@ -63,6 +80,7 @@ public class Curl } final boolean d = dump; + final boolean v = verbose; HttpExchange ex = new HttpExchange() { AtomicBoolean counted=new AtomicBoolean(false); @@ -105,7 +123,8 @@ public class Curl super.onResponseContent(content); if (d) System.out.print(content.toString()); - System.err.println("got "+content.length()); + if (v) + System.err.println("got "+content.length()); } /* ------------------------------------------------------------ */ @@ -116,7 +135,8 @@ public class Curl protected void onResponseHeader(Buffer name, Buffer value) throws IOException { super.onResponseHeader(name,value); - System.err.println(name+": "+value); + if (v) + System.err.println(name+": "+value); } /* ------------------------------------------------------------ */ @@ -127,7 +147,8 @@ public class Curl protected void onResponseHeaderComplete() throws IOException { super.onResponseHeaderComplete(); - System.err.println(); + if (v) + System.err.println(); } /* ------------------------------------------------------------ */ @@ -138,7 +159,8 @@ public class Curl protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException { super.onResponseStatus(version,status,reason); - System.err.println(version+" "+status+" "+reason); + if (v) + System.err.println(version+" "+status+" "+reason); } }; diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java b/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java new file mode 100644 index 00000000000..d8fa2f9585c --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java @@ -0,0 +1,194 @@ +package org.eclipse.jetty.client; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.io.Buffer; + + +/* ------------------------------------------------------------ */ +/** + */ +public class Siege +{ + private static final class ConcurrentExchange extends HttpExchange + { + private final long _start=System.currentTimeMillis(); + private final HttpClient _client; + private final CountDownLatch _latch; + volatile int _status; + volatile int _count; + final List _uris; + final int _repeats; + int _u; + int _r; + + AtomicBoolean counted=new AtomicBoolean(false); + + public ConcurrentExchange(HttpClient client,CountDownLatch latch, List uris, int repeats) + { + _client = client; + _latch = latch; + _uris = uris; + _repeats = repeats; + } + + @Override + protected void onConnectionFailed(Throwable ex) + { + if (!counted.getAndSet(true)) + _latch.countDown(); + super.onConnectionFailed(ex); + } + + @Override + protected void onException(Throwable ex) + { + if (!counted.getAndSet(true)) + _latch.countDown(); + super.onException(ex); + } + + @Override + protected void onExpire() + { + if (!counted.getAndSet(true)) + _latch.countDown(); + super.onExpire(); + } + + @Override + protected void onResponseComplete() throws IOException + { + if (_status==200) + _count++; + if (!next() && !counted.getAndSet(true)) + { + _latch.countDown(); + long duration=System.currentTimeMillis()-_start; + System.err.printf("Got %d/%d in %dms %d%n",_count,_uris.size()*_repeats,duration,_latch.getCount()); + } + } + + /* ------------------------------------------------------------ */ + /** + * @see org.eclipse.jetty.client.HttpExchange#onResponseHeader(org.eclipse.jetty.io.Buffer, org.eclipse.jetty.io.Buffer) + */ + @Override + protected void onResponseHeader(Buffer name, Buffer value) throws IOException + { + super.onResponseHeader(name,value); + if ("Set-Cookie".equalsIgnoreCase(name.toString())) + { + String v=value.toString(); + int c = v.indexOf(';'); + if (c>=0) + v=v.substring(0,c); + addRequestHeader("Cookie",v); + } + } + + /* ------------------------------------------------------------ */ + /** + * @see org.eclipse.jetty.client.HttpExchange#onResponseHeaderComplete() + */ + @Override + protected void onResponseHeaderComplete() throws IOException + { + super.onResponseHeaderComplete(); + } + + /* ------------------------------------------------------------ */ + /** + * @see org.eclipse.jetty.client.HttpExchange#onResponseStatus(org.eclipse.jetty.io.Buffer, int, org.eclipse.jetty.io.Buffer) + */ + @Override + protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException + { + _status=status; + super.onResponseStatus(version,status,reason); + } + + public boolean next() + { + if (_u>=_uris.size()) + { + _u=0; + _r++; + if (_r>=_repeats) + return false; + } + + String uri=_uris.get(_u++); + + reset(); + setMethod(HttpMethods.GET); + setURL(uri); + + try + { + _client.send(this); + } + catch(IOException e) + { + e.printStackTrace(); + return false; + } + return true; + } + } + + public static void main(String[] args) + throws Exception + { + if (args.length==0) + args=new String[] + { "-c", "2", "-r", "2", "http://localhost:8080/dump", "http://localhost:8080/d.txt"}; + + int concurrent=1; + int repeats=1; + final List uris = new ArrayList(); + + for (int i=0; i Date: Thu, 17 Nov 2011 14:45:41 +1100 Subject: [PATCH 02/13] improved siege like load tester --- .../test/java/org/eclipse/jetty/client/Siege.java | 12 +++++++++++- .../eclipse/jetty/server/BlockingHttpConnection.java | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java b/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java index d8fa2f9585c..62d4347b346 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java @@ -22,6 +22,7 @@ public class Siege private final CountDownLatch _latch; volatile int _status; volatile int _count; + volatile long _bytes; final List _uris; final int _repeats; int _u; @@ -70,9 +71,18 @@ public class Siege { _latch.countDown(); long duration=System.currentTimeMillis()-_start; - System.err.printf("Got %d/%d in %dms %d%n",_count,_uris.size()*_repeats,duration,_latch.getCount()); + System.err.printf("Got %d/%d with %dB in %dms %d%n",_count,_uris.size()*_repeats,_bytes,duration,_latch.getCount()); } } + + + /* ------------------------------------------------------------ */ + @Override + protected void onResponseContent(Buffer content) throws IOException + { + _bytes+=content.length(); + super.onResponseContent(content); + } /* ------------------------------------------------------------ */ /** diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/BlockingHttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/BlockingHttpConnection.java index eb8f4a6dc63..20797d9a62c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/BlockingHttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/BlockingHttpConnection.java @@ -102,6 +102,7 @@ public class BlockingHttpConnection extends AbstractHttpConnection // Reset the parser/generator progress=true; reset(); + _endp.flush(); // look for a switched connection instance? if (_response.getStatus()==HttpStatus.SWITCHING_PROTOCOLS_101) @@ -114,7 +115,7 @@ public class BlockingHttpConnection extends AbstractHttpConnection // TODO Is this required? if (!_generator.isPersistent() && !_endp.isOutputShutdown()) { - System.err.println("Safety net oshut!!!"); + LOG.warn("Safety net oshut!!! Please open a bugzilla"); _endp.shutdownOutput(); } } From 584bbd4bceb3303a3d8a96308470f702b20c04b1 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 17 Nov 2011 16:31:22 +1100 Subject: [PATCH 03/13] improved siege --- .../main/java/org/eclipse/jetty/embedded/LikeJettyXml.java | 2 +- .../src/test/java/org/eclipse/jetty/client/Siege.java | 7 +++++++ .../main/java/org/eclipse/jetty/io/nio/SslConnection.java | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java b/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java index b9371e698c3..899c682bc08 100644 --- a/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java +++ b/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java @@ -56,7 +56,7 @@ public class LikeJettyXml // Setup Threadpool QueuedThreadPool threadPool = new QueuedThreadPool(); - threadPool.setMaxThreads(100); + threadPool.setMaxThreads(500); server.setThreadPool(threadPool); // Setup Connectors diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java b/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java index 62d4347b346..bc476c27fc7 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java @@ -8,6 +8,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jetty.http.HttpMethods; import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.util.thread.QueuedThreadPool; /* ------------------------------------------------------------ */ @@ -181,8 +182,12 @@ public class Siege uris.add(arg); } + QueuedThreadPool pool = new QueuedThreadPool(); + pool.setMaxThreads(500); + pool.setDaemon(true); HttpClient client = new HttpClient(); + client.setThreadPool(pool); client.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); client.setIdleTimeout(30000); client.setConnectTimeout(30000); @@ -200,5 +205,7 @@ public class Siege } latch.await(); + client.stop(); + pool.stop(); } } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java index 902c9026587..fa2b72e2c15 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java @@ -793,7 +793,7 @@ public class SslConnection extends AbstractConnection implements AsyncConnection o=_outbound; u=_unwrapBuf; } - return "SSL:"+_endp+" "+_engine.getHandshakeStatus()+" i/u/o="+(i==null?0:i.length())+"/"+(u==null?0:u.length())+"/"+(o==null?0:o.length()+(_oshut?" oshut":"")); + return "SSL:"+_endp+" "+_engine.getHandshakeStatus()+" i/u/o="+(i==null?0:i.length())+"/"+(u==null?0:u.length())+"/"+(o==null?0:o.length()+(_ishut?" ishut":"")+(_oshut?" oshut":"")); } } From d1592cf5b5f354468e35e9360045d9a3fa1429c6 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Thu, 17 Nov 2011 12:40:32 +0100 Subject: [PATCH 04/13] Added more SSL tests to detect spinning conditions. --- .../jetty/client/SslBytesServerTest.java | 1070 +++++++++++++++++ .../eclipse/jetty/io/nio/SslConnection.java | 13 +- .../jetty/server/AbstractHttpConnection.java | 33 +- .../server/nio/SelectChannelConnector.java | 28 +- .../server/ssl/SslSelectChannelConnector.java | 36 +- 5 files changed, 1112 insertions(+), 68 deletions(-) create mode 100644 jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java new file mode 100644 index 00000000000..fb4a48af315 --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java @@ -0,0 +1,1070 @@ +package org.eclipse.jetty.client; + +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.nio.channels.SocketChannel; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSocket; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpParser; +import org.eclipse.jetty.io.AsyncEndPoint; +import org.eclipse.jetty.io.Buffers; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.nio.AsyncConnection; +import org.eclipse.jetty.io.nio.SslConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; +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.Ignore; +import org.junit.Test; + +import static org.hamcrest.Matchers.lessThan; + +public class SslBytesServerTest +{ + private final AtomicInteger sslHandles = new AtomicInteger(); + private final AtomicInteger httpParses = new AtomicInteger(); + private ExecutorService threadPool; + private Server server; + private SSLContext sslContext; + private SimpleProxy proxy; + + @Before + public void startServer() throws Exception + { + threadPool = Executors.newFixedThreadPool(2); + server = new Server(); + + SslSelectChannelConnector connector = new SslSelectChannelConnector() + { + @Override + protected SslConnection newSslConnection(AsyncEndPoint endPoint, SSLEngine engine) + { + return new SslConnection(engine, endPoint) + { + @Override + public Connection handle() throws IOException + { + sslHandles.incrementAndGet(); + return super.handle(); + } + }; + } + + @Override + protected AsyncConnection newPlainConnection(SocketChannel channel, AsyncEndPoint endPoint) + { + return new org.eclipse.jetty.server.AsyncHttpConnection(this, endPoint, getServer()) + { + @Override + protected HttpParser newHttpParser(Buffers requestBuffers, EndPoint endPoint, HttpParser.EventHandler requestHandler) + { + return new HttpParser(requestBuffers, endPoint, requestHandler) + { + @Override + public int parseNext() throws IOException + { + httpParses.incrementAndGet(); + return super.parseNext(); + } + }; + } + }; + } + }; + + connector.setPort(5870); // TODO: make this random + + File keyStore = MavenTestingUtils.getTestResourceFile("keystore"); + SslContextFactory cf = connector.getSslContextFactory(); + cf.setKeyStorePath(keyStore.getAbsolutePath()); + cf.setKeyStorePassword("storepwd"); + cf.setKeyManagerPassword("keypwd"); + server.addConnector(connector); + server.setHandler(new AbstractHandler() + { + public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException + { + request.setHandled(true); + String contentLength = request.getHeader("Content-Length"); + if (contentLength != null) + { + int length = Integer.parseInt(contentLength); + ServletInputStream input = request.getInputStream(); + for (int i = 0; i < length; ++i) + input.read(); + } + } + }); + server.start(); + + sslContext = cf.getSslContext(); + + proxy = new SimpleProxy(threadPool, "localhost", connector.getLocalPort()); + proxy.start(); + System.err.println(":" + proxy.getPort() + " <==> :" + connector.getLocalPort()); + } + + @After + public void stopServer() throws Exception + { + if (proxy != null) + proxy.stop(); + if (server != null) + server.stop(); + if (threadPool != null) + threadPool.shutdownNow(); + } + + @Test + public void testHandshake() throws Exception + { + final SSLSocket client = newClient(); + + Future handshake = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + client.startHandshake(); + return null; + } + }); + + // Client Hello + TLSRecord record = proxy.readFromClient(); + Assert.assertNotNull(record); + proxy.flushToServer(record); + + // Server Hello + Certificate + Server Done + record = proxy.readFromServer(); + Assert.assertNotNull(record); + proxy.flushToClient(record); + + // Client Key Exchange + record = proxy.readFromClient(); + Assert.assertNotNull(record); + proxy.flushToServer(record); + + // Change Cipher Spec + record = proxy.readFromClient(); + Assert.assertNotNull(record); + proxy.flushToServer(record); + + // Client Done + record = proxy.readFromClient(); + Assert.assertNotNull(record); + proxy.flushToServer(record); + + // Change Cipher Spec + record = proxy.readFromServer(); + Assert.assertNotNull(record); + proxy.flushToClient(record); + + // Server Done + record = proxy.readFromServer(); + Assert.assertNotNull(record); + proxy.flushToClient(record); + + Assert.assertNull(handshake.get(5, TimeUnit.SECONDS)); + + closeClient(client); + } + + @Test + public void testRequestResponse() throws Exception + { + final SSLSocket client = newClient(); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + client.startHandshake(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + Future request = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + OutputStream clientOutput = client.getOutputStream(); + clientOutput.write(("" + + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n").getBytes("UTF-8")); + clientOutput.flush(); + return null; + } + }); + + // Application data + TLSRecord record = proxy.readFromClient(); + proxy.flushToServer(record); + Assert.assertNull(request.get(5, TimeUnit.SECONDS)); + + // Application data + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + proxy.flushToClient(record); + + BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); + String line = reader.readLine(); + Assert.assertNotNull(line); + Assert.assertTrue(line.contains(" 200 ")); + while ((line = reader.readLine()) != null) + { + if (line.trim().length() == 0) + break; + } + + closeClient(client); + } + + @Test + public void testHandshakeAndRequestOneByteAtATime() throws Exception + { + final SSLSocket client = newClient(); + + Future handshake = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + client.startHandshake(); + return null; + } + }); + + // Client Hello + TLSRecord record = proxy.readFromClient(); + for (byte b : record.getBytes()) + { + proxy.flushToServer(b); + TimeUnit.MILLISECONDS.sleep(50); + } + + // Server Hello + Certificate + Server Done + record = proxy.readFromServer(); + proxy.flushToClient(record); + + // Client Key Exchange + record = proxy.readFromClient(); + for (byte b : record.getBytes()) + { + proxy.flushToServer(b); + TimeUnit.MILLISECONDS.sleep(50); + } + + // Change Cipher Spec + record = proxy.readFromClient(); + for (byte b : record.getBytes()) + { + proxy.flushToServer(b); + TimeUnit.MILLISECONDS.sleep(50); + } + + // Client Done + record = proxy.readFromClient(); + for (byte b : record.getBytes()) + { + proxy.flushToServer(b); + TimeUnit.MILLISECONDS.sleep(50); + } + + // Change Cipher Spec + record = proxy.readFromServer(); + proxy.flushToClient(record); + + // Server Done + record = proxy.readFromServer(); + proxy.flushToClient(record); + + Assert.assertNull(handshake.get(5, TimeUnit.SECONDS)); + + Future request = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + OutputStream clientOutput = client.getOutputStream(); + clientOutput.write(("" + + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n").getBytes("UTF-8")); + clientOutput.flush(); + return null; + } + }); + + // Application data + record = proxy.readFromClient(); + for (byte b : record.getBytes()) + { + proxy.flushToServer(b); + TimeUnit.MILLISECONDS.sleep(50); + } + Assert.assertNull(request.get(5, TimeUnit.SECONDS)); + + // Application data + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + proxy.flushToClient(record); + + BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); + String line = reader.readLine(); + Assert.assertNotNull(line); + Assert.assertTrue(line.contains(" 200 ")); + while ((line = reader.readLine()) != null) + { + if (line.trim().length() == 0) + break; + } + + client.close(); + + // Close Alert + record = proxy.readFromClient(); + for (byte b : record.getBytes()) + { + proxy.flushToServer(b); + TimeUnit.MILLISECONDS.sleep(50); + } + // Socket close + record = proxy.readFromClient(); + Assert.assertNull(record); + proxy.flushToServer(record); + + // Close Alert + record = proxy.readFromServer(); + proxy.flushToClient(record); + // Socket close + record = proxy.readFromServer(); + Assert.assertNull(record); + proxy.flushToClient(record); + } + + /** + * TODO + * Currently this test does not pass. + * The problem is a mix of Java not being able to perform SSL half closes + * (but SSL supporting it), and the current implementation in Jetty. + * See the test below, that passes and whose only difference is that we + * delay the output shutdown from the client. + * + * @throws Exception if the test fails + */ + @Ignore + @Test + public void testRequestWithCloseAlertAndShutdown() throws Exception + { + final SSLSocket client = newClient(); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + client.startHandshake(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + Future request = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + OutputStream clientOutput = client.getOutputStream(); + clientOutput.write(("" + + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n").getBytes("UTF-8")); + clientOutput.flush(); + return null; + } + }); + + // Application data + TLSRecord record = proxy.readFromClient(); + proxy.flushToServer(record); + Assert.assertNull(request.get(5, TimeUnit.SECONDS)); + + client.close(); + + // Close Alert + record = proxy.readFromClient(); + proxy.flushToServer(record); + // Socket close + record = proxy.readFromClient(); + Assert.assertNull(record); + proxy.flushToServer(record); + + // Expect response from server + // SSLSocket is limited and we cannot read the response, but we make sure + // it is application data and not a close alert + record = proxy.readFromServer(); + Assert.assertNotNull(record); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + proxy.flushToClient(record); + + // Close Alert + record = proxy.readFromServer(); + Assert.assertNotNull(record); + Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); + // We can't forward to the client, its socket is already closed + + // Socket close + record = proxy.readFromClient(); + Assert.assertNull(record); + proxy.flushToServer(record); + + // Socket close + record = proxy.readFromServer(); + Assert.assertNull(record); + proxy.flushToClient(record); + } + + @Test + public void testRequestWithCloseAlert() throws Exception + { + final SSLSocket client = newClient(); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + client.startHandshake(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + Future request = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + OutputStream clientOutput = client.getOutputStream(); + clientOutput.write(("" + + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n").getBytes("UTF-8")); + clientOutput.flush(); + return null; + } + }); + + // Application data + TLSRecord record = proxy.readFromClient(); + proxy.flushToServer(record); + Assert.assertNull(request.get(5, TimeUnit.SECONDS)); + + client.close(); + + // Close Alert + record = proxy.readFromClient(); + proxy.flushToServer(record); + + // Do not close the raw socket yet + + // Expect response from server + // SSLSocket is limited and we cannot read the response, but we make sure + // it is application data and not a close alert + record = proxy.readFromServer(); + Assert.assertNotNull(record); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + proxy.flushToClient(record); + + // Close Alert + record = proxy.readFromServer(); + Assert.assertNotNull(record); + Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); + // We can't forward to the client, its socket is already closed + + // Socket close + record = proxy.readFromClient(); + Assert.assertNull(record); + proxy.flushToServer(record); + + // Socket close + record = proxy.readFromServer(); + Assert.assertNull(record); + proxy.flushToClient(record); + } + + @Test + public void testRequestWithRawClose() throws Exception + { + final SSLSocket client = newClient(); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + client.startHandshake(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + Future request = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + OutputStream clientOutput = client.getOutputStream(); + clientOutput.write(("" + + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n").getBytes("UTF-8")); + clientOutput.flush(); + return null; + } + }); + + // Application data + TLSRecord record = proxy.readFromClient(); + proxy.flushToServer(record); + Assert.assertNull(request.get(5, TimeUnit.SECONDS)); + + // Close the raw socket + // This generates a truncation attack, and we cannot respond + // because we won't know if the request was legitimate + proxy.flushToServer((TLSRecord)null); + + // Expect raw close from server + record = proxy.readFromServer(); + Assert.assertNull(record); + proxy.flushToClient(record); + + client.close(); + } + + @Test + public void testRequestWithCloseAlertWithSplitBoundary() throws Exception + { + final SSLSocket client = newClient(); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + client.startHandshake(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + Future request = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + OutputStream clientOutput = client.getOutputStream(); + clientOutput.write(("" + + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n").getBytes("UTF-8")); + clientOutput.flush(); + return null; + } + }); + + // Application data + TLSRecord dataRecord = proxy.readFromClient(); + Assert.assertNull(request.get(5, TimeUnit.SECONDS)); + + client.close(); + + // Close Alert + TLSRecord closeRecord = proxy.readFromClient(); + + // Send request and half of the close alert bytes + byte[] dataBytes = dataRecord.getBytes(); + byte[] closeBytes = closeRecord.getBytes(); + byte[] bytes = new byte[dataBytes.length + closeBytes.length / 2]; + System.arraycopy(dataBytes, 0, bytes, 0, dataBytes.length); + System.arraycopy(closeBytes, 0, bytes, dataBytes.length, closeBytes.length / 2); + proxy.flushToServer(bytes); + + TimeUnit.MILLISECONDS.sleep(100); + + bytes = new byte[closeBytes.length - closeBytes.length / 2]; + System.arraycopy(closeBytes, closeBytes.length / 2, bytes, 0, bytes.length); + proxy.flushToServer(bytes); + + // Do not close the raw socket yet + + // Expect response from server + // SSLSocket is limited and we cannot read the response, but we make sure + // it is application data and not a close alert + TLSRecord record = proxy.readFromServer(); + Assert.assertNotNull(record); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + proxy.flushToClient(record); + + // Close Alert + record = proxy.readFromServer(); + Assert.assertNotNull(record); + Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); + // We can't forward to the client, its socket is already closed + + // Socket close + record = proxy.readFromClient(); + Assert.assertNull(record); + proxy.flushToServer(record); + + // Socket close + record = proxy.readFromServer(); + Assert.assertNull(record); + proxy.flushToClient(record); + } + + @Test + public void testRequestWithContentWithSplitBoundary() throws Exception + { + final SSLSocket client = newClient(); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + client.startHandshake(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + final String content = "0123456789ABCDEF"; + + Future request = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + OutputStream clientOutput = client.getOutputStream(); + clientOutput.write(("" + + "POST / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: " + content.length() + "\r\n" + + "\r\n" + + content).getBytes("UTF-8")); + clientOutput.flush(); + return null; + } + }); + + // Application data + TLSRecord record = proxy.readFromClient(); + Assert.assertNull(request.get(5, TimeUnit.SECONDS)); + byte[] chunk1 = new byte[2 * record.getBytes().length / 3]; + System.arraycopy(record.getBytes(), 0, chunk1, 0, chunk1.length); + proxy.flushToServer(chunk1); + + TimeUnit.MILLISECONDS.sleep(100); + + byte[] chunk2 = new byte[record.getBytes().length - chunk1.length]; + System.arraycopy(record.getBytes(), chunk1.length, chunk2, 0, chunk2.length); + proxy.flushToServer(chunk2); + + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + proxy.flushToClient(record); + + BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); + String line = reader.readLine(); + Assert.assertNotNull(line); + Assert.assertTrue(line.contains(" 200 ")); + while ((line = reader.readLine()) != null) + { + if (line.trim().length() == 0) + break; + } + + closeClient(client); + } + + @Test + public void testRequestWithBigContentWithSplitBoundary() throws Exception + { + final SSLSocket client = newClient(); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + client.startHandshake(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + // Use a content that is larger than the TLS record which is 2^14 (around 16k) + byte[] data = new byte[128 * 1024]; + Arrays.fill(data, (byte)'X'); + final String content = new String(data, "UTF-8"); + + Future request = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + OutputStream clientOutput = client.getOutputStream(); + clientOutput.write(("" + + "POST / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: " + content.length() + "\r\n" + + "\r\n" + + content).getBytes("UTF-8")); + clientOutput.flush(); + return null; + } + }); + + // The data is 8 times bigger than the TLS record max length, + // therefore there will be generated 9 TLSRecord + for (int i = 0; i < 9; ++i) + { + // Application data + TLSRecord record = proxy.readFromClient(); + byte[] bytes = record.getBytes(); + byte[] chunk1 = new byte[2 * bytes.length / 3]; + System.arraycopy(bytes, 0, chunk1, 0, chunk1.length); + proxy.flushToServer(chunk1); + + TimeUnit.MILLISECONDS.sleep(100); + + byte[] chunk2 = new byte[bytes.length - chunk1.length]; + System.arraycopy(bytes, chunk1.length, chunk2, 0, chunk2.length); + proxy.flushToServer(chunk2); + + TimeUnit.MILLISECONDS.sleep(100); + } + + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(10)); + Assert.assertThat(httpParses.get(), lessThan(50)); + + TLSRecord record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + proxy.flushToClient(record); + + BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); + String line = reader.readLine(); + Assert.assertNotNull(line); + Assert.assertTrue(line.contains(" 200 ")); + while ((line = reader.readLine()) != null) + { + if (line.trim().length() == 0) + break; + } + + closeClient(client); + } + + private SSLSocket newClient() throws IOException, InterruptedException + { + SSLSocket client = (SSLSocket)sslContext.getSocketFactory().createSocket("localhost", proxy.getPort()); + client.setUseClientMode(true); + Assert.assertTrue(proxy.awaitClient(5, TimeUnit.SECONDS)); + return client; + } + + private void closeClient(SSLSocket client) throws IOException + { + client.close(); + + // Close Alert + TLSRecord record = proxy.readFromClient(); + proxy.flushToServer(record); + // Socket close + record = proxy.readFromClient(); + Assert.assertNull(record); + proxy.flushToServer(record); + + // Close Alert + record = proxy.readFromServer(); + proxy.flushToClient(record); + // Socket close + record = proxy.readFromServer(); + Assert.assertNull(record); + proxy.flushToClient(record); + } + + public static class SimpleProxy implements Runnable + { + private final CountDownLatch latch = new CountDownLatch(1); + private final ExecutorService threadPool; + private final String serverHost; + private final int serverPort; + private volatile ServerSocket serverSocket; + private volatile Socket server; + private volatile Socket client; + + public SimpleProxy(ExecutorService threadPool, String serverHost, int serverPort) + { + this.threadPool = threadPool; + this.serverHost = serverHost; + this.serverPort = serverPort; + } + + public void start() throws Exception + { + serverSocket = new ServerSocket(5871); // TODO: make this random + Thread acceptor = new Thread(this); + acceptor.start(); + server = new Socket(serverHost, serverPort); + } + + public void stop() throws Exception + { + serverSocket.close(); + } + + public void run() + { + try + { + client = serverSocket.accept(); + latch.countDown(); + } + catch (IOException x) + { + x.printStackTrace(); + } + } + + public int getPort() + { + return serverSocket.getLocalPort(); + } + + public TLSRecord readFromClient() throws IOException + { + return read(client); + } + + private TLSRecord read(Socket socket) throws IOException + { + InputStream input = socket.getInputStream(); + int first = -2; + while (!Thread.currentThread().isInterrupted()) + { + try + { + socket.setSoTimeout(500); + first = input.read(); + break; + } + catch (SocketTimeoutException x) + { + // Ignore + } + } + if (first == -2) + throw new InterruptedIOException(); + else if (first == -1) + return null; + + if (first >= 0x80) + { + // SSLv2 Record + int hiLength = first & 0x3F; + int loLength = input.read(); + int length = (hiLength << 8) + loLength; + byte[] bytes = new byte[2 + length]; + bytes[0] = (byte)first; + bytes[1] = (byte)loLength; + return read(TLSRecord.Type.HANDSHAKE, input, bytes, 2, length); + } + else + { + // TLS Record + int major = input.read(); + int minor = input.read(); + int hiLength = input.read(); + int loLength = input.read(); + int length = (hiLength << 8) + loLength; + byte[] bytes = new byte[1 + 2 + 2 + length]; + bytes[0] = (byte)first; + bytes[1] = (byte)major; + bytes[2] = (byte)minor; + bytes[3] = (byte)hiLength; + bytes[4] = (byte)loLength; + return read(TLSRecord.Type.from(first), input, bytes, 5, length); + } + } + + private TLSRecord read(TLSRecord.Type type, InputStream input, byte[] bytes, int offset, int length) throws IOException + { + while (length > 0) + { + int read = input.read(bytes, offset, length); + if (read < 0) + throw new EOFException(); + offset += read; + length -= read; + } + return new TLSRecord(type, bytes); + } + + public void flushToServer(TLSRecord record) throws IOException + { + if (record == null) + { + server.shutdownOutput(); + if (client.isOutputShutdown()) + { + client.close(); + server.close(); + } + } + else + { + flush(server, record.getBytes()); + } + } + + public void flushToServer(byte... datum) throws IOException + { + flush(server, datum); + } + + private void flush(Socket socket, byte... bytes) throws IOException + { + OutputStream output = socket.getOutputStream(); + output.write(bytes); + output.flush(); + } + + public TLSRecord readFromServer() throws IOException + { + return read(server); + } + + public void flushToClient(TLSRecord record) throws IOException + { + if (record == null) + { + client.shutdownOutput(); + if (server.isOutputShutdown()) + { + server.close(); + client.close(); + } + } + else + { + flush(client, record.getBytes()); + } + } + + public AutomaticFlow startAutomaticFlow() + { + final CountDownLatch stopLatch = new CountDownLatch(2); + Future clientToServer = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + try + { + while (true) + { + flushToServer(readFromClient()); + } + } + catch (InterruptedIOException x) + { + return null; + } + finally + { + stopLatch.countDown(); + } + } + }); + Future serverToClient = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + try + { + while (true) + { + flushToClient(readFromServer()); + } + } + catch (InterruptedIOException x) + { + return null; + } + finally + { + stopLatch.countDown(); + } + } + }); + return new AutomaticFlow(stopLatch, clientToServer, serverToClient); + } + + public boolean awaitClient(int time, TimeUnit unit) throws InterruptedException + { + return latch.await(time, unit); + } + + public static class AutomaticFlow + { + private final CountDownLatch stopLatch; + private final Future clientToServer; + private final Future serverToClient; + + public AutomaticFlow(CountDownLatch stopLatch, Future clientToServer, Future serverToClient) + { + this.stopLatch = stopLatch; + this.clientToServer = clientToServer; + this.serverToClient = serverToClient; + } + + public boolean stop(long time, TimeUnit unit) throws InterruptedException + { + clientToServer.cancel(true); + serverToClient.cancel(true); + return stopLatch.await(time, unit); + } + } + } + + public static class TLSRecord + { + private final Type type; + private final byte[] bytes; + + public TLSRecord(Type type, byte[] bytes) + { + this.type = type; + this.bytes = bytes; + } + + public Type getType() + { + return type; + } + + public byte[] getBytes() + { + return bytes; + } + + public enum Type + { + CHANGE_CIPHER_SPEC(20), ALERT(21), HANDSHAKE(22), APPLICATION(23); + + private int code; + + private Type(int code) + { + this.code = code; + Mapper.codes.put(this.code, this); + } + + public static Type from(int code) + { + Type result = Mapper.codes.get(code); + if (result == null) + throw new IllegalArgumentException("Invalid TLSRecord.Type " + code); + return result; + } + + private static class Mapper + { + private static final Map codes = new HashMap(); + } + } + } + +} diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java index 7dfc5b6fe1e..88dd0c9b7b5 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java @@ -16,7 +16,6 @@ package org.eclipse.jetty.io.nio; import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicBoolean; - import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLEngineResult.HandshakeStatus; @@ -657,14 +656,8 @@ public class SslConnection extends AbstractConnection implements AsyncConnection while (now Date: Fri, 18 Nov 2011 09:19:52 +1100 Subject: [PATCH 05/13] prevent spin by write blocked from non dispatched thread --- .../jetty/io/nio/SelectChannelEndPoint.java | 2 ++ .../eclipse/jetty/io/nio/SelectorManager.java | 21 ++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java index 2a2e0dac18b..bedc36ef842 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java @@ -162,6 +162,8 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPo // we are not interested in further selecting _key.interestOps(0); + if (!_dispatched) + updateKey(); return; } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectorManager.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectorManager.java index 21823c8297b..8c6db52fb60 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectorManager.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectorManager.java @@ -444,7 +444,7 @@ public abstract class SelectorManager extends AbstractLifeCycle implements Dumpa // Stopped concurrently ? if (selector == null) return; - + // Make any key changes required Object change; int changes=_changes.size(); @@ -585,15 +585,6 @@ public abstract class SelectorManager extends AbstractLifeCycle implements Dumpa // Log and dump some status _paused=true; LOG.warn("Selector {} is too busy, pausing!",this); - final SelectSet set = this; - SelectorManager.this.dispatch( - new Runnable(){ - public void run() - { - System.err.println(set+":\n"+set.dump()); - } - public String toString() {return "Dump-"+super.toString();} - }); } } } @@ -991,6 +982,16 @@ public abstract class SelectorManager extends AbstractLifeCycle implements Dumpa dumpto.add(key.attachment()+" - - "); } } + + /* ------------------------------------------------------------ */ + public String toString() + { + String s=super.toString()+" "+SelectorManager.this.getState(); + Selector selector=_selector; + if (selector!=null && selector.isOpen()) + s+=",k="+selector.keys().size()+",s="+selector.selectedKeys().size(); + return s; + } } /* ------------------------------------------------------------ */ From ef49f079c89ad71f0c5e26c10e9ac594976c0b6c Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Fri, 18 Nov 2011 15:31:43 +1100 Subject: [PATCH 06/13] 363488 ShutdownHandler use stopper thread --- .../jetty/server/handler/ShutdownHandler.java | 30 ++++++++++------ .../server/handler/ShutdownHandlerTest.java | 36 ++++++++++++++++++- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java index 810f48993d9..aa4edeb7f61 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java @@ -68,6 +68,8 @@ public class ShutdownHandler extends AbstractHandler private final Server _server; private boolean _exitJvm = false; + + /** * Creates a listener that lets the server be shut down remotely (but only from localhost). @@ -110,18 +112,24 @@ public class ShutdownHandler extends AbstractHandler LOG.info("Shutting down by request from " + getRemoteAddr(request)); - try + new Thread() { - shutdownServer(); - } - catch (InterruptedException e) - { - LOG.ignore(e); - } - catch (Exception e) - { - throw new RuntimeException("Shutting down server",e); - } + public void run () + { + try + { + shutdownServer(); + } + catch (InterruptedException e) + { + LOG.ignore(e); + } + catch (Exception e) + { + throw new RuntimeException("Shutting down server",e); + } + } + }.start(); } private boolean requestFromLocalhost(HttpServletRequest request) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ShutdownHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ShutdownHandlerTest.java index 02dae765e79..0d46cc17496 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ShutdownHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ShutdownHandlerTest.java @@ -14,12 +14,18 @@ package org.eclipse.jetty.server.handler; //======================================================================== import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -48,8 +54,36 @@ public class ShutdownHandlerTest public void shutdownServerWithCorrectTokenAndIPTest() throws Exception { setDefaultExpectations(); + final CountDownLatch countDown = new CountDownLatch(1); + server.addLifeCycleListener(new AbstractLifeCycle.Listener () + { + + public void lifeCycleStarting(LifeCycle event) + { + } + + public void lifeCycleStarted(LifeCycle event) + { + } + + public void lifeCycleFailure(LifeCycle event, Throwable cause) + { + } + + public void lifeCycleStopping(LifeCycle event) + { + } + + public void lifeCycleStopped(LifeCycle event) + { + countDown.countDown(); + } + + }); shutdownHandler.handle("/shutdown",null,request,response); - assertEquals("Server should be stopped","STOPPED",server.getState()); + boolean stopped = countDown.await(1000, TimeUnit.MILLISECONDS); //wait up to 1 sec to stop + assertTrue("Server lifecycle stop listener called", stopped); + assertEquals("Server should be stopped","STOPPED",server.getState()); } @Test From defc729d6960a1c3f5b9d110b73b71bef1a7c294 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 18 Nov 2011 15:33:05 +1100 Subject: [PATCH 07/13] close idle SSL with shutdown --- .../org/eclipse/jetty/io/nio/SslConnection.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java index fa2b72e2c15..20f636f9ca6 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java @@ -239,6 +239,21 @@ public class SslConnection extends AbstractConnection implements AsyncConnection } + /* ------------------------------------------------------------ */ + @Override + public void onIdleExpired() + { + try + { + _sslEndPoint.shutdownOutput(); + } + catch (IOException e) + { + LOG.warn(e); + super.onIdleExpired(); + } + } + /* ------------------------------------------------------------ */ public void onInputShutdown() throws IOException { From 9c804921de475f55f71aaf7ac870e49ccc7dbef0 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Fri, 18 Nov 2011 11:54:31 +0100 Subject: [PATCH 08/13] Added more SSL tests for split boundary handshake and renegotiation in the middle of content. --- .../jetty/client/SslBytesServerTest.java | 294 ++++++++++++++++-- 1 file changed, 266 insertions(+), 28 deletions(-) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java index fb4a48af315..e8dfd6214a6 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java @@ -201,6 +201,115 @@ public class SslBytesServerTest closeClient(client); } + @Test + public void testHandshakeWithSplitBoundary() throws Exception + { + final SSLSocket client = newClient(); + + Future handshake = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + client.startHandshake(); + return null; + } + }); + + // Client Hello + TLSRecord record = proxy.readFromClient(); + byte[] bytes = record.getBytes(); + byte[] chunk1 = new byte[2 * bytes.length / 3]; + System.arraycopy(bytes, 0, chunk1, 0, chunk1.length); + byte[] chunk2 = new byte[bytes.length - chunk1.length]; + System.arraycopy(bytes, chunk1.length, chunk2, 0, chunk2.length); + proxy.flushToServer(chunk1); + TimeUnit.MILLISECONDS.sleep(100); + proxy.flushToServer(chunk2); + TimeUnit.MILLISECONDS.sleep(100); + + // Server Hello + Certificate + Server Done + record = proxy.readFromServer(); + proxy.flushToClient(record); + + // Client Key Exchange + record = proxy.readFromClient(); + bytes = record.getBytes(); + chunk1 = new byte[2 * bytes.length / 3]; + System.arraycopy(bytes, 0, chunk1, 0, chunk1.length); + chunk2 = new byte[bytes.length - chunk1.length]; + System.arraycopy(bytes, chunk1.length, chunk2, 0, chunk2.length); + proxy.flushToServer(chunk1); + TimeUnit.MILLISECONDS.sleep(100); + proxy.flushToServer(chunk2); + TimeUnit.MILLISECONDS.sleep(100); + + // Change Cipher Spec + record = proxy.readFromClient(); + bytes = record.getBytes(); + chunk1 = new byte[2 * bytes.length / 3]; + System.arraycopy(bytes, 0, chunk1, 0, chunk1.length); + chunk2 = new byte[bytes.length - chunk1.length]; + System.arraycopy(bytes, chunk1.length, chunk2, 0, chunk2.length); + proxy.flushToServer(chunk1); + TimeUnit.MILLISECONDS.sleep(100); + proxy.flushToServer(chunk2); + TimeUnit.MILLISECONDS.sleep(100); + + // Client Done + record = proxy.readFromClient(); + bytes = record.getBytes(); + chunk1 = new byte[2 * bytes.length / 3]; + System.arraycopy(bytes, 0, chunk1, 0, chunk1.length); + chunk2 = new byte[bytes.length - chunk1.length]; + System.arraycopy(bytes, chunk1.length, chunk2, 0, chunk2.length); + proxy.flushToServer(chunk1); + TimeUnit.MILLISECONDS.sleep(100); + proxy.flushToServer(chunk2); + TimeUnit.MILLISECONDS.sleep(100); + + // Change Cipher Spec + record = proxy.readFromServer(); + Assert.assertNotNull(record); + proxy.flushToClient(record); + + // Server Done + record = proxy.readFromServer(); + Assert.assertNotNull(record); + proxy.flushToClient(record); + + Assert.assertNull(handshake.get(5, TimeUnit.SECONDS)); + + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(httpParses.get(), lessThan(50)); + + client.close(); + + // Close Alert + record = proxy.readFromClient(); + bytes = record.getBytes(); + chunk1 = new byte[2 * bytes.length / 3]; + System.arraycopy(bytes, 0, chunk1, 0, chunk1.length); + chunk2 = new byte[bytes.length - chunk1.length]; + System.arraycopy(bytes, chunk1.length, chunk2, 0, chunk2.length); + proxy.flushToServer(chunk1); + TimeUnit.MILLISECONDS.sleep(100); + proxy.flushToServer(chunk2); + TimeUnit.MILLISECONDS.sleep(100); + // Socket close + record = proxy.readFromClient(); + Assert.assertNull("" + record, record); + proxy.flushToServer(record); + + // Close Alert + record = proxy.readFromServer(); + proxy.flushToClient(record); + // Socket close + record = proxy.readFromServer(); + Assert.assertNull("" + record, record); + proxy.flushToClient(record); + } + @Test public void testRequestResponse() throws Exception { @@ -237,7 +346,7 @@ public class SslBytesServerTest BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); String line = reader.readLine(); Assert.assertNotNull(line); - Assert.assertTrue(line.contains(" 200 ")); + Assert.assertTrue(line.startsWith("HTTP/1.1 200 ")); while ((line = reader.readLine()) != null) { if (line.trim().length() == 0) @@ -338,13 +447,17 @@ public class SslBytesServerTest BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); String line = reader.readLine(); Assert.assertNotNull(line); - Assert.assertTrue(line.contains(" 200 ")); + Assert.assertTrue(line.startsWith("HTTP/1.1 200 ")); while ((line = reader.readLine()) != null) { if (line.trim().length() == 0) break; } + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(500)); + Assert.assertThat(httpParses.get(), lessThan(150)); + client.close(); // Close Alert @@ -356,7 +469,7 @@ public class SslBytesServerTest } // Socket close record = proxy.readFromClient(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToServer(record); // Close Alert @@ -364,7 +477,7 @@ public class SslBytesServerTest proxy.flushToClient(record); // Socket close record = proxy.readFromServer(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToClient(record); } @@ -414,7 +527,7 @@ public class SslBytesServerTest proxy.flushToServer(record); // Socket close record = proxy.readFromClient(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToServer(record); // Expect response from server @@ -433,12 +546,12 @@ public class SslBytesServerTest // Socket close record = proxy.readFromClient(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToServer(record); // Socket close record = proxy.readFromServer(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToClient(record); } @@ -494,12 +607,12 @@ public class SslBytesServerTest // Socket close record = proxy.readFromClient(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToServer(record); // Socket close record = proxy.readFromServer(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToClient(record); } @@ -538,7 +651,7 @@ public class SslBytesServerTest // Expect raw close from server record = proxy.readFromServer(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToClient(record); client.close(); @@ -608,12 +721,12 @@ public class SslBytesServerTest // Socket close record = proxy.readFromClient(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToServer(record); // Socket close record = proxy.readFromServer(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToClient(record); } @@ -665,7 +778,7 @@ public class SslBytesServerTest BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); String line = reader.readLine(); Assert.assertNotNull(line); - Assert.assertTrue(line.contains(" 200 ")); + Assert.assertTrue(line.startsWith("HTTP/1.1 200 ")); while ((line = reader.readLine()) != null) { if (line.trim().length() == 0) @@ -706,8 +819,7 @@ public class SslBytesServerTest } }); - // The data is 8 times bigger than the TLS record max length, - // therefore there will be generated 9 TLSRecord + // Nine TLSRecords will be generated for the request for (int i = 0; i < 9; ++i) { // Application data @@ -715,21 +827,20 @@ public class SslBytesServerTest byte[] bytes = record.getBytes(); byte[] chunk1 = new byte[2 * bytes.length / 3]; System.arraycopy(bytes, 0, chunk1, 0, chunk1.length); - proxy.flushToServer(chunk1); - - TimeUnit.MILLISECONDS.sleep(100); - byte[] chunk2 = new byte[bytes.length - chunk1.length]; System.arraycopy(bytes, chunk1.length, chunk2, 0, chunk2.length); + proxy.flushToServer(chunk1); + TimeUnit.MILLISECONDS.sleep(100); proxy.flushToServer(chunk2); - TimeUnit.MILLISECONDS.sleep(100); } // Check that we did not spin - Assert.assertThat(sslHandles.get(), lessThan(10)); + Assert.assertThat(sslHandles.get(), lessThan(20)); Assert.assertThat(httpParses.get(), lessThan(50)); + Assert.assertNull(request.get(5, TimeUnit.SECONDS)); + TLSRecord record = proxy.readFromServer(); Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); proxy.flushToClient(record); @@ -737,7 +848,127 @@ public class SslBytesServerTest BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); String line = reader.readLine(); Assert.assertNotNull(line); - Assert.assertTrue(line.contains(" 200 ")); + Assert.assertTrue(line.startsWith("HTTP/1.1 200 ")); + while ((line = reader.readLine()) != null) + { + if (line.trim().length() == 0) + break; + } + + closeClient(client); + } + + @Test + public void testRequestWithBigContentWithRenegotiationInMiddleOfContent() throws Exception + { + final SSLSocket client = newClient(); + final OutputStream clientOutput = client.getOutputStream(); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + client.startHandshake(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + // Use a content that is larger than the TLS record which is 2^14 (around 16k) + byte[] data1 = new byte[80 * 1024]; + Arrays.fill(data1, (byte)'X'); + String content1 = new String(data1, "UTF-8"); + byte[] data2 = new byte[48 * 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 + Future renegotiation = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + client.startHandshake(); + return null; + } + }); + + // Renegotiation Handshake + TLSRecord record = proxy.readFromClient(); + proxy.flushToServer(record); + + // Renegotiation Handshake + record = proxy.readFromServer(); + 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(); + proxy.flushToClient(record); + + // Trigger a read to have the client write the final renegotiation steps + client.setSoTimeout(100); + try + { + client.getInputStream().read(); + Assert.fail(); + } + catch (SocketTimeoutException x) + { + // Expected + } + + // Renegotiation Change Cipher + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType()); + proxy.flushToServer(record); + + // Renegotiation Handshake + record = proxy.readFromClient(); + proxy.flushToServer(record); + + Assert.assertNull(renegotiation.get(5, TimeUnit.SECONDS)); + + // Write the rest of the request + Future request = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + clientOutput.write(content2.getBytes("UTF-8")); + clientOutput.flush(); + return null; + } + }); + + // Three TLSRecords will be generated for the remainder of the content + for (int i = 0; i < 3; ++i) + { + // Application data + record = proxy.readFromClient(); + proxy.flushToServer(record); + } + + Assert.assertNull(request.get(5, TimeUnit.SECONDS)); + + // Read response + // Application Data + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + proxy.flushToClient(record); + + BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); + String line = reader.readLine(); + Assert.assertNotNull(line); + Assert.assertTrue(line.startsWith("HTTP/1.1 200 ")); while ((line = reader.readLine()) != null) { if (line.trim().length() == 0) @@ -764,7 +995,7 @@ public class SslBytesServerTest proxy.flushToServer(record); // Socket close record = proxy.readFromClient(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToServer(record); // Close Alert @@ -772,7 +1003,7 @@ public class SslBytesServerTest proxy.flushToClient(record); // Socket close record = proxy.readFromServer(); - Assert.assertNull(record); + Assert.assertNull("" + record, record); proxy.flushToClient(record); } @@ -833,7 +1064,7 @@ public class SslBytesServerTest { InputStream input = socket.getInputStream(); int first = -2; - while (!Thread.currentThread().isInterrupted()) + while (true) { try { @@ -843,7 +1074,8 @@ public class SslBytesServerTest } catch (SocketTimeoutException x) { - // Ignore + if (Thread.currentThread().isInterrupted()) + break; } } if (first == -2) @@ -910,9 +1142,9 @@ public class SslBytesServerTest } } - public void flushToServer(byte... datum) throws IOException + public void flushToServer(byte... bytes) throws IOException { - flush(server, datum); + flush(server, bytes); } private void flush(Socket socket, byte... bytes) throws IOException @@ -1040,6 +1272,12 @@ public class SslBytesServerTest return bytes; } + @Override + public String toString() + { + return "TLSRecord [" + type + "] " + bytes.length + " bytes"; + } + public enum Type { CHANGE_CIPHER_SPEC(20), ALERT(21), HANDSHAKE(22), APPLICATION(23); From a313d2f5424c85b20e1009ea5af7bb5d3e2d6300 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Fri, 18 Nov 2011 12:20:34 +0100 Subject: [PATCH 09/13] Added more SSL tests renegotiation in the middle of content with split boundary. --- .../jetty/client/SslBytesServerTest.java | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java index e8dfd6214a6..90455853585 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java @@ -198,6 +198,10 @@ public class SslBytesServerTest Assert.assertNull(handshake.get(5, TimeUnit.SECONDS)); + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(httpParses.get(), lessThan(50)); + closeClient(client); } @@ -353,6 +357,10 @@ public class SslBytesServerTest break; } + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(httpParses.get(), lessThan(50)); + closeClient(client); } @@ -605,6 +613,10 @@ public class SslBytesServerTest Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); // We can't forward to the client, its socket is already closed + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(httpParses.get(), lessThan(50)); + // Socket close record = proxy.readFromClient(); Assert.assertNull("" + record, record); @@ -719,6 +731,10 @@ public class SslBytesServerTest Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); // We can't forward to the client, its socket is already closed + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(httpParses.get(), lessThan(50)); + // Socket close record = proxy.readFromClient(); Assert.assertNull("" + record, record); @@ -785,6 +801,10 @@ public class SslBytesServerTest break; } + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(httpParses.get(), lessThan(50)); + closeClient(client); } @@ -855,6 +875,10 @@ public class SslBytesServerTest break; } + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(httpParses.get(), lessThan(50)); + closeClient(client); } @@ -975,6 +999,168 @@ public class SslBytesServerTest break; } + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(httpParses.get(), lessThan(50)); + + closeClient(client); + } + + @Test + public void testRequestWithBigContentWithRenegotiationInMiddleOfContentWithSplitBoundary() throws Exception + { + final SSLSocket client = newClient(); + final OutputStream clientOutput = client.getOutputStream(); + + SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); + client.startHandshake(); + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); + + // Use a content that is larger than the TLS record which is 2^14 (around 16k) + byte[] data1 = new byte[80 * 1024]; + Arrays.fill(data1, (byte)'X'); + String content1 = new String(data1, "UTF-8"); + byte[] data2 = new byte[48 * 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 + Future renegotiation = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + client.startHandshake(); + return null; + } + }); + + // Renegotiation Handshake + TLSRecord record = proxy.readFromClient(); + byte[] bytes = record.getBytes(); + byte[] chunk1 = new byte[2 * bytes.length / 3]; + System.arraycopy(bytes, 0, chunk1, 0, chunk1.length); + byte[] chunk2 = new byte[bytes.length - chunk1.length]; + System.arraycopy(bytes, chunk1.length, chunk2, 0, chunk2.length); + proxy.flushToServer(chunk1); + TimeUnit.MILLISECONDS.sleep(100); + proxy.flushToServer(chunk2); + TimeUnit.MILLISECONDS.sleep(100); + + // Renegotiation Handshake + record = proxy.readFromServer(); + 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(); + proxy.flushToClient(record); + + // Trigger a read to have the client write the final renegotiation steps + client.setSoTimeout(100); + try + { + client.getInputStream().read(); + Assert.fail(); + } + catch (SocketTimeoutException x) + { + // Expected + } + + // Renegotiation Change Cipher + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType()); + bytes = record.getBytes(); + chunk1 = new byte[2 * bytes.length / 3]; + System.arraycopy(bytes, 0, chunk1, 0, chunk1.length); + chunk2 = new byte[bytes.length - chunk1.length]; + System.arraycopy(bytes, chunk1.length, chunk2, 0, chunk2.length); + proxy.flushToServer(chunk1); + TimeUnit.MILLISECONDS.sleep(100); + proxy.flushToServer(chunk2); + TimeUnit.MILLISECONDS.sleep(100); + + // Renegotiation Handshake + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); + bytes = record.getBytes(); + chunk1 = new byte[2 * bytes.length / 3]; + System.arraycopy(bytes, 0, chunk1, 0, chunk1.length); + chunk2 = new byte[bytes.length - chunk1.length]; + System.arraycopy(bytes, chunk1.length, chunk2, 0, chunk2.length); + proxy.flushToServer(chunk1); + TimeUnit.MILLISECONDS.sleep(100); + // Do not write the second chunk now, but merge it with content, see below + + Assert.assertNull(renegotiation.get(5, TimeUnit.SECONDS)); + + // Write the rest of the request + Future request = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + clientOutput.write(content2.getBytes("UTF-8")); + clientOutput.flush(); + return null; + } + }); + + // Three TLSRecords will be generated for the remainder of the content + // Merge the last chunk of the renegotiation with the first data record + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + byte[] dataBytes = record.getBytes(); + byte[] mergedBytes = new byte[chunk2.length + dataBytes.length]; + System.arraycopy(chunk2, 0, mergedBytes, 0, chunk2.length); + System.arraycopy(dataBytes, 0, mergedBytes, chunk2.length, dataBytes.length); + proxy.flushToServer(mergedBytes); + // Write the remaining 2 TLS records + for (int i = 0; i < 2; ++i) + { + // Application data + record = proxy.readFromClient(); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + proxy.flushToServer(record); + } + + Assert.assertNull(request.get(5, TimeUnit.SECONDS)); + + // Read response + // Application Data + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + proxy.flushToClient(record); + + BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); + String line = reader.readLine(); + Assert.assertNotNull(line); + Assert.assertTrue(line.startsWith("HTTP/1.1 200 ")); + while ((line = reader.readLine()) != null) + { + if (line.trim().length() == 0) + break; + } + + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(httpParses.get(), lessThan(50)); + closeClient(client); } From 5b67abdf02ca8876f0e11664389625fac510d871 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Fri, 18 Nov 2011 21:39:18 +0100 Subject: [PATCH 10/13] Made SslBytesServerTest more robust in case of automatic proxy flow. --- .../jetty/client/SslBytesServerTest.java | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java index 90455853585..68e9f280181 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java @@ -42,6 +42,8 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.After; import org.junit.Assert; @@ -53,6 +55,7 @@ import static org.hamcrest.Matchers.lessThan; public class SslBytesServerTest { + private final Logger logger = Log.getLogger(getClass()); private final AtomicInteger sslHandles = new AtomicInteger(); private final AtomicInteger httpParses = new AtomicInteger(); private ExecutorService threadPool; @@ -63,7 +66,7 @@ public class SslBytesServerTest @Before public void startServer() throws Exception { - threadPool = Executors.newFixedThreadPool(2); + threadPool = Executors.newCachedThreadPool(); server = new Server(); SslSelectChannelConnector connector = new SslSelectChannelConnector() @@ -133,7 +136,7 @@ public class SslBytesServerTest proxy = new SimpleProxy(threadPool, "localhost", connector.getLocalPort()); proxy.start(); - System.err.println(":" + proxy.getPort() + " <==> :" + connector.getLocalPort()); + logger.debug(":{} <==> :{}", proxy.getPort(), connector.getLocalPort()); } @After @@ -302,7 +305,7 @@ public class SslBytesServerTest TimeUnit.MILLISECONDS.sleep(100); // Socket close record = proxy.readFromClient(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToServer(record); // Close Alert @@ -310,7 +313,7 @@ public class SslBytesServerTest proxy.flushToClient(record); // Socket close record = proxy.readFromServer(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToClient(record); } @@ -477,7 +480,7 @@ public class SslBytesServerTest } // Socket close record = proxy.readFromClient(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToServer(record); // Close Alert @@ -485,7 +488,7 @@ public class SslBytesServerTest proxy.flushToClient(record); // Socket close record = proxy.readFromServer(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToClient(record); } @@ -535,7 +538,7 @@ public class SslBytesServerTest proxy.flushToServer(record); // Socket close record = proxy.readFromClient(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToServer(record); // Expect response from server @@ -554,12 +557,12 @@ public class SslBytesServerTest // Socket close record = proxy.readFromClient(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToServer(record); // Socket close record = proxy.readFromServer(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToClient(record); } @@ -619,12 +622,12 @@ public class SslBytesServerTest // Socket close record = proxy.readFromClient(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToServer(record); // Socket close record = proxy.readFromServer(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToClient(record); } @@ -656,16 +659,23 @@ public class SslBytesServerTest proxy.flushToServer(record); Assert.assertNull(request.get(5, TimeUnit.SECONDS)); - // Close the raw socket - // This generates a truncation attack, and we cannot respond - // because we won't know if the request was legitimate + // Application data + record = proxy.readFromServer(); + Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); + proxy.flushToClient(record); + + // Close the raw socket, this generates a truncation attack proxy.flushToServer((TLSRecord)null); // Expect raw close from server record = proxy.readFromServer(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToClient(record); + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(httpParses.get(), lessThan(50)); + client.close(); } @@ -737,12 +747,12 @@ public class SslBytesServerTest // Socket close record = proxy.readFromClient(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToServer(record); // Socket close record = proxy.readFromServer(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToClient(record); } @@ -1181,7 +1191,7 @@ public class SslBytesServerTest proxy.flushToServer(record); // Socket close record = proxy.readFromClient(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToServer(record); // Close Alert @@ -1189,11 +1199,11 @@ public class SslBytesServerTest proxy.flushToClient(record); // Socket close record = proxy.readFromServer(); - Assert.assertNull("" + record, record); + Assert.assertNull(String.valueOf(record), record); proxy.flushToClient(record); } - public static class SimpleProxy implements Runnable + public class SimpleProxy implements Runnable { private final CountDownLatch latch = new CountDownLatch(1); private final ExecutorService threadPool; @@ -1243,7 +1253,9 @@ public class SslBytesServerTest public TLSRecord readFromClient() throws IOException { - return read(client); + TLSRecord record = read(client); + logger.debug("C --> P {}", record); + return record; } private TLSRecord read(Socket socket) throws IOException @@ -1342,7 +1354,9 @@ public class SslBytesServerTest public TLSRecord readFromServer() throws IOException { - return read(server); + TLSRecord record = read(server); + logger.debug("P <-- S {}", record); + return record; } public void flushToClient(TLSRecord record) throws IOException @@ -1362,13 +1376,16 @@ public class SslBytesServerTest } } - public AutomaticFlow startAutomaticFlow() + public AutomaticFlow startAutomaticFlow() throws InterruptedException { + final CountDownLatch startLatch = new CountDownLatch(2); final CountDownLatch stopLatch = new CountDownLatch(2); Future clientToServer = threadPool.submit(new Callable() { public Object call() throws Exception { + startLatch.countDown(); + logger.debug("Automatic flow C --> S started"); try { while (true) @@ -1383,6 +1400,7 @@ public class SslBytesServerTest finally { stopLatch.countDown(); + logger.debug("Automatic flow C --> S finished"); } } }); @@ -1390,6 +1408,8 @@ public class SslBytesServerTest { public Object call() throws Exception { + startLatch.countDown(); + logger.debug("Automatic flow C <-- S started"); try { while (true) @@ -1404,9 +1424,11 @@ public class SslBytesServerTest finally { stopLatch.countDown(); + logger.debug("Automatic flow C <-- S finished"); } } }); + Assert.assertTrue(startLatch.await(5, TimeUnit.SECONDS)); return new AutomaticFlow(stopLatch, clientToServer, serverToClient); } @@ -1415,7 +1437,7 @@ public class SslBytesServerTest return latch.await(time, unit); } - public static class AutomaticFlow + public class AutomaticFlow { private final CountDownLatch stopLatch; private final Future clientToServer; From dbd97a52ae0e201f26589b00caec4a195770c1f8 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 19 Nov 2011 09:48:47 +1100 Subject: [PATCH 11/13] flush ssl shutdownoutput --- .../src/main/java/org/eclipse/jetty/io/nio/SslConnection.java | 1 + 1 file changed, 1 insertion(+) diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java index b26f0dde1c0..9fcc1fe57e6 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java @@ -600,6 +600,7 @@ public class SslConnection extends AbstractConnection implements AsyncConnection _engine.closeOutbound(); _oshut=true; } + flush(); } public boolean isOutputShutdown() From a00f790bc52d334fd2a665e1fa12e2c75c55d37f Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Mon, 21 Nov 2011 10:25:26 +1100 Subject: [PATCH 12/13] fixed close on early shutdown --- .../src/main/java/org/eclipse/jetty/io/nio/SslConnection.java | 1 - .../main/java/org/eclipse/jetty/server/AsyncHttpConnection.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java index 9fcc1fe57e6..a27f2d45a30 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java @@ -235,7 +235,6 @@ public class SslConnection extends AbstractConnection implements AsyncConnection /* ------------------------------------------------------------ */ public void onClose() { - } /* ------------------------------------------------------------ */ diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncHttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncHttpConnection.java index 9ed2c33c166..203af125d90 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncHttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncHttpConnection.java @@ -161,7 +161,7 @@ public class AsyncHttpConnection extends AbstractHttpConnection implements Async if (_generator.isIdle() && !_request.getAsyncContinuation().isSuspended()) { // then no more can happen, so close. - _endp.shutdownOutput(); + _endp.close(); } } From 722444506af7bfab137f61dfc8590e21ec0708a7 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Mon, 21 Nov 2011 12:30:29 +1100 Subject: [PATCH 13/13] 362626 IllegalStateException thrown when SslContextFactory preconfigured with SSLContext --- .../jetty/util/ssl/SslContextFactory.java | 17 +++++++-------- .../jetty/util/ssl/SslContextFactoryTest.java | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 10 deletions(-) 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 d4f16f39869..6180022cd7e 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 @@ -255,15 +255,8 @@ public class SslContextFactory extends AbstractLifeCycle else { // verify that keystore and truststore - // parameters are set up correctly - try - { - checkKeyStore(); - } - catch(IllegalStateException e) - { - LOG.ignore(e); - } + // parameters are set up correctly + checkKeyStore(); KeyStore keyStore = loadKeyStore(); KeyStore trustStore = loadTrustStore(); @@ -1158,13 +1151,17 @@ public class SslContextFactory extends AbstractLifeCycle /* ------------------------------------------------------------ */ /** - * Check KetyStore Configuration. Ensures that if keystore has been + * Check KeyStore Configuration. Ensures that if keystore has been * configured but there's no truststore, that keystore is * used as truststore. * @throws IllegalStateException if SslContextFactory configuration can't be used. */ public void checkKeyStore() { + if (_context != null) + return; //nothing to check if using preconfigured context + + if (_keyStore == null && _keyStoreInputStream == null && _keyStorePath == null) throw new IllegalStateException("SSL doesn't have a valid keystore"); 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 17e9cff811a..075998d85fc 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 @@ -160,4 +160,25 @@ public class SslContextFactoryTest { } } + + @Test + public void testNoKeyConfig() throws Exception + { + SslContextFactory cf = new SslContextFactory(); + try + { + ((StdErrLog)Log.getLogger(AbstractLifeCycle.class)).setHideStacks(true); + cf.setTrustStore("/foo"); + cf.start(); + Assert.fail(); + } + catch (IllegalStateException e) + { + + } + catch (Exception e) + { + Assert.fail("Unexpected exception"); + } + } }