summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'test/com/isode/stroke/network/HTTPConnectProxiedConnectionTest.java')
-rw-r--r--test/com/isode/stroke/network/HTTPConnectProxiedConnectionTest.java477
1 files changed, 477 insertions, 0 deletions
diff --git a/test/com/isode/stroke/network/HTTPConnectProxiedConnectionTest.java b/test/com/isode/stroke/network/HTTPConnectProxiedConnectionTest.java
new file mode 100644
index 0000000..1f228dc
--- /dev/null
+++ b/test/com/isode/stroke/network/HTTPConnectProxiedConnectionTest.java
@@ -0,0 +1,477 @@
+/* Copyright (c) 2016, Isode Limited, London, England.
+ * All rights reserved.
+ *
+ * Acquisition and use of this software and related materials for any
+ * purpose requires a written license agreement from Isode Limited,
+ * or a written license from an organisation licensed by Isode Limited
+ * to grant such a license.
+ *
+ */
+package com.isode.stroke.network;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Vector;
+import java.util.logging.Logger;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.isode.stroke.base.ByteArray;
+import com.isode.stroke.base.SafeByteArray;
+import com.isode.stroke.eventloop.Event.Callback;
+import com.isode.stroke.eventloop.DummyEventLoop;
+import com.isode.stroke.eventloop.EventLoop;
+import com.isode.stroke.network.Connection.Error;
+import com.isode.stroke.network.HTTPConnectProxiedConnection.Pair;
+import com.isode.stroke.signals.Slot1;
+
+/**
+ * Tests for {@link HTTPConnectProxiedConnection}
+ */
+public class HTTPConnectProxiedConnectionTest {
+
+ private final String proxyHost = "doo.bah";
+ private final int proxyPort = 1234;
+ private final HostAddressPort proxyHostAddress = new HostAddressPort(new HostAddress("1.1.1.1"), proxyPort);
+ private final HostAddressPort host = new HostAddressPort(new HostAddress("2.2.2.2"), 2345);
+ private final DummyEventLoop eventLoop = new DummyEventLoop();
+ private final StaticDomainNameResolver resolver = new StaticDomainNameResolver(eventLoop);
+ private final MockConnectionFactory connectionFactory = new MockConnectionFactory(eventLoop);
+ private final TimerFactory timerFactory = new DummyTimerFactory();
+ private boolean connectFinished = false;
+ private boolean connectFinishedWithError = false;
+ private boolean disconnected = false;
+ private Connection.Error disconnectedError = null;
+ private final ByteArray dataRead = new ByteArray();
+
+ private static Logger logger =
+ Logger.getLogger(HTTPConnectProxiedConnectionTest.class.getName());
+
+ @Before
+ public void setUp() {
+ resolver.addAddress(proxyHost, proxyHostAddress.getAddress());
+ }
+
+ @Test
+ public void testConnect_CreatesConnectionToProxy() {
+ HTTPConnectProxiedConnection testling = createTestling();
+
+ connect(testling, host);
+
+ assertEquals(1,connectionFactory.connections.size());
+ assertNotNull(connectionFactory.connections.get(0).hostAddressPort);
+ assertEquals(proxyHostAddress,connectionFactory.connections.get(0).hostAddressPort);
+ assertFalse(connectFinished);
+ }
+
+ @Test
+ public void testConnect_SendsConnectRequest() {
+ HTTPConnectProxiedConnection testling = createTestling();
+
+ connect(testling, new HostAddressPort(new HostAddress("2.2.2.2"), 2345));
+
+ assertEquals(new ByteArray("CONNECT 2.2.2.2:2345 HTTP/1.1\r\n\r\n"),
+ connectionFactory.connections.get(0).dataWritten);
+ }
+
+ @Test
+ public void testConnect_ReceiveConnectResponse() {
+ HTTPConnectProxiedConnection testling = createTestling();
+ connect(testling, new HostAddressPort(new HostAddress("2.2.2.2"), 2345));
+
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray("HTTP/1.0 200 Connection established\r\n\r\n"));
+ eventLoop.processEvents();
+
+ assertTrue(connectFinished);
+ assertFalse(connectFinishedWithError);
+ assertTrue(dataRead.isEmpty());
+ }
+
+ @Test
+ public void testConnect_ReceiveConnectChunkedResponse() {
+ HTTPConnectProxiedConnection testling = createTestling();
+ connect(testling, new HostAddressPort(new HostAddress("2.2.2.2"), 2345));
+
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray("HTTP/1.0 "));
+ eventLoop.processEvents();
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray("200 Connection established\r\n\r\n"));
+ eventLoop.processEvents();
+
+ assertTrue(connectFinished);
+ assertFalse(connectFinishedWithError);
+ assertTrue(dataRead.isEmpty());
+ }
+
+ @Test
+ public void testConnect_ReceiveMalformedConnectResponse() {
+ HTTPConnectProxiedConnection testling = createTestling();
+ connect(testling, new HostAddressPort(new HostAddress("2.2.2.2"), 2345));
+
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray("FLOOP"));
+ eventLoop.processEvents();
+
+ assertTrue(connectFinished);
+ assertTrue(connectFinishedWithError);
+ assertTrue(connectionFactory.connections.get(0).disconnected);
+ }
+
+ @Test
+ public void testConnect_ReceiveErrorConnectResponse() {
+ HTTPConnectProxiedConnection testling = createTestling();
+ connect(testling, new HostAddressPort(new HostAddress("2.2.2.2"), 2345));
+
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray("HTTP/1.0 401 Unauthorized\r\n\r\n"));
+ eventLoop.processEvents();
+
+ assertTrue(connectFinished);
+ assertTrue(connectFinishedWithError);
+ assertTrue(connectionFactory.connections.get(0).disconnected);
+ }
+
+ @Test
+ public void testConnect_ReceiveDataAfterConnect() {
+ HTTPConnectProxiedConnection testling = createTestling();
+ connect(testling, new HostAddressPort(new HostAddress("2.2.2.2"), 2345));
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray("HTTP/1.0 200 Connection established\r\n\r\n"));
+ eventLoop.processEvents();
+
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray("abcdef"));
+
+ assertEquals(new ByteArray("abcdef"),dataRead);
+ }
+
+ @Test
+ public void testWrite_AfterConnect() {
+ HTTPConnectProxiedConnection testling = createTestling();
+ connect(testling, new HostAddressPort(new HostAddress("2.2.2.2"), 2345));
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray("HTTP/1.0 200 Connection established\r\n\r\n"));
+ eventLoop.processEvents();
+ connectionFactory.connections.get(0).dataWritten.clear();
+
+ testling.write(new SafeByteArray("abcdef"));
+
+ assertEquals(new ByteArray("abcdef"),connectionFactory.connections.get(0).dataWritten);
+ }
+
+ @Test
+ public void testDisconnect_AfterConnectRequest() {
+ HTTPConnectProxiedConnection testling = createTestling();
+ connect(testling, new HostAddressPort(new HostAddress("2.2.2.2"), 2345));
+
+ testling.disconnect();
+
+ assertTrue(connectionFactory.connections.get(0).disconnected);
+ assertTrue(disconnected);
+ assertNull(disconnectedError);
+ }
+
+ @Test
+ public void testDisconnect_AfterConnect() {
+ HTTPConnectProxiedConnection testling = createTestling();
+ connect(testling, new HostAddressPort(new HostAddress("2.2.2.2"), 2345));
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray("HTTP/1.0 200 Connection established\r\n\r\n"));
+ eventLoop.processEvents();
+
+ testling.disconnect();
+
+ assertTrue(connectionFactory.connections.get(0).disconnected);
+ assertTrue(disconnected);
+ assertNull(disconnectedError);
+ }
+
+ @Test
+ public void testTrafficFilter() {
+ HTTPConnectProxiedConnection testling = createTestling();
+
+ ExampleHTTPTrafficFilter httpTrafficFilter = new ExampleHTTPTrafficFilter();
+
+ testling.setHTTPTrafficFilter(httpTrafficFilter);
+ connect(testling, new HostAddressPort(new HostAddress("2.2.2.2"), 2345));
+
+ // set a default response so the server response is answered by the traffic filter
+ httpTrafficFilter.filterResponseReturn.clear();
+ httpTrafficFilter.filterResponseReturn.add(new Pair("Authorization", "Negotiate a87421000492aa874209af8bc028"));
+
+ connectionFactory.connections.get(0).dataWritten.clear();
+
+ // test chunked response
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray("HTTP/1.0 401 Unauthorized\r\n"));
+ eventLoop.processEvents();
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray("WWW-Authenticate: Negotiate\r\n\r\n"));
+ eventLoop.processEvents();
+
+
+ // verify that the traffic filter got called and answered with its response
+ assertEquals(1,httpTrafficFilter.filterResponses.size());
+ assertEquals("WWW-Authenticate",httpTrafficFilter.filterResponses.get(0).get(0).a);
+
+ // remove the default response from the traffic filter
+ httpTrafficFilter.filterResponseReturn.clear();
+ eventLoop.processEvents();
+
+ // verify that the traffic filter answer is send over the wire
+ assertEquals(new ByteArray("CONNECT 2.2.2.2:2345 HTTP/1.1\r\nAuthorization: Negotiate a87421000492aa874209af8bc028\r\n\r\n"), connectionFactory.connections.get(1).dataWritten);
+
+ // verify that after without the default response, the traffic filter is skipped, authentication proceeds and traffic goes right through
+ connectionFactory.connections.get(1).dataWritten.clear();
+ testling.write(new SafeByteArray("abcdef"));
+ assertEquals(new ByteArray("abcdef"), connectionFactory.connections.get(1).dataWritten);
+ }
+
+ @Test
+ public void testTrafficFilterNoConnectionReuse() {
+ HTTPConnectProxiedConnection testling = createTestling();
+
+ ProxyAuthenticationHTTPTrafficFilter httpTrafficFilter = new ProxyAuthenticationHTTPTrafficFilter();
+ testling.setHTTPTrafficFilter(httpTrafficFilter);
+
+
+ connect(testling, new HostAddressPort(new HostAddress("2.2.2.2"), 2345));
+
+ // First HTTP CONNECT request assumes the proxy will work.
+ assertEquals(new ByteArray("CONNECT 2.2.2.2:2345 HTTP/1.1\r\n\r\n"),
+ connectionFactory.connections.get(0).dataWritten);
+
+ // First reply presents initiator with authentication options.
+ connectionFactory.connections.get(0).onDataRead.emit(new SafeByteArray(
+ "HTTP/1.0 407 ProxyAuthentication Required\r\n"
+ +"proxy-Authenticate: Negotiate\r\n"
+ +"Proxy-Authenticate: Kerberos\r\n"
+ +"proxy-Authenticate: NTLM\r\n"
+ +"\r\n"));
+ eventLoop.processEvents();
+ assertFalse(connectFinished);
+ assertFalse(connectFinishedWithError);
+
+ // The HTTP proxy responds with code 407, so the traffic filter should inject the authentication response on a new connection.
+ assertEquals(new ByteArray("CONNECT 2.2.2.2:2345 HTTP/1.1\r\nProxy-Authorization: "
+ + "NTLM TlRMTVNTUAABAAAAt7II4gkACQAxAAAACQAJACgAAAVNTUAADAAFASgKAAAAD0"
+ + "xBQlNNT0tFM1dPUktHUk9VUA==\r\n\r\n"),
+ connectionFactory.connections.get(1).dataWritten);
+
+ // The proxy responds with another authentication step.
+ connectionFactory.connections.get(1).onDataRead.emit(new SafeByteArray(
+ "HTTP/1.0 407 ProxyAuthentication Required\r\n"
+ +"Proxy-Authenticate: NTLM TlRMTVNTUAACAAAAEAAQADgAAAA1goriluCDYHcYI/sAAAAAAAAAA"
+ + "FQAVABIAAAABQLODgAAAA9TAFAASQBSAEkAVAAxAEIAAgAQAFMAUABJAFIASQBUADEAQgABABAAUw"
+ + "BQAEkAUgBJAFQAMQBCAAQAEABzAHAAaQByAGkAdAAxAGIAAwAQAHMAcABpAHIAaQB0ADEAYgAAAAAA"
+ + "\r\n\r\n"));
+ eventLoop.processEvents();
+ assertFalse(connectFinished);
+ assertFalse(connectFinishedWithError);
+
+ // Last HTTP request that should succeed. Further traffic will go over the connection of this request.
+ assertEquals(new ByteArray("CONNECT 2.2.2.2:2345 HTTP/1.1\r\nProxy-Authorization: "
+ + "NTLM TlRMTVNTUAADAAAAGAAYAHIAAAAYABgAigAAABIAEgBIAAAABgAGAFoAAAASABIVNT"
+ + "UAADAAYAAAABAAEACiAAAANYKI4gUBKAoAAAAPTABBAEIAUwBNAE8ASwBFADMAXwBxAGEAT"
+ + "ABBAEIAUwBNAE8ASwBFADMA0NKq8HYYhj8AAAAAAAAAAAAAAAAAAAAAOIiih3mR+AkyM4r99"
+ + "sy1mdFonCu2ILODro1WTTrJ4b4JcXEzUBA2Ig==\r\n\r\n"),
+ connectionFactory.connections.get(2).dataWritten);
+
+ connectionFactory.connections.get(2).onDataRead.emit(new SafeByteArray("HTTP/1.0 200 OK"
+ + "\r\n\r\n"));
+ eventLoop.processEvents();
+
+ // The HTTP CONNECT proxy initialization finished without error.
+ assertTrue(connectFinished);
+ assertFalse(connectFinishedWithError);
+
+ // Further traffic is written directly, without interception of the filter.
+ connectionFactory.connections.get(2).dataWritten.clear();
+ testling.write(new SafeByteArray("This is some basic data traffic."));
+ assertEquals(new ByteArray("This is some basic data traffic."),
+ connectionFactory.connections.get(2).dataWritten);
+ }
+
+ private void connect(HTTPConnectProxiedConnection connection, HostAddressPort to) {
+ connection.connect(to);
+ eventLoop.processEvents();
+ eventLoop.processEvents();
+ eventLoop.processEvents();
+ }
+
+ private HTTPConnectProxiedConnection createTestling() {
+ HTTPConnectProxiedConnection connection = HTTPConnectProxiedConnection.create(resolver,
+ connectionFactory, timerFactory, proxyHost, proxyPort,
+ new SafeByteArray(""), new SafeByteArray(""));
+ connection.onConnectFinished.connect(new Slot1<Boolean>() {
+
+ @Override
+ public void call(Boolean hadError) {
+ handleConnectFinished(hadError.booleanValue());
+ }
+
+ });
+ connection.onDisconnected.connect(new Slot1<Connection.Error>() {
+
+ @Override
+ public void call(Error error) {
+ handleDisconnected(error);
+ }
+
+ });
+ connection.onDataRead.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray data) {
+ handleDataRead(data);
+ }
+
+ });
+ return connection;
+ }
+
+ private void handleConnectFinished(boolean hadError) {
+ connectFinished = true;
+ connectFinishedWithError = hadError;
+ }
+
+ private void handleDisconnected(Connection.Error error) {
+ disconnected = true;
+ disconnectedError = error;
+ }
+
+ private void handleDataRead(SafeByteArray data) {
+ dataRead.append(data);
+ }
+
+ private static class ExampleHTTPTrafficFilter implements HTTPTrafficFilter {
+
+ @Override
+ public Vector<Pair> filterHTTPResponseHeader(String statusLine, Vector<Pair> response) {
+ filterResponses.add(response);
+ logger.fine("\n");
+ return filterResponseReturn;
+ }
+
+ private Vector<Vector<Pair>> filterResponses = new Vector<Vector<Pair>>();
+
+ private Vector<Pair> filterResponseReturn = new Vector<Pair>();
+
+ }
+
+ public static class ProxyAuthenticationHTTPTrafficFilter implements HTTPTrafficFilter {
+
+ @Override
+ public Vector<Pair> filterHTTPResponseHeader(String statusLine, Vector<Pair> response) {
+ Vector<Pair> filterResponseReturn = new Vector<Pair>();
+ String[] rawStatusLineFields = statusLine.split("\\s+");
+ Vector<String> statusLineFields = new Vector(Arrays.asList(rawStatusLineFields));
+
+ int statusCode = Integer.valueOf(statusLineFields.get(1));
+ if (statusCode == 407) {
+ for (Pair field : response) {
+ if ("Proxy-Authenticate".equalsIgnoreCase(field.a)) {
+ if (field.b.length() >= 6 && field.b.startsWith(" NTLM ")) {
+ filterResponseReturn.add(new Pair("Proxy-Authorization",
+ "NTLM TlRMTVNTUAADAAAAGAAYAHIAAAAYABgAigAAABIAEgBIAAAABgAGAFo"
+ + "AAAASABIVNTUAADAAYAAAABAAEACiAAAANYKI4gUBKAoAAAAPTABBAEIAU"
+ + "wBNAE8ASwBFADMAXwBxAGEATABBAEIAUwBNAE8ASwBFADMA0NKq8HYYhj"
+ + "8AAAAAAAAAAAAAAAAAAAAAOIiih3mR+AkyM4r99sy1mdFonCu2ILODro1W"
+ + "TTrJ4b4JcXEzUBA2Ig=="));
+ return filterResponseReturn;
+ }
+ else if (field.b.length() >= 5 && field.b.startsWith(" NTLM")) {
+ filterResponseReturn.add(new Pair("Proxy-Authorization",
+ "NTLM TlRMTVNTUAABAAAAt7II4gkACQAxAAAACQAJACgAAAVNTUAADAAFASg"
+ + "KAAAAD0xBQlNNT0tFM1dPUktHUk9VUA=="));
+ return filterResponseReturn;
+ }
+ }
+ }
+
+ return filterResponseReturn;
+ }
+ else {
+ return new Vector<Pair>();
+ }
+ }
+
+ }
+
+ private static class MockConnection extends Connection {
+
+ private final EventLoop eventLoop;
+ private HostAddressPort hostAddressPort = null;
+ private final List<HostAddressPort> failingPorts;
+ private final ByteArray dataWritten = new ByteArray();
+ private boolean disconnected = false;
+
+ private MockConnection(Collection<? extends HostAddressPort> failingPorts,
+ EventLoop eventLoop) {
+ this.eventLoop = eventLoop;
+ this.failingPorts = new ArrayList<HostAddressPort>(failingPorts);
+ }
+
+ @Override
+ public void listen() {
+ fail();
+ }
+
+ @Override
+ public void connect(HostAddressPort address) {
+ hostAddressPort = address;
+ final boolean fail = failingPorts.contains(address);
+ eventLoop.postEvent(new Callback() {
+
+ @Override
+ public void run() {
+ onConnectFinished.emit(fail);
+ }
+
+ });
+ }
+
+ @Override
+ public void disconnect() {
+ disconnected = true;
+ onDisconnected.emit(null);
+ }
+
+ @Override
+ public void write(SafeByteArray data) {
+ dataWritten.append(data);
+ }
+
+ @Override
+ public HostAddressPort getLocalAddress() {
+ return new HostAddressPort();
+ }
+
+ public HostAddressPort getRemoteAddress() {
+ return new HostAddressPort();
+ }
+
+ }
+
+ private static class MockConnectionFactory implements ConnectionFactory {
+
+ private final EventLoop eventLoop;
+ private final List<MockConnection> connections = new ArrayList<MockConnection>();
+ private final List<HostAddressPort> failingPorts = new ArrayList<HostAddressPort>();
+
+ private MockConnectionFactory(EventLoop eventLoop) {
+ this.eventLoop = eventLoop;
+ }
+
+ @Override
+ public Connection createConnection() {
+ MockConnection connection = new MockConnection(failingPorts, eventLoop);
+ connections.add(connection);
+ logger.fine("New connection created\n");
+ return connection;
+ }
+
+ }
+
+}