// Copyright (C) 2005 - 2011 Philip Aston
// All rights reserved.
//
// This file is part of The Grinder software distribution. Refer to
// the file LICENSE which is part of The Grinder distribution for
// licensing details. The Grinder distribution is available on the
// Internet at http://grinder.sourceforge.net/
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.
package net.grinder.tools.tcpproxy;
import static net.grinder.testutility.SocketUtilities.findFreePort;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.contains;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLSocket;
import net.grinder.common.Logger;
import net.grinder.common.UncheckedInterruptedException;
import net.grinder.testutility.AssertUtilities;
import net.grinder.util.StreamCopier;
import net.grinder.util.TerminalColour;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Unit tests for {@link HTTPProxyTCPProxyEngine}.
*
* @author Philip Aston
*/
public class TestHTTPProxyTCPProxyEngine {
private final List<AcceptAndEcho> m_echoers = new java.util.LinkedList<AcceptAndEcho>();
private final MyFilterStubFactory m_requestFilterStubFactory =
new MyFilterStubFactory();
private final TCPProxyFilter m_requestFilter =
m_requestFilterStubFactory.getStub();
private final MyFilterStubFactory m_responseFilterStubFactory =
new MyFilterStubFactory();
private final TCPProxyFilter m_responseFilter =
m_responseFilterStubFactory.getStub();
@Mock private Logger m_logger;
private EndPoint m_localEndPoint;
private TCPProxySSLSocketFactory m_sslSocketFactory;
private EndPoint createFreeLocalEndPoint() throws IOException {
return new EndPoint("localhost", findFreePort());
}
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(m_logger.getOutputLogWriter())
.thenReturn(new PrintWriter(new StringWriter()));
m_localEndPoint = createFreeLocalEndPoint();
m_sslSocketFactory = new TCPProxySSLSocketFactoryImplementation();
// Set the filters not to generate random output.
m_requestFilterStubFactory.setResult(null);
m_responseFilterStubFactory.setResult(null);
// Speed things up.
System.setProperty("tcpproxy.connecttimeout", "500");
}
@After public void tearDown() throws Exception {
final Iterator<AcceptAndEcho> iterator = m_echoers.iterator();
while (iterator.hasNext()) {
iterator.next().shutdown();
}
}
@Test public void testBadLocalPort() throws Exception {
try {
new HTTPProxyTCPProxyEngine(null,
m_requestFilter,
m_responseFilter,
m_logger,
new EndPoint("unknownhost", 222),
false,
1000,
null,
null);
fail("Expected UnknownHostException");
}
catch (UnknownHostException e) {
}
}
@Test public void testTimeOut() throws Exception {
final TCPProxyEngine engine =
new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
m_requestFilter,
m_responseFilter,
m_logger,
m_localEndPoint,
false,
10,
null,
null);
// If this ends up spinning its probably because
// some other test has not terminated all of its filter
// threads correctly.
engine.run();
verify(m_logger).error(contains("Listen time out"));
}
/**
* Read a response from a socket until it matches a given regular expression.
*
* @param clientSocket
* Socket to read from.
* @param terminalExpression
* The expression, or <code>null</code> to return the first buffer
* full.
* @return The response.
* @throws IOException If a IO problem occurs.
* @throws InterruptedException If we're interrupted.
*/
private String readResponse(final Socket clientSocket,
String terminalExpression)
throws IOException, InterruptedException {
final InputStream clientInputStream = clientSocket.getInputStream();
if (clientSocket instanceof SSLSocket) {
// Another reason to hate JSSE: available() returns 0 until the
// first read after the server has sent something; reading nothing
// works around this.
clientSocket.getInputStream().read(new byte[0]);
}
while (clientInputStream.available() <= 0) {
Thread.sleep(10);
}
final ByteArrayOutputStream response = new ByteArrayOutputStream();
// Don't use a StreamCopier because it will block reading the
// input stream.
final byte[] buffer = new byte[100];
final Pattern terminalPattern =
Pattern.compile(terminalExpression != null ? terminalExpression : ".*");
while (true) {
while (clientInputStream.available() > 0) {
final int bytesRead = clientInputStream.read(buffer, 0, buffer.length);
response.write(buffer, 0, bytesRead);
}
// Workaround JRockit bug.
//final String s = response.toString();
final String s = response.toString(System.getProperty("file.encoding"));
final Matcher matcher = terminalPattern.matcher(s);
if (matcher.find()) {
return s;
}
final long RETRIES = 100;
for (int i=0; i<RETRIES && clientInputStream.available() == 0; ++i) {
Thread.sleep(10);
}
if (clientInputStream.available() == 0) {
fail("Stream has been idle for " + (RETRIES * 10/1000d) +
" seconds and the terminal expression '" + terminalExpression +
"' does not match received data:\n" + s);
}
}
}
private void waitUntilAllStreamThreadsStopped(AbstractTCPProxyEngine engine)
throws InterruptedException {
for (int i = 0;
i < 10 && engine.getStreamThreadGroup().activeCount() > 0;
++i) {
Thread.sleep(50);
}
assertEquals("Failed waiting for all stream threads to stop",
0, engine.getStreamThreadGroup().activeCount());
}
private void httpProxyEngineBadRequestTests(AbstractTCPProxyEngine engine)
throws Exception {
final Socket clientSocket =
new Socket(engine.getListenEndPoint().getHost(),
engine.getListenEndPoint().getPort());
final PrintWriter clientWriter =
new PrintWriter(clientSocket.getOutputStream(), true);
final String message = "This is not a valid HTTP message";
clientWriter.print(message);
clientWriter.flush();
final String response = readResponse(clientSocket, null);
AssertUtilities.assertStartsWith(response, "HTTP/1.0 400 Bad Request");
AssertUtilities.assertContainsHeader(response, "Connection", "close");
AssertUtilities.assertContainsHeader(response, "Content-Type", "text/html");
AssertUtilities.assertContains(response, message);
clientSocket.close();
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.assertNoMoreCalls();
verify(m_logger).error(contains("Failed to determine proxy destination"));
final Socket clientSocket2 =
new Socket(engine.getListenEndPoint().getHost(),
engine.getListenEndPoint().getPort());
clientSocket2.shutdownOutput();
try {
readResponse(clientSocket, null);
fail("Expected IOException");
}
catch (IOException e) {
}
clientSocket2.close();
verify(m_logger, timeout(10000).times(2))
.error(contains("Failed to determine proxy destination"));
final Socket clientSocket3 =
new Socket(engine.getListenEndPoint().getHost(),
engine.getListenEndPoint().getPort());
final byte[] hugeBunchOfCrap = new byte[50000];
clientSocket3.getOutputStream().write(hugeBunchOfCrap);
final String response3 = readResponse(clientSocket3, null);
AssertUtilities.assertStartsWith(response3, "HTTP/1.0 400 Bad Request");
AssertUtilities.assertContainsHeader(response3, "Connection", "close");
AssertUtilities.assertContainsHeader(response3, "Content-Type", "text/html");
clientSocket3.close();
waitUntilAllStreamThreadsStopped(engine);
verify(m_logger, timeout(10000))
.error(contains("failed to match HTTP message"));
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.assertNoMoreCalls();
}
private void httpProxyEngineGoodRequestTests(AbstractTCPProxyEngine engine)
throws Exception {
final AcceptAndEcho echoer = new AcceptAndEcho();
final Socket clientSocket =
new Socket(engine.getListenEndPoint().getHost(),
engine.getListenEndPoint().getPort());
final PrintWriter clientWriter =
new PrintWriter(clientSocket.getOutputStream(), true);
final String message0 =
"GET http://" + echoer.getEndPoint() + "/foo HTTP/1.1\r\n" +
"foo: bah\r\n" +
"\r\n" +
"A \u00e0 message";
clientWriter.print(message0);
clientWriter.flush();
final String response0 = readResponse(clientSocket, "message$");
AssertUtilities.assertStartsWith(response0, "GET /foo HTTP/1.1\r\n");
AssertUtilities.assertContainsHeader(response0, "foo", "bah");
AssertUtilities.assertContainsPattern(response0,
"\r\n\r\nA \u00e0 message$");
m_requestFilterStubFactory.assertSuccess("connectionOpened",
ConnectionDetails.class);
m_requestFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.assertSuccess("connectionOpened",
ConnectionDetails.class);
m_responseFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_responseFilterStubFactory.assertNoMoreCalls();
final String message1Headers =
"POST http://" + echoer.getEndPoint() + "/blah?x=123&y=99 HTTP/1.0\r\n" +
"\r\n" +
"Another message";
clientWriter.print(message1Headers);
clientWriter.flush();
final String message1PostBody = "Some data, lah 0x810x820x830x84 dah";
clientWriter.print(message1PostBody);
clientWriter.flush();
final String response1 = readResponse(clientSocket, "dah$");
AssertUtilities.assertStartsWith(response1,
"POST /blah?x=123&y=99 HTTP/1.0\r\n");
AssertUtilities.assertContainsPattern(response1,
"\r\n\r\nAnother message" +
message1PostBody + "$");
// Do again, but force engine to handle body in two parts.
clientWriter.print(message1Headers);
clientWriter.flush();
final String response2a = readResponse(clientSocket, "Another message$");
AssertUtilities.assertStartsWith(response2a,
"POST /blah?x=123&y=99 HTTP/1.0\r\n");
clientWriter.print(message1PostBody);
clientWriter.flush();
final String response2b = readResponse(clientSocket, "dah$");
assertNotNull(response2b);
clientSocket.close();
waitUntilAllStreamThreadsStopped(engine);
assertTrue(m_requestFilterStubFactory.getCallHistory().indexOf(
"connectionClosed") != -1 &&
m_responseFilterStubFactory.getCallHistory().indexOf(
"connectionClosed") != -1);
m_requestFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_requestFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_requestFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
// handle() must have been called at least 3 times, but can be called
// more.
while (m_requestFilterStubFactory.peekFirst() != null &&
m_requestFilterStubFactory.peekFirst().getMethod().getName()
.equals("handle")) {
m_requestFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
}
m_responseFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_responseFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_responseFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
while (m_responseFilterStubFactory.peekFirst() != null &&
m_responseFilterStubFactory.peekFirst().getMethod().getName()
.equals("handle")) {
m_responseFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
}
m_requestFilterStubFactory.assertSuccess("connectionClosed",
ConnectionDetails.class);
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.assertSuccess("connectionClosed",
ConnectionDetails.class);
m_responseFilterStubFactory.assertNoMoreCalls();
}
private void httpsProxyEngineGoodRequestTest(AbstractTCPProxyEngine engine)
throws Exception {
final AcceptAndEcho echoer = new SSLAcceptAndEcho();
final Socket clientPlainSocket =
new Socket(engine.getListenEndPoint().getHost(),
engine.getListenEndPoint().getPort());
final PrintWriter clientWriter =
new PrintWriter(clientPlainSocket.getOutputStream(), true);
clientWriter.print("CONNECT " + echoer.getEndPoint() + "\r\n\r\n");
clientWriter.flush();
final String response = readResponse(clientPlainSocket, "Proxy-agent");
AssertUtilities.assertStartsWith(response, "HTTP/1.0 200 OK\r\n");
AssertUtilities.assertContainsHeader(response,
"Proxy-agent",
"The Grinder.*");
final Socket clientSSLSocket =
m_sslSocketFactory.createClientSocket(clientPlainSocket,
echoer.getEndPoint());
final PrintWriter secureClientWriter =
new PrintWriter(clientSSLSocket.getOutputStream(), true);
// No URL decoration should take place. Feed an absolute URL
// to be difficult.
final String message0 =
"GET http://galafray/foo HTTP/1.1\r\n" +
"foo: bah\r\n" +
"\r\n" +
"A \u00e0 message";
secureClientWriter.print(message0);
secureClientWriter.flush();
final String response0 = readResponse(clientSSLSocket, "message$");
AssertUtilities.assertStartsWith(response0,
"GET http://galafray/foo HTTP/1.1\r\n");
AssertUtilities.assertContainsHeader(response0, "foo", "bah");
AssertUtilities.assertContainsPattern(response0,
"\r\n\r\nA \u00e0 message$");
m_requestFilterStubFactory.assertSuccess("connectionOpened",
ConnectionDetails.class);
m_requestFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.assertSuccess("connectionOpened",
ConnectionDetails.class);
m_responseFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_responseFilterStubFactory.assertNoMoreCalls();
clientSSLSocket.close();
waitUntilAllStreamThreadsStopped(engine);
m_requestFilterStubFactory.waitUntilCalled(5000);
m_requestFilterStubFactory.assertSuccess("connectionClosed",
ConnectionDetails.class);
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.waitUntilCalled(5000);
m_responseFilterStubFactory.assertSuccess("connectionClosed",
ConnectionDetails.class);
m_responseFilterStubFactory.assertNoMoreCalls();
}
@Test public void testHTTPProxyEngine() throws Exception {
final AbstractTCPProxyEngine engine =
new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
m_requestFilter,
m_responseFilter,
m_logger,
m_localEndPoint,
false,
100000,
null,
null);
final Thread engineThread = new Thread(engine, "Run engine");
engineThread.start();
m_responseFilterStubFactory.assertNoMoreCalls();
m_requestFilterStubFactory.assertNoMoreCalls();
assertEquals(m_localEndPoint, engine.getListenEndPoint());
assertNotNull(engine.getSocketFactory());
m_requestFilterStubFactory.assertIsWrappedBy(engine.getRequestFilter());
m_responseFilterStubFactory.assertIsWrappedBy(engine.getResponseFilter());
assertEquals(TerminalColour.NONE, engine.getRequestColour());
assertEquals(TerminalColour.NONE, engine.getResponseColour());
m_requestFilterStubFactory.resetCallHistory();
m_responseFilterStubFactory.resetCallHistory();
httpProxyEngineBadRequestTests(engine);
httpProxyEngineGoodRequestTests(engine);
httpsProxyEngineGoodRequestTest(engine);
engine.stop();
engineThread.join();
// Stopping engine again doesn't do anything.
engine.stop();
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.assertNoMoreCalls();
}
@Test public void testColourHTTPProxyEngine() throws Exception {
final AbstractTCPProxyEngine engine =
new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
m_requestFilter,
m_responseFilter,
m_logger,
m_localEndPoint,
true,
100000,
null,
null);
final Thread engineThread = new Thread(engine, "Run engine");
engineThread.start();
m_responseFilterStubFactory.assertNoMoreCalls();
m_requestFilterStubFactory.assertNoMoreCalls();
assertEquals(m_localEndPoint, engine.getListenEndPoint());
assertNotNull(engine.getSocketFactory());
m_requestFilterStubFactory.assertIsWrappedBy(engine.getRequestFilter());
m_responseFilterStubFactory.assertIsWrappedBy(engine.getResponseFilter());
assertEquals(TerminalColour.RED, engine.getRequestColour());
assertEquals(TerminalColour.BLUE, engine.getResponseColour());
m_requestFilterStubFactory.resetCallHistory();
m_responseFilterStubFactory.resetCallHistory();
httpProxyEngineBadRequestTests(engine);
httpProxyEngineGoodRequestTests(engine);
engine.stop();
engineThread.join();
// Stopping engine again doesn't do anything.
engine.stop();
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.assertNoMoreCalls();
}
@Test public void testWithChainedHTTPProxy() throws Exception {
final AcceptAndEcho echoer = new AcceptAndEcho();
final EndPoint chainedProxyEndPoint = createFreeLocalEndPoint();
final AbstractTCPProxyEngine chainedProxy =
new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
m_requestFilter,
m_responseFilter,
m_logger,
chainedProxyEndPoint,
true,
100000,
null,
null);
final Thread chainedProxyThread =
new Thread(chainedProxy, "Run chained proxy engine");
chainedProxyThread.start();
final AbstractTCPProxyEngine engine =
new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
new NullFilter(),
new NullFilter(),
m_logger,
m_localEndPoint,
true,
100000,
chainedProxyEndPoint,
null);
final Thread engineThread = new Thread(engine, "Run engine");
engineThread.start();
m_requestFilterStubFactory.resetCallHistory();
m_responseFilterStubFactory.resetCallHistory();
final Socket clientSocket =
new Socket(engine.getListenEndPoint().getHost(),
engine.getListenEndPoint().getPort());
final PrintWriter clientWriter =
new PrintWriter(clientSocket.getOutputStream(), true);
final String message0 =
"GET http://" + echoer.getEndPoint() + "/ HTTP/1.1\r\n" +
"foo: bah\r\n" +
"\r\n" +
"Proxy me";
clientWriter.print(message0);
clientWriter.flush();
final String response0 = readResponse(clientSocket, "Proxy me$");
AssertUtilities.assertStartsWith(response0, "GET / HTTP/1.1\r\n");
AssertUtilities.assertContainsHeader(response0, "foo", "bah");
AssertUtilities.assertContainsPattern(response0, "\r\n\r\nProxy me$");
m_requestFilterStubFactory.assertSuccess("connectionOpened",
ConnectionDetails.class);
m_requestFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.assertSuccess("connectionOpened",
ConnectionDetails.class);
m_responseFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_responseFilterStubFactory.assertNoMoreCalls();
chainedProxy.stop();
chainedProxyThread.join();
engine.stop();
engineThread.join();
waitUntilAllStreamThreadsStopped(engine);
m_requestFilterStubFactory.assertSuccess(
"connectionClosed", ConnectionDetails.class);
m_responseFilterStubFactory.assertSuccess(
"connectionClosed", ConnectionDetails.class);
// Stopping engine again doesn't do anything.
engine.stop();
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.assertNoMoreCalls();
}
@Test public void testWithChainedHTTPSProxy() throws Exception {
final AcceptAndEcho echoer = new SSLAcceptAndEcho();
final EndPoint chainedProxyEndPoint = createFreeLocalEndPoint();
final AbstractTCPProxyEngine chainedProxy =
new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
m_requestFilter,
m_responseFilter,
m_logger,
chainedProxyEndPoint,
true,
100000,
null,
null);
final Thread chainedProxyThread =
new Thread(chainedProxy, "Run chained proxy engine");
chainedProxyThread.start();
final AbstractTCPProxyEngine engine =
new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
new NullFilter(),
new NullFilter(),
m_logger,
m_localEndPoint,
true,
100000,
null,
chainedProxyEndPoint);
final Thread engineThread = new Thread(engine, "Run engine");
engineThread.start();
m_requestFilterStubFactory.resetCallHistory();
m_responseFilterStubFactory.resetCallHistory();
final Socket clientPlainSocket =
new Socket(engine.getListenEndPoint().getHost(),
engine.getListenEndPoint().getPort());
final PrintWriter clientWriter =
new PrintWriter(clientPlainSocket.getOutputStream(), true);
clientWriter.print("CONNECT " + echoer.getEndPoint() + "\r\n\r\n");
clientWriter.flush();
final String response = readResponse(clientPlainSocket, "Proxy-agent");
AssertUtilities.assertStartsWith(response, "HTTP/1.0 200 OK\r\n");
AssertUtilities.assertContainsHeader(response,
"Proxy-agent",
"The Grinder.*");
final Socket clientSSLSocket =
m_sslSocketFactory.createClientSocket(clientPlainSocket,
echoer.getEndPoint());
final PrintWriter secureClientWriter =
new PrintWriter(clientSSLSocket.getOutputStream(), true);
// No URL decoration should take place. Feed an absolute URL
// to be difficult.
final String message0 =
"GET http://galafray/foo HTTP/1.1\r\n" +
"foo: bah\r\n" +
"\r\n" +
"A \u00e0 message";
secureClientWriter.print(message0);
secureClientWriter.flush();
final String response0 = readResponse(clientSSLSocket, "message$");
AssertUtilities.assertStartsWith(response0,
"GET http://galafray/foo HTTP/1.1\r\n");
AssertUtilities.assertContainsHeader(response0, "foo", "bah");
AssertUtilities.assertContainsPattern(response0,
"\r\n\r\nA \u00e0 message$");
m_requestFilterStubFactory.assertSuccess("connectionOpened",
ConnectionDetails.class);
m_requestFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.assertSuccess("connectionOpened",
ConnectionDetails.class);
m_responseFilterStubFactory.assertSuccess("handle",
ConnectionDetails.class,
new byte[0].getClass(),
Integer.class);
m_responseFilterStubFactory.assertNoMoreCalls();
engine.stop();
engineThread.join();
chainedProxy.stop();
chainedProxyThread.join();
waitUntilAllStreamThreadsStopped(engine);
m_requestFilterStubFactory.setIgnoreCallOrder(true);
m_responseFilterStubFactory.setIgnoreCallOrder(true);
m_requestFilterStubFactory.assertSuccess(
"connectionClosed", ConnectionDetails.class);
m_responseFilterStubFactory.assertSuccess(
"connectionClosed", ConnectionDetails.class);
// Sometimes log an SSL exception when shutting down.
// m_loggerStubFactory.assertNoMoreCalls();
// Stopping engine or filter again doesn't do anything.
engine.stop();
m_requestFilterStubFactory.assertNoMoreCalls();
m_responseFilterStubFactory.assertNoMoreCalls();
}
@Test public void testStopWithBlockedFilterThreads() throws Exception {
final AcceptAndEcho echoer = new AcceptAndEcho();
// I wanted to implement this test so that a filter thread blocked
// on some hung connection. However, it's hard to simulate a connection
// timeout in a single JVM; I thought it could be done by connected to a
// socket that never accepted but the client side of the connection was
// established just fine. Instead, we use an evil filter that blocks
// when it recevies the connection opened event.
final AbstractTCPProxyEngine engine =
new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
new HungFilter(),
m_responseFilter,
m_logger,
m_localEndPoint,
false,
100000,
null,
null);
final Thread engineThread = new Thread(engine, "Run engine");
engineThread.start();
final ServerSocket serverSocket = new ServerSocket(0);
final Socket clientSocket =
new Socket(engine.getListenEndPoint().getHost(),
engine.getListenEndPoint().getPort());
final PrintWriter clientWriter =
new PrintWriter(clientSocket.getOutputStream(), true);
final String message0 =
"GET http://" + echoer.getEndPoint() + "/foo HTTP/1.1\r\n" +
"foo: bah\r\n" +
"\r\n" +
"A \u00e0 message";
clientWriter.print(message0);
clientWriter.flush();
// Wait until the filter thread is spinning so that there's
// a good chancce it's hung.
for (int i = 0;
i < 10 && engine.getStreamThreadGroup().activeCount() != 1;
++i) {
Thread.sleep(50);
}
assertEquals(1, engine.getStreamThreadGroup().activeCount());
engine.stop();
engineThread.join();
serverSocket.close();
waitUntilAllStreamThreadsStopped(engine);
}
private static class HungFilter implements TCPProxyFilter {
public void connectionClosed(ConnectionDetails connectionDetails)
throws FilterException {
}
public void connectionOpened(ConnectionDetails connectionDetails)
throws FilterException {
try {
synchronized (this) {
wait();
}
}
catch (InterruptedException e) {
throw new UncheckedInterruptedException(e);
}
}
public byte[] handle(ConnectionDetails connectionDetails,
byte[] buffer,
int bytesRead) throws FilterException {
return null;
}
}
private class AcceptAndEcho implements Runnable {
private final ServerSocket m_serverSocket;
public AcceptAndEcho() throws IOException {
this(new ServerSocket(0));
}
protected AcceptAndEcho(ServerSocket serverSocket) throws IOException {
m_serverSocket = serverSocket;
new Thread(this, getClass().getName()).start();
m_echoers.add(this);
}
public EndPoint getEndPoint() {
return EndPoint.serverEndPoint(m_serverSocket);
}
public void run() {
try {
while (true) {
final Socket socket = m_serverSocket.accept();
new Thread(
new StreamCopier(1000, true).getRunnable(socket.getInputStream(),
socket.getOutputStream()),
"Echo thread").start();
}
}
catch (SocketException e) {
// Ignore - probably shutdown.
}
catch (IOException e) {
fail("Got a " + e.getClass());
}
}
public void shutdown() throws IOException {
m_serverSocket.close();
}
}
private class SSLAcceptAndEcho extends AcceptAndEcho {
public SSLAcceptAndEcho() throws IOException {
super(
m_sslSocketFactory.createServerSocket(createFreeLocalEndPoint(), 0));
}
}
}