diff --git a/src/main/java/org/littleshoot/proxy/MitmManager.java b/src/main/java/org/littleshoot/proxy/MitmManager.java index 7ba17eb35..996993766 100644 --- a/src/main/java/org/littleshoot/proxy/MitmManager.java +++ b/src/main/java/org/littleshoot/proxy/MitmManager.java @@ -1,7 +1,6 @@ package org.littleshoot.proxy; import io.netty.handler.codec.http.HttpRequest; - import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSession; @@ -16,7 +15,7 @@ public interface MitmManager { * * @param peerHost to start a client connection to the server. * @param peerPort to start a client connection to the server. - * + * * @return an SSLEngine used to connect to an upstream server */ SSLEngine serverSslEngine(String peerHost, int peerPort); @@ -33,13 +32,13 @@ public interface MitmManager { * Creates an {@link SSLEngine} for encrypting the client connection based * on the given serverSslSession. *

- * + * *

* The serverSslSession is provided in case this method needs to inspect the * server's certificates or something else about the encryption on the way * to the server. *

- * + * *

* This is the place where one would implement impersonation of the server * by issuing replacement certificates signed by the proxy's own diff --git a/src/main/java/org/littleshoot/proxy/SelectiveMitmManager.java b/src/main/java/org/littleshoot/proxy/SelectiveMitmManager.java new file mode 100755 index 000000000..3b758a9ca --- /dev/null +++ b/src/main/java/org/littleshoot/proxy/SelectiveMitmManager.java @@ -0,0 +1,18 @@ +package org.littleshoot.proxy; + +/** + * An extension to the {@link MitmManager} interface to allow MITM to be + * selectively applied based on the peer. Added as a new interface to not + * break existing implementations. + */ +public interface SelectiveMitmManager extends MitmManager { + /** + * Checks if MITM should be applied for a given peer. + * + * @param peerHost The peer host + * @param peerPort The peer port + * + * @return true to continue with MITM, false to act as if MITM was not enabled for this peer and tunnel raw content. + */ + boolean shouldMITMPeer(String peerHost, int peerPort); +} diff --git a/src/main/java/org/littleshoot/proxy/SelectiveMitmManagerAdapter.java b/src/main/java/org/littleshoot/proxy/SelectiveMitmManagerAdapter.java new file mode 100755 index 000000000..288f9c210 --- /dev/null +++ b/src/main/java/org/littleshoot/proxy/SelectiveMitmManagerAdapter.java @@ -0,0 +1,31 @@ +package org.littleshoot.proxy; + +import io.netty.handler.codec.http.HttpRequest; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSession; + +/** + * Convenience base class for adapting a non-selective-{@link MitmManager} into a {@link SelectiveMitmManager}. + */ +public abstract class SelectiveMitmManagerAdapter implements SelectiveMitmManager { + private final MitmManager childMitmManager; + + public SelectiveMitmManagerAdapter(MitmManager childMitmManager) { + this.childMitmManager = childMitmManager; + } + + @Override + public SSLEngine serverSslEngine(String peerHost, int peerPort) { + return this.childMitmManager.serverSslEngine(peerHost, peerPort); + } + + @Override + public SSLEngine serverSslEngine() { + return this.childMitmManager.serverSslEngine(); + } + + @Override + public SSLEngine clientSslEngineFor(HttpRequest httpRequest, SSLSession serverSslSession) { + return this.childMitmManager.clientSslEngineFor(httpRequest, serverSslSession); + } +} diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java index b612f5160..4c84f8330 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java @@ -39,6 +39,7 @@ import org.littleshoot.proxy.FullFlowContext; import org.littleshoot.proxy.HttpFilters; import org.littleshoot.proxy.MitmManager; +import org.littleshoot.proxy.SelectiveMitmManager; import org.littleshoot.proxy.TransportProtocol; import org.littleshoot.proxy.UnknownTransportProtocolException; @@ -56,15 +57,14 @@ import static org.littleshoot.proxy.impl.ConnectionState.AWAITING_INITIAL; import static org.littleshoot.proxy.impl.ConnectionState.CONNECTING; import static org.littleshoot.proxy.impl.ConnectionState.DISCONNECTED; -import static org.littleshoot.proxy.impl.ConnectionState.HANDSHAKING; - +import static org.littleshoot.proxy.impl.ConnectionState.HANDSHAKING;; /** *

* Represents a connection from our proxy to a server on the web. * ProxyConnections are reused fairly liberally, and can go from disconnected to * connected, back to disconnected and so on. *

- * + * *

* Connecting a {@link ProxyToServerConnection} can involve more than just * connecting the underlying {@link Channel}. In particular, the connection may @@ -134,13 +134,13 @@ public class ProxyToServerConnection extends ProxyConnection { private volatile GlobalTrafficShapingHandler trafficHandler; /** - * Minimum size of the adaptive recv buffer when throttling is enabled. + * Minimum size of the adaptive recv buffer when throttling is enabled. */ private static final int MINIMUM_RECV_BUFFER_SIZE_BYTES = 64; - + /** * Create a new ProxyToServerConnection. - * + * * @param proxyServer * @param clientConnection * @param serverHostAndPort @@ -262,12 +262,12 @@ protected void readRaw(ByteBuf buf) { * doesn't know that any given response is to a HEAD request, so it needs to * be told that there's no content so that it doesn't hang waiting for it. *

- * + * *

* See the documentation for {@link HttpResponseDecoder} for information * about why HEAD requests need special handling. *

- * + * *

* Thanks to nataliakoval for * pointing out that with connections being reused as they are, this needs @@ -302,7 +302,7 @@ protected boolean isContentAlwaysEmpty(HttpMessage httpMessage) { /** * Like {@link #write(Object)} and also sets the current filters to the * given value. - * + * * @param msg * @param filters */ @@ -497,7 +497,7 @@ protected HttpFilters getHttpFiltersFromProxyServer(HttpRequest httpRequest) { /** * Keeps track of the current HttpResponse so that we can associate its * headers with future related chunks for this same transfer. - * + * * @param response */ private void rememberCurrentResponse(HttpResponse response) { @@ -512,7 +512,7 @@ private void rememberCurrentResponse(HttpResponse response) { /** * Respond to the client with the given {@link HttpObject}. - * + * * @param httpObject */ private void respondWith(HttpObject httpObject) { @@ -554,31 +554,39 @@ private void initializeConnectionFlow() { if (hasUpstreamChainedProxy()) { connectionFlow.then( serverConnection.HTTPCONNECTWithChainedProxy); - } - + } + MitmManager mitmManager = proxyServer.getMitmManager(); - boolean isMitmEnabled = mitmManager != null; + boolean shouldMITM = mitmManager != null; - if (isMitmEnabled) { + if (shouldMITM) { // When MITM is enabled and when chained proxy is set up, remoteAddress // will be the chained proxy's address. So we use serverHostAndPort // which is the end server's address. HostAndPort parsedHostAndPort = HostAndPort.fromString(serverHostAndPort); - // SNI may be disabled for this request due to a previous failed attempt to connect to the server - // with SNI enabled. - if (disableSni) { - connectionFlow.then(serverConnection.EncryptChannel(proxyServer.getMitmManager() - .serverSslEngine())); - } else { - connectionFlow.then(serverConnection.EncryptChannel(proxyServer.getMitmManager() - .serverSslEngine(parsedHostAndPort.getHost(), parsedHostAndPort.getPort()))); + // Potentially skip MITM + if (mitmManager instanceof SelectiveMitmManager) { + shouldMITM = ((SelectiveMitmManager)mitmManager).shouldMITMPeer(parsedHostAndPort.getHost(), parsedHostAndPort.getPort()); } - connectionFlow - .then(clientConnection.RespondCONNECTSuccessful) - .then(serverConnection.MitmEncryptClientChannel); - } else { + if (shouldMITM) { + // SNI may be disabled for this request due to a previous failed attempt to connect to the server + // with SNI enabled. + if (disableSni) { + connectionFlow.then(serverConnection.EncryptChannel(mitmManager + .serverSslEngine())); + } else { + connectionFlow.then(serverConnection.EncryptChannel(mitmManager + .serverSslEngine(parsedHostAndPort.getHost(), parsedHostAndPort.getPort()))); + } + + connectionFlow + .then(clientConnection.RespondCONNECTSuccessful) + .then(serverConnection.MitmEncryptClientChannel); + } + } + if (!shouldMITM) { connectionFlow.then(serverConnection.StartTunneling) .then(clientConnection.RespondCONNECTSuccessful) .then(clientConnection.StartTunneling); @@ -699,7 +707,7 @@ void read(ConnectionFlow flow, Object msg) { *

* Encrypts the client channel based on our server {@link SSLSession}. *

- * + * *

* This does not wait for the handshake to finish so that we can go on and * respond to the CONNECT request. @@ -809,7 +817,7 @@ private void resetConnectionForRetry() throws UnknownHostException { /** * Set up our connection parameters based on server address and chained * proxies. - * + * * @throws UnknownHostException when unable to resolve the hostname to an IP address */ private void setupConnectionParameters() throws UnknownHostException { @@ -854,15 +862,15 @@ private void setupConnectionParameters() throws UnknownHostException { /** * Initialize our {@link ChannelPipeline} to connect the upstream server. * LittleProxy acts as a client here. - * + * * A {@link ChannelPipeline} invokes the read (Inbound) handlers in * ascending ordering of the list and then the write (Outbound) handlers in * descending ordering. - * + * * Regarding the Javadoc of {@link HttpObjectAggregator} it's needed to have * the {@link HttpResponseEncoder} or {@link HttpRequestEncoder} before the * {@link HttpObjectAggregator} in the {@link ChannelPipeline}. - * + * * @param pipeline * @param httpRequest */ @@ -906,7 +914,7 @@ private void initChannelPipeline(ChannelPipeline pipeline, * Do all the stuff that needs to be done after our {@link ConnectionFlow} * has succeeded. *

- * + * * @param shouldForwardInitialRequest * whether or not we should forward the initial HttpRequest to * the server after the connection has been established. @@ -941,7 +949,7 @@ void connectionSucceeded(boolean shouldForwardInitialRequest) { /** * Build an {@link InetSocketAddress} for the given hostAndPort. - * + * * @param hostAndPort String representation of the host and port * @param proxyServer the current {@link DefaultHttpProxyServer} * @return a resolved InetSocketAddress for the specified hostAndPort @@ -966,7 +974,7 @@ public static InetSocketAddress addressFor(String hostAndPort, DefaultHttpProxyS /*************************************************************************** * Activity Tracking/Statistics - * + * * We track statistics on bytes, requests and responses by adding handlers * at the appropriate parts of the pipeline (see initChannelPipeline()). **************************************************************************/ diff --git a/src/test/java/org/littleshoot/proxy/BaseMitmProxyTest.java b/src/test/java/org/littleshoot/proxy/BaseMitmProxyTest.java new file mode 100755 index 000000000..6394e50b6 --- /dev/null +++ b/src/test/java/org/littleshoot/proxy/BaseMitmProxyTest.java @@ -0,0 +1,146 @@ +package org.littleshoot.proxy; + +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import java.nio.charset.Charset; +import java.util.HashSet; +import java.util.Set; +import org.littleshoot.proxy.extras.SelfSignedMitmManager; + +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * Base class for testing a single basic proxy running as a man in the middle. + */ +public abstract class BaseMitmProxyTest extends BaseProxyTest { + private Set requestPreMethodsSeen = new HashSet(); + private Set requestPostMethodsSeen = new HashSet(); + private StringBuilder responsePreBody = new StringBuilder(); + private StringBuilder responsePostBody = new StringBuilder(); + private Set responsePreOriginalRequestMethodsSeen = new HashSet(); + private Set responsePostOriginalRequestMethodsSeen = new HashSet(); + + protected MitmManager getMitmManager() { + return new SelfSignedMitmManager(); + } + + @Override + protected void setUp() { + this.proxyServer = bootstrapProxy() + .withPort(0) + .withManInTheMiddle(this.getMitmManager()) + .withFiltersSource(new HttpFiltersSourceAdapter() { + @Override + public HttpFilters filterRequest(HttpRequest originalRequest) { + return new HttpFiltersAdapter(originalRequest) { + @Override + public HttpResponse clientToProxyRequest( + HttpObject httpObject) { + if (httpObject instanceof HttpRequest) { + requestPreMethodsSeen + .add(((HttpRequest) httpObject) + .getMethod()); + } + return null; + } + + @Override + public HttpResponse proxyToServerRequest( + HttpObject httpObject) { + if (httpObject instanceof HttpRequest) { + requestPostMethodsSeen + .add(((HttpRequest) httpObject) + .getMethod()); + } + return null; + } + + @Override + public HttpObject serverToProxyResponse( + HttpObject httpObject) { + if (httpObject instanceof HttpResponse) { + responsePreOriginalRequestMethodsSeen + .add(originalRequest.getMethod()); + } else if (httpObject instanceof HttpContent) { + responsePreBody.append(((HttpContent) httpObject) + .content().toString( + Charset.forName("UTF-8"))); + } + return httpObject; + } + + @Override + public HttpObject proxyToClientResponse( + HttpObject httpObject) { + if (httpObject instanceof HttpResponse) { + responsePostOriginalRequestMethodsSeen + .add(originalRequest.getMethod()); + } else if (httpObject instanceof HttpContent) { + responsePostBody.append(((HttpContent) httpObject) + .content().toString( + Charset.forName("UTF-8"))); + } + return httpObject; + } + }; + } + }) + .start(); + } + + protected void assertMethodSeenInRequestFilters(HttpMethod method) { + assertThat(method + + " should have been seen in clientToProxyRequest filter", + requestPreMethodsSeen, hasItem(method)); + assertThat(method + + " should have been seen in proxyToServerRequest filter", + requestPostMethodsSeen, hasItem(method)); + } + + protected void assertMethodSeenInResponseFilters(HttpMethod method) { + assertThat( + method + + " should have been seen as the original requests's method in serverToProxyResponse filter", + responsePreOriginalRequestMethodsSeen, hasItem(method)); + assertThat( + method + + " should have been seen as the original requests's method in proxyToClientResponse filter", + responsePostOriginalRequestMethodsSeen, hasItem(method)); + } + + protected void assertMethodNotSeenInRequestFilters(HttpMethod method) { + assertThat(method + + " should have not been seen in clientToProxyRequest filter", + requestPreMethodsSeen, not(hasItem(method))); + assertThat(method + + " should have not been seen in proxyToServerRequest filter", + requestPostMethodsSeen, not(hasItem(method))); + } + + protected void assertMethodNotSeenInResponseFilters(HttpMethod method) { + assertThat( + method + + " should have not been seen as the original requests's method in serverToProxyResponse filter", + responsePreOriginalRequestMethodsSeen, not(hasItem(method))); + assertThat( + method + + " should have not been seen as the original requests's method in proxyToClientResponse filter", + responsePostOriginalRequestMethodsSeen, not(hasItem(method))); + } + + protected void assertResponseFromFiltersMatchesActualResponse() { + assertEquals( + "Data received through HttpFilters.serverToProxyResponse should match response", + lastResponse, responsePreBody.toString()); + assertEquals( + "Data received through HttpFilters.proxyToClientResponse should match response", + lastResponse, responsePostBody.toString()); + } + +} diff --git a/src/test/java/org/littleshoot/proxy/MitmProxyTest.java b/src/test/java/org/littleshoot/proxy/MitmProxyTest.java index 7b2853a19..022d01503 100644 --- a/src/test/java/org/littleshoot/proxy/MitmProxyTest.java +++ b/src/test/java/org/littleshoot/proxy/MitmProxyTest.java @@ -1,96 +1,11 @@ package org.littleshoot.proxy; -import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; -import org.littleshoot.proxy.extras.SelfSignedMitmManager; - -import java.nio.charset.Charset; -import java.util.HashSet; -import java.util.Queue; -import java.util.Set; - -import static org.hamcrest.Matchers.hasItem; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; /** * Tests just a single basic proxy running as a man in the middle. */ -public class MitmProxyTest extends BaseProxyTest { - private Set requestPreMethodsSeen = new HashSet(); - private Set requestPostMethodsSeen = new HashSet(); - private StringBuilder responsePreBody = new StringBuilder(); - private StringBuilder responsePostBody = new StringBuilder(); - private Set responsePreOriginalRequestMethodsSeen = new HashSet(); - private Set responsePostOriginalRequestMethodsSeen = new HashSet(); - - @Override - protected void setUp() { - this.proxyServer = bootstrapProxy() - .withPort(0) - .withManInTheMiddle(new SelfSignedMitmManager()) - .withFiltersSource(new HttpFiltersSourceAdapter() { - @Override - public HttpFilters filterRequest(HttpRequest originalRequest) { - return new HttpFiltersAdapter(originalRequest) { - @Override - public HttpResponse clientToProxyRequest( - HttpObject httpObject) { - if (httpObject instanceof HttpRequest) { - requestPreMethodsSeen - .add(((HttpRequest) httpObject) - .getMethod()); - } - return null; - } - - @Override - public HttpResponse proxyToServerRequest( - HttpObject httpObject) { - if (httpObject instanceof HttpRequest) { - requestPostMethodsSeen - .add(((HttpRequest) httpObject) - .getMethod()); - } - return null; - } - - @Override - public HttpObject serverToProxyResponse( - HttpObject httpObject) { - if (httpObject instanceof HttpResponse) { - responsePreOriginalRequestMethodsSeen - .add(originalRequest.getMethod()); - } else if (httpObject instanceof HttpContent) { - responsePreBody.append(((HttpContent) httpObject) - .content().toString( - Charset.forName("UTF-8"))); - } - return httpObject; - } - - @Override - public HttpObject proxyToClientResponse( - HttpObject httpObject) { - if (httpObject instanceof HttpResponse) { - responsePostOriginalRequestMethodsSeen - .add(originalRequest.getMethod()); - } else if (httpObject instanceof HttpContent) { - responsePostBody.append(((HttpContent) httpObject) - .content().toString( - Charset.forName("UTF-8"))); - } - return httpObject; - } - }; - } - }) - .start(); - } - +public class MitmProxyTest extends BaseMitmProxyTest { @Override protected boolean isMITM() { return true; @@ -129,34 +44,4 @@ public void testSimplePostRequestOverHTTPS() throws Exception { assertMethodSeenInResponseFilters(HttpMethod.POST); assertResponseFromFiltersMatchesActualResponse(); } - - private void assertMethodSeenInRequestFilters(HttpMethod method) { - assertThat(method - + " should have been seen in clientToProxyRequest filter", - requestPreMethodsSeen, hasItem(method)); - assertThat(method - + " should have been seen in proxyToServerRequest filter", - requestPostMethodsSeen, hasItem(method)); - } - - private void assertMethodSeenInResponseFilters(HttpMethod method) { - assertThat( - method - + " should have been seen as the original requests's method in serverToProxyResponse filter", - responsePreOriginalRequestMethodsSeen, hasItem(method)); - assertThat( - method - + " should have been seen as the original requests's method in proxyToClientResponse filter", - responsePostOriginalRequestMethodsSeen, hasItem(method)); - } - - private void assertResponseFromFiltersMatchesActualResponse() { - assertEquals( - "Data received through HttpFilters.serverToProxyResponse should match response", - lastResponse, responsePreBody.toString()); - assertEquals( - "Data received through HttpFilters.proxyToClientResponse should match response", - lastResponse, responsePostBody.toString()); - } - } diff --git a/src/test/java/org/littleshoot/proxy/SelectiveMitmProxyTest.java b/src/test/java/org/littleshoot/proxy/SelectiveMitmProxyTest.java new file mode 100755 index 000000000..14f87a989 --- /dev/null +++ b/src/test/java/org/littleshoot/proxy/SelectiveMitmProxyTest.java @@ -0,0 +1,83 @@ +package org.littleshoot.proxy; + +import com.google.common.net.HostAndPort; +import io.netty.handler.codec.http.HttpMethod; +import java.util.ArrayList; +import java.util.List; +import org.apache.http.HttpHost; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests just a single basic proxy running as a man in the middle, where the + * MTM manager is a selective manager, that for the purpose of the tests, always + * returns false. + */ +public class SelectiveMitmProxyTest extends BaseMitmProxyTest { + private List mitmRequests = new ArrayList<>(); + + @Override + protected boolean isMITM() { + return false; + } + + @Override + protected MitmManager getMitmManager() { + return new SelectiveMitmManagerAdapter(super.getMitmManager()) { + @Override + public boolean shouldMITMPeer(String peerHost, int peerPort) { + mitmRequests.add(HostAndPort.fromParts(peerHost, peerPort)); + return false; + } + }; + } + + @Override + public void testSimpleGetRequest() throws Exception { + super.testSimpleGetRequest(); + assertMethodSeenInRequestFilters(HttpMethod.GET); + assertMethodSeenInResponseFilters(HttpMethod.GET); + assertNoMitmChecks(); + assertResponseFromFiltersMatchesActualResponse(); + } + + @Override + public void testSimpleGetRequestOverHTTPS() throws Exception { + super.testSimpleGetRequestOverHTTPS(); + assertMethodSeenInRequestFilters(HttpMethod.CONNECT); + assertSingleMitmCheck(httpsWebHost); + assertMethodNotSeenInRequestFilters(HttpMethod.GET); + assertMethodNotSeenInRequestFilters(HttpMethod.GET); + } + + @Override + public void testSimplePostRequest() throws Exception { + super.testSimplePostRequest(); + assertMethodSeenInRequestFilters(HttpMethod.POST); + assertMethodSeenInResponseFilters(HttpMethod.POST); + assertNoMitmChecks(); + assertResponseFromFiltersMatchesActualResponse(); + } + + @Override + public void testSimplePostRequestOverHTTPS() throws Exception { + super.testSimplePostRequestOverHTTPS(); + assertMethodSeenInRequestFilters(HttpMethod.CONNECT); + assertSingleMitmCheck(httpsWebHost); + assertMethodNotSeenInRequestFilters(HttpMethod.POST); + assertMethodNotSeenInRequestFilters(HttpMethod.POST); + } + + private void assertNoMitmChecks() { + assertThat(mitmRequests, is(empty())); + } + + private void assertSingleMitmCheck(HttpHost host) { + assertThat(mitmRequests, hasSize(1)); + assertThat(mitmRequests.get(0), is(equalTo(HostAndPort.fromParts(host.getHostName(), host.getPort())))); + } +}