blob: d4c8ce8ab04a240971e8559c13ccc84da710e70d [file] [log] [blame]
//
// ========================================================================
// 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.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ManagedSelector;
import org.eclipse.jetty.io.SelectChannelEndPoint;
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 SelectChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException
{
SelectChannelEndPoint 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);
}
}