diff options
author | Alex Clayton <alex.clayton@isode.com> | 2016-02-22 16:05:37 (GMT) |
---|---|---|
committer | Alex Clayton <alex.clayton@isode.com> | 2016-02-29 12:10:44 (GMT) |
commit | d636d68c84229c82ff746c7697d2014ff4dd4477 (patch) | |
tree | a534ffdb9696c68d21d1cec6624023795ef683d7 | |
parent | 2de569d23468c94fdcf1adc336a580b053423fd7 (diff) | |
download | stroke-d636d68c84229c82ff746c7697d2014ff4dd4477.zip stroke-d636d68c84229c82ff746c7697d2014ff4dd4477.tar.bz2 |
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
8 files changed, 697 insertions, 23 deletions
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; + } + + } + +} |