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 794cc31b938..3f133c5d44e 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; @@ -55,7 +56,7 @@ public class LikeJettyXml // Setup Threadpool QueuedThreadPool threadPool = new QueuedThreadPool(); - threadPool.setMaxThreads(100); + threadPool.setMaxThreads(500); server.setThreadPool(threadPool); // Setup Connectors @@ -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..bc476c27fc7 --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/Siege.java @@ -0,0 +1,211 @@ +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; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + + +/* ------------------------------------------------------------ */ +/** + */ +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; + volatile long _bytes; + 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 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); + } + + /* ------------------------------------------------------------ */ + /** + * @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 :{}", 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)); + + // Check that we did not spin + Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(httpParses.get(), lessThan(50)); + + 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(String.valueOf(record), record); + proxy.flushToServer(record); + + // Close Alert + record = proxy.readFromServer(); + proxy.flushToClient(record); + // Socket close + record = proxy.readFromServer(); + Assert.assertNull(String.valueOf(record), record); + proxy.flushToClient(record); + } + + @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.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); + } + + @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.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 + record = proxy.readFromClient(); + for (byte b : record.getBytes()) + { + proxy.flushToServer(b); + TimeUnit.MILLISECONDS.sleep(50); + } + // Socket close + record = proxy.readFromClient(); + Assert.assertNull(String.valueOf(record), record); + proxy.flushToServer(record); + + // Close Alert + record = proxy.readFromServer(); + proxy.flushToClient(record); + // Socket close + record = proxy.readFromServer(); + Assert.assertNull(String.valueOf(record), 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(String.valueOf(record), 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(String.valueOf(record), record); + proxy.flushToServer(record); + + // Socket close + record = proxy.readFromServer(); + Assert.assertNull(String.valueOf(record), 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 + + // 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(String.valueOf(record), record); + proxy.flushToServer(record); + + // Socket close + record = proxy.readFromServer(); + Assert.assertNull(String.valueOf(record), 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)); + + // 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(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(); + } + + @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 + + // 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(String.valueOf(record), record); + proxy.flushToServer(record); + + // Socket close + record = proxy.readFromServer(); + Assert.assertNull(String.valueOf(record), 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.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); + } + + @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; + } + }); + + // Nine TLSRecords will be generated for the request + 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); + 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(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); + + 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); + } + + @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) + 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); + } + + 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(String.valueOf(record), record); + proxy.flushToServer(record); + + // Close Alert + record = proxy.readFromServer(); + proxy.flushToClient(record); + // Socket close + record = proxy.readFromServer(); + Assert.assertNull(String.valueOf(record), record); + proxy.flushToClient(record); + } + + public 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 + { + TLSRecord record = read(client); + logger.debug("C --> P {}", record); + return record; + } + + private TLSRecord read(Socket socket) throws IOException + { + InputStream input = socket.getInputStream(); + int first = -2; + while (true) + { + try + { + socket.setSoTimeout(500); + first = input.read(); + break; + } + catch (SocketTimeoutException x) + { + if (Thread.currentThread().isInterrupted()) + break; + } + } + 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... bytes) throws IOException + { + flush(server, bytes); + } + + private void flush(Socket socket, byte... bytes) throws IOException + { + OutputStream output = socket.getOutputStream(); + output.write(bytes); + output.flush(); + } + + public TLSRecord readFromServer() throws IOException + { + TLSRecord record = read(server); + logger.debug("P <-- S {}", record); + return record; + } + + 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() 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) + { + flushToServer(readFromClient()); + } + } + catch (InterruptedIOException x) + { + return null; + } + finally + { + stopLatch.countDown(); + logger.debug("Automatic flow C --> S finished"); + } + } + }); + Future serverToClient = threadPool.submit(new Callable() + { + public Object call() throws Exception + { + startLatch.countDown(); + logger.debug("Automatic flow C <-- S started"); + try + { + while (true) + { + flushToClient(readFromServer()); + } + } + catch (InterruptedIOException x) + { + return null; + } + finally + { + stopLatch.countDown(); + logger.debug("Automatic flow C <-- S finished"); + } + } + }); + Assert.assertTrue(startLatch.await(5, TimeUnit.SECONDS)); + return new AutomaticFlow(stopLatch, clientToServer, serverToClient); + } + + public boolean awaitClient(int time, TimeUnit unit) throws InterruptedException + { + return latch.await(time, unit); + } + + public 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; + } + + @Override + public String toString() + { + return "TLSRecord [" + type + "] " + bytes.length + " 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/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; + } } /* ------------------------------------------------------------ */ 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..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 @@ -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; @@ -236,9 +235,23 @@ public class SslConnection extends AbstractConnection implements AsyncConnection /* ------------------------------------------------------------ */ public void onClose() { - } + /* ------------------------------------------------------------ */ + @Override + public void onIdleExpired() + { + try + { + _sslEndPoint.shutdownOutput(); + } + catch (IOException e) + { + LOG.warn(e); + super.onIdleExpired(); + } + } + /* ------------------------------------------------------------ */ public void onInputShutdown() throws IOException { @@ -586,6 +599,7 @@ public class SslConnection extends AbstractConnection implements AsyncConnection _engine.closeOutbound(); _oshut=true; } + flush(); } public boolean isOutputShutdown() @@ -786,14 +800,14 @@ public class SslConnection extends AbstractConnection implements AsyncConnection Buffer i; Buffer o; Buffer u; - + synchronized(SslConnection.this) { i=_inbound; 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":"")); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractHttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractHttpConnection.java index d988d51b035..68a6aacc574 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractHttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractHttpConnection.java @@ -42,6 +42,7 @@ import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.AsyncEndPoint; import org.eclipse.jetty.io.Buffer; import org.eclipse.jetty.io.BufferCache.CachedBuffer; +import org.eclipse.jetty.io.Buffers; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.EofException; @@ -148,7 +149,7 @@ public abstract class AbstractHttpConnection extends AbstractConnection _uri = StringUtil.__UTF8.equals(URIUtil.__CHARSET)?new HttpURI():new EncodedHttpURI(URIUtil.__CHARSET); _connector = connector; HttpBuffers ab = (HttpBuffers)_connector; - _parser = new HttpParser(ab.getRequestBuffers(), endpoint, new RequestHandler()); + _parser = newHttpParser(ab.getRequestBuffers(), endpoint, new RequestHandler()); _requestFields = new HttpFields(); _responseFields = new HttpFields(server.getMaxCookieVersion()); _request = new Request(this); @@ -163,7 +164,7 @@ public abstract class AbstractHttpConnection extends AbstractConnection Parser parser, Generator generator, Request request) { super(endpoint); - + _uri = URIUtil.__CHARSET.equals(StringUtil.__UTF8)?new HttpURI():new EncodedHttpURI(URIUtil.__CHARSET); _connector = connector; _parser = parser; @@ -176,6 +177,11 @@ public abstract class AbstractHttpConnection extends AbstractConnection _server = server; } + protected HttpParser newHttpParser(Buffers requestBuffers, EndPoint endpoint, HttpParser.EventHandler requestHandler) + { + return new HttpParser(requestBuffers, endpoint, requestHandler); + } + /* ------------------------------------------------------------ */ /** * @return the parser used by this connection @@ -193,13 +199,13 @@ public abstract class AbstractHttpConnection extends AbstractConnection { return _requests; } - + /* ------------------------------------------------------------ */ public Server getServer() { return _server; } - + /* ------------------------------------------------------------ */ /** * @return Returns the associatedObject. @@ -385,11 +391,11 @@ public abstract class AbstractHttpConnection extends AbstractConnection /* ------------------------------------------------------------ */ public void reset() { - _parser.reset(); + _parser.reset(); _parser.returnBuffers(); // TODO maybe only on unhandle _requestFields.clear(); _request.recycle(); - _generator.reset(); + _generator.reset(); _generator.returnBuffers();// TODO maybe only on unhandle _responseFields.clear(); _response.recycle(); @@ -566,7 +572,7 @@ public abstract class AbstractHttpConnection extends AbstractConnection } catch(RuntimeException e) { - LOG.warn("header full: "+e); + LOG.warn("header full: " + e); _response.reset(); _generator.reset(); @@ -672,7 +678,7 @@ public abstract class AbstractHttpConnection extends AbstractConnection { LOG.debug("closed {}",this); } - + /* ------------------------------------------------------------ */ public boolean isExpecting100Continues() { @@ -717,7 +723,7 @@ public abstract class AbstractHttpConnection extends AbstractConnection public void startRequest(Buffer method, Buffer uri, Buffer version) throws IOException { uri=uri.asImmutableBuffer(); - + _host = false; _expect = false; _expect100Continue=false; @@ -864,11 +870,11 @@ public abstract class AbstractHttpConnection extends AbstractConnection _generator.setPersistent(true); _parser.setPersistent(true); } - + if (_server.getSendDateHeader()) _generator.setDate(_request.getTimeStampBuffer()); break; - + case HttpVersions.HTTP_1_1_ORDINAL: _generator.setHead(_head); @@ -959,7 +965,7 @@ public abstract class AbstractHttpConnection extends AbstractConnection if (LOG.isDebugEnabled()) LOG.debug("Bad request!: "+version+" "+status+" "+reason); } - + } @@ -1144,5 +1150,5 @@ public abstract class AbstractHttpConnection extends AbstractConnection } } - + } 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(); } } 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(); } } 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/main/java/org/eclipse/jetty/server/nio/SelectChannelConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/SelectChannelConnector.java index f59e3135538..6b37246a919 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/nio/SelectChannelConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/SelectChannelConnector.java @@ -31,14 +31,11 @@ import org.eclipse.jetty.io.nio.SelectChannelEndPoint; import org.eclipse.jetty.io.nio.SelectorManager; import org.eclipse.jetty.io.nio.SelectorManager.SelectSet; import org.eclipse.jetty.server.AsyncHttpConnection; -import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.component.AggregateLifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.ThreadPool; -import org.eclipse.jetty.util.thread.Timeout.Task; /* ------------------------------------------------------------------------------- */ /** @@ -97,7 +94,7 @@ public class SelectChannelConnector extends AbstractNIOConnector { server = _acceptChannel; } - + if (server!=null && server.isOpen() && _manager.isStarted()) { SocketChannel channel = server.accept(); @@ -144,7 +141,7 @@ public class SelectChannelConnector extends AbstractNIOConnector { return _manager; } - + /* ------------------------------------------------------------ */ public synchronized Object getConnection() { @@ -297,7 +294,7 @@ public class SelectChannelConnector extends AbstractNIOConnector /* ------------------------------------------------------------------------------- */ protected AsyncConnection newConnection(SocketChannel channel,final AsyncEndPoint endpoint) { - return new SelectChannelHttpConnection(SelectChannelConnector.this,endpoint,getServer()); + return new AsyncHttpConnection(SelectChannelConnector.this,endpoint,getServer()); } /* ------------------------------------------------------------ */ @@ -310,24 +307,9 @@ public class SelectChannelConnector extends AbstractNIOConnector channel=_acceptChannel; } if (channel==null) - AggregateLifeCycle.dump(out,indent,Arrays.asList(new Object[]{null,"CLOSED",_manager})); + AggregateLifeCycle.dump(out,indent,Arrays.asList(null,"CLOSED",_manager)); else - AggregateLifeCycle.dump(out,indent,Arrays.asList(new Object[]{channel,channel.isOpen()?"OPEN":"CLOSED",_manager})); - } - - /* ------------------------------------------------------------ */ - /* ------------------------------------------------------------ */ - /* ------------------------------------------------------------ */ - private class SelectChannelHttpConnection extends AsyncHttpConnection - { - private final AsyncEndPoint _endpoint; - - private SelectChannelHttpConnection(Connector connector, EndPoint endpoint, Server server) - { - super(connector,endpoint,server); - _endpoint=null; - } - + AggregateLifeCycle.dump(out,indent,Arrays.asList(channel,channel.isOpen()?"OPEN":"CLOSED",_manager)); } /* ------------------------------------------------------------ */ diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/SslSelectChannelConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/SslSelectChannelConnector.java index 7fd6a27432b..5c6ca0d60fe 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/SslSelectChannelConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/SslSelectChannelConnector.java @@ -14,16 +14,12 @@ package org.eclipse.jetty.server.ssl; import java.io.IOException; -import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; - import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; -import org.eclipse.jetty.http.HttpParser; import org.eclipse.jetty.http.HttpSchemes; import org.eclipse.jetty.io.AsyncEndPoint; import org.eclipse.jetty.io.Buffers; @@ -33,10 +29,7 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.RuntimeIOException; import org.eclipse.jetty.io.bio.SocketEndPoint; import org.eclipse.jetty.io.nio.AsyncConnection; -import org.eclipse.jetty.io.nio.SelectChannelEndPoint; -import org.eclipse.jetty.io.nio.SelectorManager.SelectSet; import org.eclipse.jetty.io.nio.SslConnection; -import org.eclipse.jetty.server.AsyncHttpConnection; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -102,7 +95,7 @@ public class SslSelectChannelConnector extends SelectChannelConnector implements SslConnection.SslEndPoint sslEndpoint=(SslConnection.SslEndPoint)endpoint; SSLEngine sslEngine=sslEndpoint.getSslEngine(); SSLSession sslSession=sslEngine.getSession(); - + SslCertificates.customize(sslSession,endpoint,request); } @@ -541,13 +534,6 @@ public class SslSelectChannelConnector extends SelectChannelConnector implements return integralPort==0||integralPort==request.getServerPort(); } - /* ------------------------------------------------------------------------------- */ - @Override - protected SelectChannelEndPoint newEndPoint(SocketChannel channel, SelectSet selectSet, SelectionKey key) throws IOException - { - return super.newEndPoint(channel,selectSet,key); - } - /* ------------------------------------------------------------------------------- */ @Override protected AsyncConnection newConnection(SocketChannel channel, AsyncEndPoint endpoint) @@ -555,20 +541,28 @@ public class SslSelectChannelConnector extends SelectChannelConnector implements try { SSLEngine engine = createSSLEngine(channel); - - SslConnection connection = new SslConnection(engine,endpoint); - - AsyncConnection delegate = super.newConnection(channel,connection.getSslEndPoint()); + SslConnection connection = newSslConnection(endpoint, engine); + AsyncConnection delegate = newPlainConnection(channel, connection.getSslEndPoint()); connection.getSslEndPoint().setConnection(delegate); connection.setAllowRenegotiate(_sslContextFactory.isAllowRenegotiate()); return connection; } - catch(IOException e) + catch (IOException e) { throw new RuntimeIOException(e); } } + protected AsyncConnection newPlainConnection(SocketChannel channel, AsyncEndPoint endPoint) + { + return super.newConnection(channel, endPoint); + } + + protected SslConnection newSslConnection(AsyncEndPoint endpoint, SSLEngine engine) + { + return new SslConnection(engine, endpoint); + } + /* ------------------------------------------------------------ */ /** * @param channel A channel which if passed is used as to extract remote @@ -589,7 +583,7 @@ public class SslSelectChannelConnector extends SelectChannelConnector implements { engine = _sslContextFactory.newSslEngine(); } - + engine.setUseClientMode(false); return engine; } 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 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"); + } + } }