From d636d68c84229c82ff746c7697d2014ff4dd4477 Mon Sep 17 00:00:00 2001
From: Alex Clayton <alex.clayton@isode.com>
Date: Mon, 22 Feb 2016 16:05:37 +0000
Subject: Finish porting on Network Package

As per PortingProgress.txt finsh porting all the classes I can from the network
package.  This involved some updates as the tests and code had changed since
they existing classes had been imported.

I have added notes for the classes I did not port in PortingProgress explaining
why they were not ported.

Test-information:

All unit tests pass.

Change-Id: Ibb52ae409f1da9b72a4c1e590cd22835a1be95eb

diff --git a/PortingProgress.txt b/PortingProgress.txt
index 0f798e5..7a8a986 100644
--- a/PortingProgress.txt
+++ b/PortingProgress.txt
@@ -133,14 +133,22 @@ Network:
 
 All files ported to 6ca201d0b48f4273e24dd7bff17c4a46eeaddf39 except for:
 
-GConfProxyProvider, UnixProxyProvider, WindowsProxyProvider, MacOSXProxyProvider -- Not Yet Ported!
-NetworkEnvironment, SolarisNetworkEnvironment, UnixNetworkEnvironment, WindowsNetworkEnvironment -- Not Yet Ported!
-HTTPConnectProxiedConnectionTest -- Not Yet Ported!
-HostNameOrAddress -- Not Yet Ported!
-PlatformNATTraversalWorker, MiniUPnPInterface, NATPMPInterface -- Not Yet Ported!
-PlatformDomainNameAddressQuery -- Not Yet Ported!
-PlatformDomainNameServiceQuery -- Constructor needs change.
-UnboundDomainNameResolver -- Not Yet Ported!
+GConfProxyProvider, UnixProxyProvider, WindowsProxyProvider, MacOSXProxyProvider
+-- No need to port.  We already have a JavaProxyProvider.
+
+SolarisNetworkEnvironment, UnixNetworkEnvironment, WindowsNetworkEnvironment, PlatformNATTraversalWorker 
+-- No need to port.  A JavaNetworkEnviroment has been implemented.
+
+HostNameOrAddress -- No need to port. Just a utiltity method to allow .toString to be called on something
+that is either HostName or a String.  We can do this in java with Object.toString().
+
+MiniUPnPInterface, NATPMPInterface -- Not yet ported. These are difficult to import, we are using libminiupnpc which we do not have
+a java equivalent for?
+
+PlatformDomainNameServiceQuery -- Constructor needs change.  Swiften version has an extra field required in constructor that is used
+for swiften implementation but not in stroke so this is not needed.
+
+UnboundDomainNameResolver -- Not yet ported, uses unbound Library which we do not have a java equivalent for?  
 
 -----
 Parser:
diff --git a/src/com/isode/stroke/network/HTTPConnectProxiedConnection.java b/src/com/isode/stroke/network/HTTPConnectProxiedConnection.java
index a85758c..d2915bd 100644
--- a/src/com/isode/stroke/network/HTTPConnectProxiedConnection.java
+++ b/src/com/isode/stroke/network/HTTPConnectProxiedConnection.java
@@ -4,7 +4,7 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 /*
- * Copyright (c) 2011-2015 Isode Limited.
+ * Copyright (c) 2011-2016 Isode Limited.
  * All rights reserved.
  * See the COPYING file for more information.
  */
@@ -19,6 +19,8 @@ package com.isode.stroke.network;
 import com.isode.stroke.base.SafeByteArray;
 import com.isode.stroke.stringcodecs.Base64;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Scanner;
 import java.util.Vector;
 
