Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Answer https without connecting upstream. #282

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}

}