Skip to content

Commit

Permalink
Answer https without connecting upstream.
Browse files Browse the repository at this point in the history
This enables a caching proxy for offline use, which is supported with
HTTP at the moment for HTTPS too. It's needed to suppress the handshake
to upstream without a connection. Since it's necessary to disable
resolving of IP addresses a HostResolver answering unresolved addresses
is needed, too. This is used to detect the offline mode in LittleProxy
Man-In-The-Middle handling.
  • Loading branch information
ganskef committed May 22, 2016
1 parent 59abde8 commit eb2f436
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 53 deletions.
12 changes: 8 additions & 4 deletions src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

/**
Expand Down
107 changes: 58 additions & 49 deletions src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -135,11 +134,6 @@ public class ProxyToServerConnection extends ProxyConnection<HttpResponse> {
*/
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.
*
Expand Down Expand Up @@ -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);
}
}

}
}

Expand Down Expand Up @@ -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()
Expand All @@ -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() {

Expand All @@ -680,7 +680,7 @@ public void operationComplete(ChannelFuture arg0) throws Exception {
}
}
});
return future;
return future;
} else {
return writeToChannel(initialRequest);
}
Expand Down Expand Up @@ -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<Future<? super Channel>>() {
@Override
Expand All @@ -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.
Expand Down
124 changes: 124 additions & 0 deletions src/test/java/org/littleshoot/proxy/MitmOfflineTest.java
Original file line number Diff line number Diff line change
@@ -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);
}

}

0 comments on commit eb2f436

Please sign in to comment.