diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java index 58c3eb240..a9487cd96 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java @@ -548,16 +548,20 @@ public SSLEngine getSslEngine() { * Call this to stop reading. */ protected void stopReading() { - LOG.debug("Stopped reading"); - this.channel.config().setAutoRead(false); + if (channel != null) { + LOG.debug("Stopped reading"); + this.channel.config().setAutoRead(false); + } } /** * Call this to resume reading. */ protected void resumeReading() { - LOG.debug("Resumed reading"); - this.channel.config().setAutoRead(true); + if (channel != null) { + LOG.debug("Resumed reading"); + this.channel.config().setAutoRead(true); + } } /** diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java index c9cd0f2d4..35852efdc 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java @@ -33,7 +33,6 @@ import org.littleshoot.proxy.ChainedProxyManager; import org.littleshoot.proxy.FullFlowContext; import org.littleshoot.proxy.HttpFilters; -import org.littleshoot.proxy.MitmManager; import org.littleshoot.proxy.TransportProtocol; import org.littleshoot.proxy.UnknownTransportProtocolException; @@ -135,11 +134,6 @@ public class ProxyToServerConnection extends ProxyConnection { */ private volatile GlobalTrafficShapingHandler trafficHandler; - /** - * 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. * @@ -549,55 +543,63 @@ private void connectAndWrite(HttpRequest initialRequest) { connectionFlow.start(); } + private boolean isMitmEnabled() { + return proxyServer.getMitmManager() != null; + } + /** * This method initializes our {@link ConnectionFlow} based on however this connection has been configured. If * the {@link #disableSni} value is true, this method will not pass peer information to the MitmManager when * handling CONNECTs. */ private void initializeConnectionFlow() { - this.connectionFlow = new ConnectionFlow(clientConnection, this, - connectLock) - .then(ConnectChannel); + connectionFlow = new ConnectionFlow(clientConnection, this, connectLock); + if (remoteAddress.isUnresolved() && isMitmEnabled() && ProxyUtils.isCONNECT(initialRequest)) { + // A caching proxy needs to install a HostResolver which returns + // unresolved addresses in off line mode. So, an unresolved address + // here means a cached response is requested. Don't connect/encrypt + // a channel to the upstream proxy or server. + connectionFlow.then(clientConnection.RespondCONNECTSuccessful); + connectionFlow.then(serverConnection.MitmEncryptClientChannel); + } else { + // Otherwise an upstream connection is required + connectionFlow.then(ConnectChannel); - if (chainedProxy != null && chainedProxy.requiresEncryption()) { - connectionFlow.then(serverConnection.EncryptChannel(chainedProxy - .newSslEngine())); - } + if (chainedProxy != null && chainedProxy.requiresEncryption()) { + connectionFlow.then(serverConnection.EncryptChannel(chainedProxy.newSslEngine())); + } - if (ProxyUtils.isCONNECT(initialRequest)) { - // If we're chaining, forward the CONNECT request - if (hasUpstreamChainedProxy()) { - connectionFlow.then( - serverConnection.HTTPCONNECTWithChainedProxy); - } - - MitmManager mitmManager = proxyServer.getMitmManager(); - boolean isMitmEnabled = mitmManager != null; - - if (isMitmEnabled) { - // 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.getHostText(), parsedHostAndPort.getPort()))); + if (ProxyUtils.isCONNECT(initialRequest)) { + // If we're chaining, forward the CONNECT request + if (hasUpstreamChainedProxy()) { + connectionFlow.then(serverConnection.HTTPCONNECTWithChainedProxy); } + if (isMitmEnabled()) { + // 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.getHostText(), parsedHostAndPort.getPort()))); + } - connectionFlow - .then(clientConnection.RespondCONNECTSuccessful) - .then(serverConnection.MitmEncryptClientChannel); - } else { - connectionFlow.then(serverConnection.StartTunneling) - .then(clientConnection.RespondCONNECTSuccessful) - .then(clientConnection.StartTunneling); + connectionFlow.then(clientConnection.RespondCONNECTSuccessful); + connectionFlow.then(serverConnection.MitmEncryptClientChannel); + } else { + connectionFlow.then(serverConnection.StartTunneling); + connectionFlow.then(clientConnection.RespondCONNECTSuccessful); + connectionFlow.then(clientConnection.StartTunneling); + } } + } } @@ -658,8 +660,6 @@ protected void initChannel(Channel ch) throws Exception { protected Future execute() { LOG.debug("Handling CONNECT request through Chained Proxy"); chainedProxy.filterRequest(initialRequest); - MitmManager mitmManager = proxyServer.getMitmManager(); - boolean isMitmEnabled = mitmManager != null; /* * We ignore the LastHttpContent which we read from the client * connection when we are negotiating connect (see readHttp() @@ -669,7 +669,7 @@ protected Future execute() { * when the next request is written. Writing the EmptyLastContent * resets its state. */ - if(isMitmEnabled){ + if(isMitmEnabled()){ ChannelFuture future = writeToChannel(initialRequest); future.addListener(new ChannelFutureListener() { @@ -680,7 +680,7 @@ public void operationComplete(ChannelFuture arg0) throws Exception { } } }); - return future; + return future; } else { return writeToChannel(initialRequest); } @@ -736,7 +736,7 @@ boolean shouldSuppressInitialRequest() { protected Future execute() { return clientConnection .encrypt(proxyServer.getMitmManager() - .clientSslEngineFor(initialRequest, sslEngine.getSession()), false) + .clientSslEngineFor(initialRequest, getSSLSessionOrNull()), false) .addListener( new GenericFutureListener>() { @Override @@ -751,6 +751,15 @@ public void operationComplete( } }; + private SSLSession getSSLSessionOrNull() { + // A SSLSession in the proxy to server connection could be null in an + // offline situation. Therefore this avoids a NullPointerException here. + if (sslEngine == null) { + return null; + } + return sslEngine.getSession(); + } + /** * Called when the connection to the server or upstream chained proxy fails. This method may return true to indicate * that the connection should be retried. If returning true, this method must set up the connection itself. diff --git a/src/test/java/org/littleshoot/proxy/MitmOfflineTest.java b/src/test/java/org/littleshoot/proxy/MitmOfflineTest.java new file mode 100644 index 000000000..5ac5a5347 --- /dev/null +++ b/src/test/java/org/littleshoot/proxy/MitmOfflineTest.java @@ -0,0 +1,124 @@ +package org.littleshoot.proxy; + +import static org.junit.Assert.assertEquals; + +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +import org.apache.http.HttpHost; +import org.junit.Test; +import org.littleshoot.proxy.extras.SelfSignedMitmManager; +import org.littleshoot.proxy.impl.ProxyUtils; + +/** + * Tests a proxy running as a man in the middle without server connection. The + * purpose is to store traffic while Online and spool it in an Offline mode. + */ +public class MitmOfflineTest extends AbstractProxyTest { + + private static final String OFFLINE_RESPONSE = "Offline response"; + + private static final ResponseInfo EXPEXTED = new ResponseInfo(200, + OFFLINE_RESPONSE); + + private HttpHost httpHost; + + private HttpHost secureHost; + + @Override + protected void setUp() { + httpHost = new HttpHost("unknown", 80, "http"); + secureHost = new HttpHost("unknown", 443, "https"); + proxyServer = bootstrapProxy().withPort(0) + .withManInTheMiddle(new SelfSignedMitmManager()) + .withFiltersSource(new HttpFiltersSourceAdapter() { + @Override + public HttpFilters filterRequest( + HttpRequest originalRequest, + ChannelHandlerContext ctx) { + + // The connect request must bypass the filter! Otherwise + // the handshake will fail. + // + if (ProxyUtils.isCONNECT(originalRequest)) { + return new HttpFiltersAdapter(originalRequest, ctx); + } + + return new HttpFiltersAdapter(originalRequest, ctx) { + + // This filter delivers special responses while + // connection is limited + // + @Override + public HttpResponse clientToProxyRequest( + HttpObject httpObject) { + return createOfflineResponse(); + } + + }; + } + + }).withServerResolver(new HostResolver() { + @Override + public InetSocketAddress resolve(String host, int port) + throws UnknownHostException { + + // This unresolved address marks the Offline mode, + // checked in ProxyToServerConnection, to suppress the + // server handshake. + // + return new InetSocketAddress(host, port); + } + }).start(); + } + + private HttpResponse createOfflineResponse() { + ByteBuf buffer = Unpooled.wrappedBuffer(OFFLINE_RESPONSE.getBytes()); + HttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buffer); + HttpHeaders.setContentLength(response, buffer.readableBytes()); + HttpHeaders.setHeader(response, HttpHeaders.Names.CONTENT_TYPE, + "text/html"); + return response; + } + + @Test + public void testSimpleGetRequestOffline() throws Exception { + ResponseInfo actual = httpGetWithApacheClient(httpHost, + DEFAULT_RESOURCE, true, false); + assertEquals(EXPEXTED, actual); + } + + @Test + public void testSimpleGetRequestOverHTTPSOffline() throws Exception { + ResponseInfo actual = httpGetWithApacheClient(secureHost, + DEFAULT_RESOURCE, true, false); + assertEquals(EXPEXTED, actual); + } + + @Test + public void testSimplePostRequestOffline() throws Exception { + ResponseInfo actual = httpPostWithApacheClient(httpHost, + DEFAULT_RESOURCE, true); + assertEquals(EXPEXTED, actual); + } + + @Test + public void testSimplePostRequestOverHTTPSOffline() throws Exception { + ResponseInfo actual = httpPostWithApacheClient(secureHost, + DEFAULT_RESOURCE, true); + assertEquals(EXPEXTED, actual); + } + +}