@@ -28,6 +30,7 @@ public class HTTPConnectProxiedConnection extends ProxiedConnection {
 	private SafeByteArray authPassword_;
 	private HTTPTrafficFilter trafficFilter_;
 	private StringBuffer httpResponseBuffer_ = new StringBuffer("");
+	private final List<Pair> nextHTTPRequestHeaders_ = new ArrayList<Pair>();
 
 	public static class Pair {
 		String a;
@@ -51,6 +54,8 @@ public class HTTPConnectProxiedConnection extends ProxiedConnection {
 	}
 
 	protected void initializeProxy() {
+	    httpResponseBuffer_.setLength(0);
+	    
 		StringBuffer connect = new StringBuffer();
 		connect.append("CONNECT ").append(getServer().getAddress().toString()).append(":").append(getServer().getPort()).append(" HTTP/1.1\r\n");
 		SafeByteArray data = new SafeByteArray(connect.toString());
@@ -62,8 +67,16 @@ public class HTTPConnectProxiedConnection extends ProxiedConnection {
 			data.append(Base64.encode(credentials));
 			data.append(new SafeByteArray("\r\n"));
 		}
+		else if (!nextHTTPRequestHeaders_.isEmpty()) {
+		    for (Pair headerField : nextHTTPRequestHeaders_) {
+		        data.append(headerField.a);
+		        data.append(": ");
+		        data.append(headerField.b);
+		        data.append("\r\n");
+		    }
+		    nextHTTPRequestHeaders_.clear();
+		}
 		data.append(new SafeByteArray("\r\n"));
-		//SWIFT_LOG(debug) << "HTTP Proxy send headers: " << byteArrayToString(ByteArray(data.begin(), data.end())) << std::endl;
 		write(data);
 	}
 
@@ -85,12 +98,12 @@ public class HTTPConnectProxiedConnection extends ProxiedConnection {
 		String statusLine = parseHTTPHeader(httpResponseBuffer_.substring(0, headerEnd), headerFields);
 
 		if (trafficFilter_ != null) {
-			Vector<Pair> newHeaderFields = trafficFilter_.filterHTTPResponseHeader(headerFields);
+			Vector<Pair> newHeaderFields = trafficFilter_.filterHTTPResponseHeader(statusLine, headerFields);
 			if (!newHeaderFields.isEmpty()) {
-				StringBuffer statusLines = new StringBuffer();
-				statusLines.append("CONNECT ").append(getServer().getAddress().toString()).append(":").append(getServer().getPort());
-				sendHTTPRequest(statusLines.toString(), newHeaderFields);
-				return;
+	            reconnect();
+	            nextHTTPRequestHeaders_.clear();
+	            nextHTTPRequestHeaders_.addAll(newHeaderFields);
+	            return;
 			}
 		}
 
diff --git a/src/com/isode/stroke/network/HTTPTrafficFilter.java b/src/com/isode/stroke/network/HTTPTrafficFilter.java
index 86f0659..c9a039e 100644
--- a/src/com/isode/stroke/network/HTTPTrafficFilter.java
+++ b/src/com/isode/stroke/network/HTTPTrafficFilter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015 Isode Limited.
+ * Copyright (c) 2015-2016 Isode Limited.
  * All rights reserved.
  * See the COPYING file for more information.
  */
@@ -18,8 +18,9 @@ public interface HTTPTrafficFilter {
 	/**
 	 * @brief This method is called by the HTTPConnectPRoxiedConnection on every incoming HTTP response.
 	 *        It can be used to insert additional HTTP requests into the HTTP CONNECT proxy initalization process.
+	 * @param statusLine status line from a HTTP header
 	 * @return A vector of HTTP header fields to use in a new request. If an empty vector is returned,
 	 *         no new request will be send and the normal proxy logic continues.
 	 */
-	public Vector<HTTPConnectProxiedConnection.Pair> filterHTTPResponseHeader(final Vector<HTTPConnectProxiedConnection.Pair> responseHeader);
+	public Vector<HTTPConnectProxiedConnection.Pair> filterHTTPResponseHeader(String statusLine, final Vector<HTTPConnectProxiedConnection.Pair> responseHeader);
 }
\ No newline at end of file
diff --git a/src/com/isode/stroke/network/JavaNetworkEnviroment.java b/src/com/isode/stroke/network/JavaNetworkEnviroment.java
new file mode 100644
index 0000000..0113c57
--- /dev/null
+++ b/src/com/isode/stroke/network/JavaNetworkEnviroment.java
@@ -0,0 +1,51 @@
+/*  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 java.net.SocketException;
+import java.util.Enumeration;
+import java.util.Vector;
+import java.util.logging.Logger;
+
+/**
+ * Java implementation of {@link NetworkEnvironment}
+ */
+public class JavaNetworkEnviroment extends NetworkEnvironment {
+
+    /**
+     * Logger
+     */
+    private final Logger logger = Logger.getLogger(this.getClass().getName());
+    
+    @Override
+    public Vector<NetworkInterface> getNetworkInterfaces() {
+        Vector<NetworkInterface> results = new Vector<NetworkInterface>();
+        try {
+            Enumeration<java.net.NetworkInterface> javaNIEnumeration = 
+                    java.net.NetworkInterface.getNetworkInterfaces();
+            if (javaNIEnumeration.hasMoreElements()) {
+                java.net.NetworkInterface javaNI = javaNIEnumeration.nextElement();
+                try {
+                    NetworkInterface strokeNI = new NetworkInterface(javaNI);
+                    results.add(strokeNI);
+                } catch (SocketException e) {
+                    logger.warning("Error determining if "+javaNI+
+                            " is loopback : "+e.getMessage());
+                }
+                
+            }
+        } 
+        catch (SocketException e) {
+            logger.warning("Error occured when getting network interfaces - "+e.getMessage());
+        }
+        return results;
+    }
+
+}
diff --git a/src/com/isode/stroke/network/NetworkInterface.java b/src/com/isode/stroke/network/NetworkInterface.java
index 5590837..1bffafc 100644
--- a/src/com/isode/stroke/network/NetworkInterface.java
+++ b/src/com/isode/stroke/network/NetworkInterface.java
@@ -4,7 +4,7 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 /*
- * Copyright (c) 2015 Isode Limited.
+ * Copyright (c) 2015-2016 Isode Limited.
  * All rights reserved.
  * See the COPYING file for more information.
  */
@@ -16,6 +16,9 @@
 
 package com.isode.stroke.network;
 
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.util.Enumeration;
 import java.util.Vector;
 
 public class NetworkInterface {
@@ -28,6 +31,25 @@ public class NetworkInterface {
 		this.name = name;
 		this.loopback = loopback;
 	}
+	
+	/**
+	 * Creates a {@link NetworkInterface} from a {@link java.net.NetworkInterface} including
+	 * all addresses in the {@link java.net.NetworkInterface}
+	 * @param javaNI The  {@link java.net.NetworkInterface} to create the {@link NetworkInterface}
+	 * from, should not be {@code null}.
+	 * @throws SocketException If an I/O error occurs when trying to determine if it is
+	 * a loop back interface.
+	 */
+	public NetworkInterface(java.net.NetworkInterface javaNI) throws SocketException {
+	    this.name = javaNI.getName();
+	    this.loopback = javaNI.isLoopback();
+	    Enumeration<InetAddress> addressEnumeration = javaNI.getInetAddresses();
+	    while (addressEnumeration.hasMoreElements()) {
+	        InetAddress inetAddress = addressEnumeration.nextElement();
+	        HostAddress hostAddress = new HostAddress(inetAddress);
+	        addAddress(hostAddress);
+	    }
+	}
 
 	public void addAddress(final HostAddress address) {
 		addresses.add(address);
diff --git a/src/com/isode/stroke/network/PlatformDomainNameAddressQuery.java b/src/com/isode/stroke/network/PlatformDomainNameAddressQuery.java
new file mode 100644
index 0000000..24deb4d
--- /dev/null
+++ b/src/com/isode/stroke/network/PlatformDomainNameAddressQuery.java
@@ -0,0 +1,86 @@
+/*  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 java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.isode.stroke.eventloop.Event.Callback;
+import com.isode.stroke.eventloop.EventLoop;
+
+public class PlatformDomainNameAddressQuery extends DomainNameAddressQuery {
+
+    private final String host_;
+    private final EventLoop eventLoop_;
+    
+    public PlatformDomainNameAddressQuery(String host,EventLoop eventLoop) {
+        host_ = host;
+        eventLoop_ = eventLoop;
+    }
+    
+    private class QueryRunnable implements Runnable {
+
+        private final List<HostAddress> results_ = 
+                Collections.synchronizedList(new ArrayList<HostAddress>());
+
+        @Override
+        public void run() {
+            try {
+                InetAddress[] inetAddresses = InetAddress.getAllByName(host_);
+                for (InetAddress address : inetAddresses) {
+                    HostAddress result = new HostAddress(address);
+                    results_.add(result);
+                }
+            } catch (UnknownHostException e) {
+                emitError();
+            }
+            emitResults();
+        }
+
+        private void emitError() {
+            eventLoop_.postEvent(new Callback() {
+                
+                @Override
+                public void run() {
+                    onResult.emit(new ArrayList<HostAddress>(),new DomainNameResolveError());
+                }
+                
+            });
+        }
+
+        private void emitResults() {
+            eventLoop_.postEvent(new Callback() {
+                
+                @Override
+                public void run() {
+                    // For thread safety emit a copy of the results
+                    List<HostAddress> resultCopy = new ArrayList<HostAddress>();
+                    synchronized (results_) {
+                        resultCopy.addAll(results_);
+                    }
+                    onResult.emit(results_,null);
+                }
+                
+            });
+        }
+        
+    }
+
+    @Override
+    public void run() {
+        Thread queryThread = new Thread(new QueryRunnable());
+        queryThread.setDaemon(true);
+        queryThread.run();
+    }
+
+}
diff --git a/src/com/isode/stroke/network/ProxiedConnection.java b/src/com/isode/stroke/network/ProxiedConnection.java
index a94fbc5..6f4c044 100644
--- a/src/com/isode/stroke/network/ProxiedConnection.java
+++ b/src/com/isode/stroke/network/ProxiedConnection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012-2015 Isode Limited.
+ * Copyright (c) 2012-2016 Isode Limited.
  * All rights reserved.
  * See the COPYING file for more information.
  */
@@ -27,8 +27,8 @@ public abstract class ProxiedConnection extends Connection {
 	private HostAddressPort server_;
 	private Connector connector_;
 	private Connection connection_;
-	private SignalConnection onDataReadConnection;
-	private SignalConnection onDisconnectedConnection;
+	private SignalConnection onDataReadConnection_;
+	private SignalConnection onDisconnectedConnection_;
 	private SignalConnection onConnectFinishedConnection;
 
 	public ProxiedConnection(DomainNameResolver resolver, ConnectionFactory connectionFactory, TimerFactory timerFactory, final String proxyHost, int proxyPort) {
@@ -45,8 +45,8 @@ public abstract class ProxiedConnection extends Connection {
 		try {
 			cancelConnector();
 			if (connection_ != null) {
-				onDataReadConnection.disconnect();
-				onDisconnectedConnection.disconnect();
+				onDataReadConnection_.disconnect();
+				onDisconnectedConnection_.disconnect();
 			}
 			if (connected_) {
 				System.err.println("Warning: Connection was still established.");
@@ -147,4 +147,20 @@ public abstract class ProxiedConnection extends Connection {
 	protected HostAddressPort getServer() {
 		return server_;
 	}
+	
+	protected void reconnect() {
+	    if (onDataReadConnection_ != null) {
+	        onDataReadConnection_.disconnect();
+	        onDataReadConnection_ = null;
+	    }
+	    if (onDisconnectedConnection_ != null) {
+	        onDisconnectedConnection_.disconnect();
+	        onDisconnectedConnection_ = null;
+	    }
+	    if (connected_) {
+	        connection_.disconnect();
+	    }
+	    connect(server_);
+	}
+	
 }
\ No newline at end of file
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;
+        }
+        
+    }
+
+}
-- 
cgit v0.10.2-6-g49f6