// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.client.ssl; import java.io.BufferedReader; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.SocketTimeoutException; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.Arrays; 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 java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; 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.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.ssl.SslBytesTest.TLSRecord.Type; import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.HttpParser; import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConnection; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.OS; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assert; import org.junit.Assume; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; public class SslBytesServerTest extends SslBytesTest { private final AtomicInteger sslFills = new AtomicInteger(); private final AtomicInteger sslFlushes = new AtomicInteger(); private final AtomicInteger httpParses = new AtomicInteger(); private final AtomicReference<EndPoint> serverEndPoint = new AtomicReference<>(); private final int idleTimeout = 2000; private ExecutorService threadPool; private Server server; private SslContextFactory sslContextFactory; private int serverPort; private SSLContext sslContext; private SimpleProxy proxy; private Runnable idleHook; @Before public void init() throws Exception { threadPool = Executors.newCachedThreadPool(); server = new Server(); File keyStore = MavenTestingUtils.getTestResourceFile("keystore.jks"); sslContextFactory = new SslContextFactory(); sslContextFactory.setKeyStorePath(keyStore.getAbsolutePath()); sslContextFactory.setKeyStorePassword("storepwd"); HttpConnectionFactory httpFactory = new HttpConnectionFactory() { @Override public Connection newConnection(Connector connector, EndPoint endPoint) { return configure(new HttpConnection(getHttpConfiguration(), connector, endPoint,getHttpCompliance(),isRecordHttpComplianceViolations()) { @Override protected HttpParser newHttpParser(HttpCompliance compliance) { return new HttpParser(newRequestHandler(), getHttpConfiguration().getRequestHeaderSize(),compliance) { @Override public boolean parseNext(ByteBuffer buffer) { httpParses.incrementAndGet(); return super.parseNext(buffer); } }; } @Override protected boolean onReadTimeout() { final Runnable idleHook = SslBytesServerTest.this.idleHook; if (idleHook != null) idleHook.run(); return super.onReadTimeout(); } }, connector, endPoint); } }; httpFactory.getHttpConfiguration().addCustomizer(new SecureRequestCustomizer()); SslConnectionFactory sslFactory = new SslConnectionFactory(sslContextFactory, httpFactory.getProtocol()) { @Override protected SslConnection newSslConnection(Connector connector, EndPoint endPoint, SSLEngine engine) { return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine) { @Override protected DecryptedEndPoint newDecryptedEndPoint() { return new DecryptedEndPoint() { @Override public int fill(ByteBuffer buffer) throws IOException { sslFills.incrementAndGet(); return super.fill(buffer); } @Override public boolean flush(ByteBuffer... appOuts) throws IOException { sslFlushes.incrementAndGet(); return super.flush(appOuts); } }; } }; } }; ServerConnector connector = new ServerConnector(server, null,null,null,1,1,sslFactory, httpFactory) { @Override protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException { ChannelEndPoint endp = super.newEndPoint(channel,selectSet,key); serverEndPoint.set(endp); return endp; } }; connector.setIdleTimeout(idleTimeout); connector.setPort(0); server.addConnector(connector); server.setHandler(new AbstractHandler() { @Override public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException { try { request.setHandled(true); String contentLength = request.getHeader("Content-Length"); if (contentLength != null) { int length = Integer.parseInt(contentLength); ServletInputStream input = httpRequest.getInputStream(); ServletOutputStream output = httpResponse.getOutputStream(); byte[] buffer = new byte[32 * 1024]; while (length > 0) { int read = input.read(buffer); if (read < 0) throw new EOFException(); length -= read; if (target.startsWith("/echo")) output.write(buffer, 0, read); } } } catch (IOException x) { if (!(target.endsWith("suppress_exception"))) throw x; } } }); server.start(); serverPort = connector.getLocalPort(); sslContext = sslContextFactory.getSslContext(); proxy = new SimpleProxy(threadPool, "localhost", serverPort); proxy.start(); logger.info("proxy:{} <==> server:{}", proxy.getPort(), serverPort); } @After public void destroy() throws Exception { if (proxy != null) proxy.stop(); if (server != null) server.stop(); if (threadPool != null) threadPool.shutdownNow(); } @Test(timeout=10000) public void testHandshake() throws Exception { final SSLSocket client = newClient(); Future<Object> handshake = threadPool.submit(() -> { 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 TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); closeClient(client); } @Test(timeout=60000) public void testHandshakeWithResumedSessionThenClose() throws Exception { // First socket will establish the SSL session SSLSocket client1 = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client1.startHandshake(); client1.close(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); int proxyPort = proxy.getPort(); proxy.stop(); proxy = new SimpleProxy(threadPool, proxyPort, "localhost", serverPort); proxy.start(); logger.info("proxy:{} <==> server:{}", proxy.getPort(), serverPort); final SSLSocket client2 = newClient(proxy); threadPool.submit(() -> { client2.startHandshake(); return null; }); // Client Hello with SessionID TLSRecord record = proxy.readFromClient(); Assert.assertNotNull(record); proxy.flushToServer(record); // Server Hello record = proxy.readFromServer(); Assert.assertNotNull(record); proxy.flushToClient(record); // Change Cipher Spec record = proxy.readFromServer(); Assert.assertNotNull(record); proxy.flushToClient(record); // Server Done record = proxy.readFromServer(); Assert.assertNotNull(record); proxy.flushToClient(record); // Client Key Exchange record = proxy.readFromClient(); Assert.assertNotNull(record); // Client Done TLSRecord doneRecord = proxy.readFromClient(); Assert.assertNotNull(doneRecord); // Close client2.close(); TLSRecord closeRecord = proxy.readFromClient(); Assert.assertNotNull(closeRecord); Assert.assertEquals(TLSRecord.Type.ALERT, closeRecord.getType()); // Flush to server Client Key Exchange + Client Done + Close in one chunk byte[] recordBytes = record.getBytes(); byte[] doneBytes = doneRecord.getBytes(); byte[] closeRecordBytes = closeRecord.getBytes(); byte[] chunk = new byte[recordBytes.length + doneBytes.length + closeRecordBytes.length]; System.arraycopy(recordBytes, 0, chunk, 0, recordBytes.length); System.arraycopy(doneBytes, 0, chunk, recordBytes.length, doneBytes.length); System.arraycopy(closeRecordBytes, 0, chunk, recordBytes.length + doneBytes.length, closeRecordBytes.length); proxy.flushToServer(0, chunk); // Close the raw socket proxy.flushToServer(null); // Expect the server to send a TLS Alert. record = proxy.readFromServer(); Assert.assertNotNull(record); Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); record = proxy.readFromServer(); Assert.assertNull(record); // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); } @Test(timeout=60000) public void testHandshakeWithSplitBoundary() throws Exception { final SSLSocket client = newClient(); Future<Object> handshake = threadPool.submit(() -> { 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(100, chunk1); proxy.flushToServer(100, chunk2); // 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(100, chunk1); proxy.flushToServer(100, chunk2); // 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(100, chunk1); proxy.flushToServer(100, chunk2); // 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(100, chunk1); proxy.flushToServer(100, chunk2); // 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 TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(40)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); 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(100, chunk1); proxy.flushToServer(100, chunk2); // Socket close record = proxy.readFromClient(); Assert.assertNull(String.valueOf(record), record); proxy.flushToServer(record); // Socket close record = proxy.readFromServer(); if (record!=null) { Assert.assertEquals(record.getType(),Type.ALERT); // Now should be a raw close record = proxy.readFromServer(); Assert.assertNull(String.valueOf(record), record); } } @Test(timeout=60000) public void testClientHelloIncompleteThenReset() throws Exception { final SSLSocket client = newClient(); threadPool.submit(() -> { 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); proxy.flushToServer(100, chunk1); proxy.sendRSTToServer(); // Wait a while to detect spinning TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); client.close(); } @Test(timeout=60000) public void testClientHelloThenReset() throws Exception { final SSLSocket client = newClient(); threadPool.submit(() -> { client.startHandshake(); return null; }); // Client Hello TLSRecord record = proxy.readFromClient(); Assert.assertNotNull(record); proxy.flushToServer(record); proxy.sendRSTToServer(); // Wait a while to detect spinning TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); client.close(); } @Test(timeout=60000) public void testHandshakeThenReset() throws Exception { final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); proxy.sendRSTToServer(); // Wait a while to detect spinning TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); client.close(); } @Test(timeout=60000) public void testRequestIncompleteThenReset() throws Exception { final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); threadPool.submit(() -> { OutputStream clientOutput = client.getOutputStream(); clientOutput.write(("" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n").getBytes(StandardCharsets.UTF_8)); clientOutput.flush(); return null; }); // 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(100, chunk1); proxy.sendRSTToServer(); // Wait a while to detect spinning TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); client.close(); } @Test(timeout=60000) public void testRequestResponse() throws Exception { final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); Future<Object> request = threadPool.submit(() -> { OutputStream clientOutput = client.getOutputStream(); clientOutput.write(("" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n").getBytes(StandardCharsets.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(), StandardCharsets.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 TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); closeClient(client); } @Test(timeout=60000) public void testHandshakeAndRequestOneByteAtATime() throws Exception { final SSLSocket client = newClient(); Future<Object> handshake = threadPool.submit(() -> { client.startHandshake(); return null; }); // Client Hello TLSRecord record = proxy.readFromClient(); for (byte b : record.getBytes()) proxy.flushToServer(5, b); // Server Hello + Certificate + Server Done record = proxy.readFromServer(); proxy.flushToClient(record); // Client Key Exchange record = proxy.readFromClient(); for (byte b : record.getBytes()) proxy.flushToServer(5,b); // Change Cipher Spec record = proxy.readFromClient(); for (byte b : record.getBytes()) proxy.flushToServer(5, b); // Client Done record = proxy.readFromClient(); for (byte b : record.getBytes()) proxy.flushToServer(5, b); // Change Cipher Spec record = proxy.readFromServer(); proxy.flushToClient(record); // Server Done record = proxy.readFromServer(); proxy.flushToClient(record); Assert.assertNull(handshake.get(1, TimeUnit.SECONDS)); Future<Object> request = threadPool.submit(() -> { OutputStream clientOutput = client.getOutputStream(); clientOutput.write(("" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n").getBytes(StandardCharsets.UTF_8)); clientOutput.flush(); return null; }); // Application data record = proxy.readFromClient(); for (byte b : record.getBytes()) proxy.flushToServer(5, b); Assert.assertNull(request.get(1, 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(), StandardCharsets.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 TimeUnit.MILLISECONDS.sleep(1000); Assert.assertThat(sslFills.get(), Matchers.lessThan(2000)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); // An average of 958 httpParses is seen in standard Oracle JDK's // An average of 1183 httpParses is seen in OpenJDK JVMs. Assert.assertThat(httpParses.get(), Matchers.lessThan(2000)); client.close(); // Close Alert record = proxy.readFromClient(); for (byte b : record.getBytes()) proxy.flushToServer(5, b); // Socket close record = proxy.readFromClient(); Assert.assertNull(String.valueOf(record), record); proxy.flushToServer(record); // Socket close record = proxy.readFromServer(); // Raw close or alert if (record!=null) { Assert.assertEquals(record.getType(),Type.ALERT); // Now should be a raw close record = proxy.readFromServer(); Assert.assertNull(String.valueOf(record), record); } } @Test(timeout=60000) public void testRequestWithCloseAlertAndShutdown() throws Exception { // See next test on why we only run in Linux Assume.assumeTrue(OS.IS_LINUX); final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); Future<Object> request = threadPool.submit(() -> { OutputStream clientOutput = client.getOutputStream(); clientOutput.write(("" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n").getBytes(StandardCharsets.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); // Socket close record = proxy.readFromServer(); if (record!=null) { Assert.assertEquals(record.getType(),Type.ALERT); // Now should be a raw close record = proxy.readFromServer(); Assert.assertNull(String.valueOf(record), record); } // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); } @Test(timeout=60000) public void testRequestWithCloseAlert() throws Exception { // Currently we are ignoring this test on anything other then linux // http://tools.ietf.org/html/rfc2246#section-7.2.1 // TODO (react to this portion which seems to allow win/mac behavior) // It is required that the other party respond with a close_notify alert of its own // and close down the connection immediately, discarding any pending writes. It is not // required for the initiator of the close to wait for the responding // close_notify alert before closing the read side of the connection. Assume.assumeTrue(OS.IS_LINUX); final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); Future<Object> request = threadPool.submit(() -> { OutputStream clientOutput = client.getOutputStream(); clientOutput.write(("" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n").getBytes(StandardCharsets.UTF_8)); clientOutput.flush(); return null; }); // Application data TLSRecord record = proxy.readFromClient(); Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); proxy.flushToServer(record); Assert.assertNull(request.get(5, TimeUnit.SECONDS)); client.close(); // Close Alert record = proxy.readFromClient(); Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); 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); // Socket close record = proxy.readFromServer(); if (record!=null) { Assert.assertEquals(record.getType(),Type.ALERT); // Now should be a raw close record = proxy.readFromServer(); Assert.assertNull(String.valueOf(record), record); } // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); // Socket close record = proxy.readFromClient(); Assert.assertNull(String.valueOf(record), record); proxy.flushToServer(record); } @Test(timeout=60000) public void testRequestWithRawClose() throws Exception { final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); Future<Object> request = threadPool.submit(() -> { OutputStream clientOutput = client.getOutputStream(); clientOutput.write(("" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n").getBytes(StandardCharsets.UTF_8)); clientOutput.flush(); return null; }); // Application data TLSRecord record = proxy.readFromClient(); Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); 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(null); // Expect raw close from server OR ALERT record = proxy.readFromServer(); // TODO check that this is OK? if (record!=null) { Assert.assertEquals(record.getType(),Type.ALERT); // Now should be a raw close record = proxy.readFromServer(); Assert.assertNull(String.valueOf(record), record); } // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); client.close(); } @Test(timeout=60000) public void testRequestWithImmediateRawClose() throws Exception { final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); Future<Object> request = threadPool.submit(() -> { OutputStream clientOutput = client.getOutputStream(); clientOutput.write(("" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n").getBytes(StandardCharsets.UTF_8)); clientOutput.flush(); return null; }); // Application data TLSRecord record = proxy.readFromClient(); Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); proxy.flushToServer(record, 0); // Close the raw socket, this generates a truncation attack proxy.flushToServer(null); Assert.assertNull(request.get(5, TimeUnit.SECONDS)); // Application data record = proxy.readFromServer(); Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); proxy.flushToClient(record); // Expect raw close from server record = proxy.readFromServer(); if (record!=null) { Assert.assertEquals(record.getType(),Type.ALERT); // Now should be a raw close record = proxy.readFromServer(); Assert.assertNull(String.valueOf(record), record); } // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); client.close(); } @Test(timeout=60000) public void testRequestWithBigContentWriteBlockedThenReset() throws Exception { // Don't run on Windows (buggy JVM) Assume.assumeTrue(!OS.IS_WINDOWS); final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); byte[] data = new byte[128 * 1024]; Arrays.fill(data, (byte)'X'); final String content = new String(data, StandardCharsets.UTF_8); Future<Object> request = threadPool.submit(() -> { OutputStream clientOutput = client.getOutputStream(); clientOutput.write(("" + "GET /echo HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: " + content.length() + "\r\n" + "\r\n" + content).getBytes(StandardCharsets.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(); Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); proxy.flushToServer(record, 0); } Assert.assertNull(request.get(5, TimeUnit.SECONDS)); // We asked the server to echo back the data we sent // but we do not read it, thus causing a write interest // on the server. // However, we then simulate that the client resets the // connection, and this will cause an exception in the // server that is trying to write the data TimeUnit.MILLISECONDS.sleep(500); proxy.sendRSTToServer(); // Wait a while to detect spinning TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(40)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(40)); Assert.assertThat(httpParses.get(), Matchers.lessThan(50)); client.close(); } @Test(timeout=60000) public void testRequestWithBigContentReadBlockedThenReset() throws Exception { // Don't run on Windows (buggy JVM) Assume.assumeTrue(!OS.IS_WINDOWS); final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); byte[] data = new byte[128 * 1024]; Arrays.fill(data, (byte)'X'); final String content = new String(data, StandardCharsets.UTF_8); Future<Object> request = threadPool.submit(() -> { OutputStream clientOutput = client.getOutputStream(); clientOutput.write(("" + "GET /echo_suppress_exception HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: " + content.length() + "\r\n" + "\r\n" + content).getBytes(StandardCharsets.UTF_8)); clientOutput.flush(); return null; }); // Nine TLSRecords will be generated for the request, // but we write only 5 of them, so the server goes in read blocked state for (int i = 0; i < 5; ++i) { // Application data TLSRecord record = proxy.readFromClient(); Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); proxy.flushToServer(record, 0); } Assert.assertNull(request.get(5, TimeUnit.SECONDS)); // The server should be read blocked, and we send a RST TimeUnit.MILLISECONDS.sleep(500); proxy.sendRSTToServer(); // Wait a while to detect spinning TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(40)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(40)); Assert.assertThat(httpParses.get(), Matchers.lessThan(50)); client.close(); } @Test(timeout=60000) public void testRequestWithCloseAlertWithSplitBoundary() throws Exception { if (!OS.IS_LINUX) { // currently we are ignoring this test on anything other then linux //http://tools.ietf.org/html/rfc2246#section-7.2.1 // TODO (react to this portion which seems to allow win/mac behavior) //It is required that the other party respond with a close_notify alert of its own //and close down the connection immediately, discarding any pending writes. It is not //required for the initiator of the close to wait for the responding //close_notify alert before closing the read side of the connection. return; } final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); Future<Object> request = threadPool.submit(() -> { OutputStream clientOutput = client.getOutputStream(); clientOutput.write(("" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n").getBytes(StandardCharsets.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(100, bytes); // Send the other half of the close alert bytes bytes = new byte[closeBytes.length - closeBytes.length / 2]; System.arraycopy(closeBytes, closeBytes.length / 2, bytes, 0, bytes.length); proxy.flushToServer(100, 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); // Socket close record = proxy.readFromServer(); if (record!=null) { Assert.assertEquals(record.getType(),Type.ALERT); // Now should be a raw close record = proxy.readFromServer(); Assert.assertNull(String.valueOf(record), record); } // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); } @Test(timeout=60000) 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<Object> request = threadPool.submit(() -> { 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(StandardCharsets.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(100, chunk1); byte[] chunk2 = new byte[record.getBytes().length - chunk1.length]; System.arraycopy(record.getBytes(), chunk1.length, chunk2, 0, chunk2.length); proxy.flushToServer(100, chunk2); record = proxy.readFromServer(); Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); proxy.flushToClient(record); BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.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 TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); closeClient(client); } @Test(timeout=60000) 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, StandardCharsets.UTF_8); Future<Object> request = threadPool.submit(() -> { 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(StandardCharsets.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(100, chunk1); proxy.flushToServer(100, chunk2); } // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(100)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(50)); Assert.assertThat(httpParses.get(), Matchers.lessThan(100)); 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(), StandardCharsets.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 TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(100)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(50)); Assert.assertThat(httpParses.get(), Matchers.lessThan(100)); closeClient(client); } // TODO work out why this test frequently fails @Ignore @Test(timeout=10000) public void testRequestWithContentWithRenegotiationInMiddleOfContentWhenRenegotiationIsForbidden() throws Exception { assumeJavaVersionSupportsTLSRenegotiations(); sslContextFactory.setRenegotiationAllowed(false); final SSLSocket client = newClient(); final OutputStream clientOutput = client.getOutputStream(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); byte[] data1 = new byte[1024]; Arrays.fill(data1, (byte)'X'); String content1 = new String(data1, StandardCharsets.UTF_8); byte[] data2 = new byte[1024]; Arrays.fill(data2, (byte)'Y'); final String content2 = new String(data2, StandardCharsets.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(StandardCharsets.UTF_8)); clientOutput.flush(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); // Renegotiate threadPool.submit(() -> { client.startHandshake(); return null; }); // Renegotiation Handshake TLSRecord record = proxy.readFromClient(); Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); proxy.flushToServer(record); // Renegotiation now allowed, server has closed record = proxy.readFromServer(); Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); proxy.flushToClient(record); record = proxy.readFromServer(); Assert.assertNull(record); // Write the rest of the request threadPool.submit(() -> { clientOutput.write(content2.getBytes(StandardCharsets.UTF_8)); clientOutput.flush(); return null; }); // Trying to write more application data results in an exception since the server closed record = proxy.readFromClient(); proxy.flushToServer(record); try { record = proxy.readFromClient(); Assert.assertNotNull(record); proxy.flushToServer(record); Assert.fail(); } catch (IOException expected) { // Expected } // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(50)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(50)); client.close(); } @Test(timeout=60000) public void testRequestWithBigContentWithRenegotiationInMiddleOfContent() throws Exception { assumeJavaVersionSupportsTLSRenegotiations(); 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, StandardCharsets.UTF_8); byte[] data2 = new byte[48 * 1024]; Arrays.fill(data2, (byte)'Y'); final String content2 = new String(data2, StandardCharsets.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(StandardCharsets.UTF_8)); clientOutput.flush(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); // Renegotiate Future<Object> renegotiation = threadPool.submit(() -> { client.startHandshake(); return null; }); // Renegotiation Handshake TLSRecord record = proxy.readFromClient(); Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); proxy.flushToServer(record); // Renegotiation Handshake record = proxy.readFromServer(); Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); proxy.flushToClient(record); // Renegotiation Change Cipher record = proxy.readFromServer(); Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType()); proxy.flushToClient(record); // Renegotiation Handshake record = proxy.readFromServer(); Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); proxy.flushToClient(record); // 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(); Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); proxy.flushToServer(record); Assert.assertNull(renegotiation.get(5, TimeUnit.SECONDS)); // Write the rest of the request Future<Object> request = threadPool.submit(() -> { clientOutput.write(content2.getBytes(StandardCharsets.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(), StandardCharsets.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 TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(50)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(50)); closeClient(client); } @Test(timeout=10000) public void testRequestWithBigContentWithRenegotiationInMiddleOfContentWithSplitBoundary() throws Exception { assumeJavaVersionSupportsTLSRenegotiations(); 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, StandardCharsets.UTF_8); byte[] data2 = new byte[48 * 1024]; Arrays.fill(data2, (byte)'Y'); final String content2 = new String(data2, StandardCharsets.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(StandardCharsets.UTF_8)); clientOutput.flush(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); // Renegotiate Future<Object> renegotiation = threadPool.submit(() -> { client.startHandshake(); return null; }); // Renegotiation Handshake TLSRecord record = proxy.readFromClient(); Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); 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(100, chunk1); proxy.flushToServer(100, chunk2); // Renegotiation Handshake record = proxy.readFromServer(); Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); proxy.flushToClient(record); // Renegotiation Change Cipher record = proxy.readFromServer(); Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType()); proxy.flushToClient(record); // Renegotiation Handshake record = proxy.readFromServer(); Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType()); proxy.flushToClient(record); // 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(100, chunk1); proxy.flushToServer(100, chunk2); // 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(100, chunk1); // 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<Object> request = threadPool.submit(() -> { clientOutput.write(content2.getBytes(StandardCharsets.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(100, 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(), StandardCharsets.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 TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(50)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(100)); closeClient(client); } @Test(timeout=60000) public void testServerShutdownOutputClientDoesNotCloseServerCloses() 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)); byte[] data = new byte[3 * 1024]; Arrays.fill(data, (byte)'Y'); String content = new String(data, StandardCharsets.UTF_8); automaticProxyFlow = proxy.startAutomaticFlow(); clientOutput.write(("" + "POST / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: " + content.length() + "\r\n" + "Connection: close\r\n" + "\r\n" + content).getBytes(StandardCharsets.UTF_8)); clientOutput.flush(); BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.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; } Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); // Check client is at EOF Assert.assertEquals(-1, client.getInputStream().read()); // Client should close the socket, but let's hold it open. // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); // The server has shutdown the output since the client sent a Connection: close // but the client does not close, so the server must idle timeout the endPoint. TimeUnit.MILLISECONDS.sleep(idleTimeout + idleTimeout / 2); Assert.assertFalse(serverEndPoint.get().isOpen()); } @Test(timeout=60000) public void testPlainText() throws Exception { final SSLSocket client = newClient(); threadPool.submit(() -> { client.startHandshake(); return null; }); // Instead of passing the Client Hello, we simulate plain text was passed in proxy.flushToServer(0, "GET / HTTP/1.1\r\n".getBytes(StandardCharsets.UTF_8)); // We expect that the server sends the TLS Alert. TLSRecord record = proxy.readFromServer(); Assert.assertNotNull(record); Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); record = proxy.readFromServer(); Assert.assertNull(record); // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(20)); client.close(); } @Test(timeout=60000) public void testRequestConcurrentWithIdleExpiration() throws Exception { final SSLSocket client = newClient(); final OutputStream clientOutput = client.getOutputStream(); final CountDownLatch latch = new CountDownLatch(1); idleHook = () -> { if (latch.getCount()==0) return; try { // Send request clientOutput.write(("" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n").getBytes(StandardCharsets.UTF_8)); clientOutput.flush(); latch.countDown(); } catch (Exception x) { // Latch won't trigger and test will fail x.printStackTrace(); } }; SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); client.startHandshake(); Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); Assert.assertTrue(latch.await(idleTimeout * 2, TimeUnit.MILLISECONDS)); // Be sure that the server sent a SSL close alert TLSRecord record = proxy.readFromServer(); Assert.assertNotNull(record); Assert.assertEquals(TLSRecord.Type.ALERT, record.getType()); // Write the request to the server, to simulate a request // concurrent with the SSL close alert record = proxy.readFromClient(); Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType()); proxy.flushToServer(record, 0); // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); Assert.assertThat(sslFills.get(), Matchers.lessThan(20)); Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20)); Assert.assertThat(httpParses.get(), Matchers.lessThan(50)); record = proxy.readFromServer(); Assert.assertNull(record); TimeUnit.MILLISECONDS.sleep(200); Assert.assertThat(((Dumpable)server.getConnectors()[0]).dump(), Matchers.not(Matchers.containsString("SCEP@"))); } private void assumeJavaVersionSupportsTLSRenegotiations() { // Due to a security bug, TLS renegotiations were disabled in JDK 1.6.0_19-21 // so we check the java version in order to avoid to fail the test. String javaVersion = System.getProperty("java.version"); Pattern regexp = Pattern.compile("1\\.6\\.0_(\\d{2})"); Matcher matcher = regexp.matcher(javaVersion); if (matcher.matches()) { String nano = matcher.group(1); Assume.assumeThat(Integer.parseInt(nano), Matchers.greaterThan(21)); } } private SSLSocket newClient() throws IOException, InterruptedException { return newClient(proxy); } private SSLSocket newClient(SimpleProxy proxy) 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 Exception { 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); // Socket close record = proxy.readFromServer(); if (record!=null) { Assert.assertEquals(record.getType(),Type.ALERT); record = proxy.readFromServer(); } Assert.assertNull(record); } }