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

MITM and Chained proxy support #251

Merged
merged 3 commits into from
Dec 21, 2015
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -709,8 +709,7 @@ public HttpProxyServerBootstrap withChainProxyManager(
ChainedProxyManager chainProxyManager) {
this.chainProxyManager = chainProxyManager;
if (this.mitmManager != null) {
LOG.warn("Enabled proxy chaining with man in the middle. These are mutually exclusive - man in the middle will be disabled.");
this.mitmManager = null;
LOG.info("Enabled proxy chaining with man in the middle.");
}
return this;
}
Expand All @@ -720,8 +719,7 @@ public HttpProxyServerBootstrap withManInTheMiddle(
MitmManager mitmManager) {
this.mitmManager = mitmManager;
if (this.chainProxyManager != null) {
LOG.warn("Enabled man in the middle along with proxy chaining. These are mutually exclusive - proxy chaining will be disabled.");
this.chainProxyManager = null;
LOG.info("Enabled man in the middle along with proxy chaining.");
}
return this;
}
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,15 @@ protected Future<Channel> encrypt(ChannelPipeline pipeline,
channel.config().setAutoRead(true);
}
SslHandler handler = new SslHandler(sslEngine);
pipeline.addFirst("ssl", handler);
if(pipeline.get("ssl") == null) {
pipeline.addFirst("ssl", handler);
} else {
// The second SSL handler is added to handle the case
// where the proxy (running as MITM) has to chain with
// another SSL enabled proxy. The second SSL handler
// is to perform SSL with the server.
pipeline.addAfter("ssl", "sslWithServer", handler);
}
return handler.handshakeFuture();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import io.netty.bootstrap.ChannelFactory;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
Expand Down Expand Up @@ -267,8 +269,16 @@ protected boolean isContentAlwaysEmpty(HttpMessage httpMessage) {
identifyCurrentRequest();
}

return HttpMethod.HEAD.equals(currentHttpRequest.getMethod()) ?
true : super.isContentAlwaysEmpty(httpMessage);
// The current HTTP Request can be null when this proxy is
// negotiating a CONNECT request with a chained proxy
// while it is running as a MITM. Since the response to a
// CONNECT request does not have any content, we return true.
if(currentHttpRequest == null) {
return true;
} else {
return HttpMethod.HEAD.equals(currentHttpRequest.getMethod()) ?
true : super.isContentAlwaysEmpty(httpMessage);
}
}
};

Expand Down Expand Up @@ -550,23 +560,36 @@ private void initializeConnectionFlow() {
}

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) {
connectionFlow
.then(serverConnection.EncryptChannel(mitmManager
.serverSslEngine(remoteAddress.getHostName(),
remoteAddress.getPort())))
if (isMitmEnabled) {
if(hasUpstreamChainedProxy()){
// 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);

connectionFlow.then(serverConnection.EncryptChannel(proxyServer.getMitmManager()
.serverSslEngine(parsedHostAndPort.getHostText(),
parsedHostAndPort.getPort())));
} else {
connectionFlow.then(serverConnection.EncryptChannel(proxyServer.getMitmManager()
.serverSslEngine(remoteAddress.getHostName(),
remoteAddress.getPort())));
}

connectionFlow
.then(clientConnection.RespondCONNECTSuccessful)
.then(serverConnection.MitmEncryptClientChannel);
} else {
// If we're chaining, forward the CONNECT request
if (hasUpstreamChainedProxy()) {
connectionFlow.then(
serverConnection.HTTPCONNECTWithChainedProxy);
}

connectionFlow.then(serverConnection.StartTunneling)
.then(clientConnection.RespondCONNECTSuccessful)
.then(clientConnection.StartTunneling);
Expand Down Expand Up @@ -631,7 +654,32 @@ protected void initChannel(Channel ch) throws Exception {
protected Future<?> execute() {
LOG.debug("Handling CONNECT request through Chained Proxy");
chainedProxy.filterRequest(initialRequest);
return writeToChannel(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()
* in ProxyConnection). This cannot be ignored while we are
* doing MITM + Chained Proxy because the HttpRequestEncoder
* of the ProxyToServerConnection will be in an invalid state
* when the next request is written. Writing the EmptyLastContent
* resets its state.
*/
if(isMitmEnabled){
ChannelFuture future = writeToChannel(initialRequest);
future.addListener(new ChannelFutureListener() {

@Override
public void operationComplete(ChannelFuture arg0) throws Exception {
if(arg0.isSuccess()){
writeToChannel(LastHttpContent.EMPTY_LAST_CONTENT);
}
}
});
return future;
} else {
return writeToChannel(initialRequest);
}
}

void onSuccess(ConnectionFlow flow) {
Expand Down
12 changes: 6 additions & 6 deletions src/test/java/org/littleshoot/proxy/BaseChainedProxyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@
* by the downstream proxy was received by the upstream proxy.
*/
public abstract class BaseChainedProxyTest extends BaseProxyTest {
private final AtomicLong REQUESTS_SENT_BY_DOWNSTREAM = new AtomicLong(
protected final AtomicLong REQUESTS_SENT_BY_DOWNSTREAM = new AtomicLong(
0l);
private final AtomicLong REQUESTS_RECEIVED_BY_UPSTREAM = new AtomicLong(
protected final AtomicLong REQUESTS_RECEIVED_BY_UPSTREAM = new AtomicLong(
0l);
private final ConcurrentSkipListSet<TransportProtocol> TRANSPORTS_USED = new ConcurrentSkipListSet<TransportProtocol>();
protected final ConcurrentSkipListSet<TransportProtocol> TRANSPORTS_USED = new ConcurrentSkipListSet<TransportProtocol>();

private final ActivityTracker DOWNSTREAM_TRACKER = new ActivityTrackerAdapter() {
protected final ActivityTracker DOWNSTREAM_TRACKER = new ActivityTrackerAdapter() {
@Override
public void requestSentToServer(FullFlowContext flowContext,
io.netty.handler.codec.http.HttpRequest httpRequest) {
Expand All @@ -36,15 +36,15 @@ public void requestSentToServer(FullFlowContext flowContext,
}
};

private final ActivityTracker UPSTREAM_TRACKER = new ActivityTrackerAdapter() {
protected final ActivityTracker UPSTREAM_TRACKER = new ActivityTrackerAdapter() {
@Override
public void requestReceivedFromClient(FlowContext flowContext,
HttpRequest httpRequest) {
REQUESTS_RECEIVED_BY_UPSTREAM.incrementAndGet();
}
};

private HttpProxyServer upstreamProxy;
protected HttpProxyServer upstreamProxy;

@Override
protected void setUp() {
Expand Down
8 changes: 0 additions & 8 deletions src/test/java/org/littleshoot/proxy/MitmProxyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,6 @@ public class MitmProxyTest extends BaseProxyTest {
protected void setUp() {
this.proxyServer = bootstrapProxy()
.withPort(0)
// Include a ChainedProxyManager to make sure that MITM setting
// overrides this
.withChainProxyManager(new ChainedProxyManager() {
@Override
public void lookupChainedProxies(HttpRequest httpRequest,
Queue<ChainedProxy> chainedProxies) {
}
})
.withManInTheMiddle(new SelfSignedMitmManager())
.withFiltersSource(new HttpFiltersSourceAdapter() {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.littleshoot.proxy;

import static org.littleshoot.proxy.TransportProtocol.*;

import javax.net.ssl.SSLEngine;

import org.junit.Ignore;
import org.littleshoot.proxy.extras.SelfSignedSslEngineSource;

/**
* Tests that clients are authenticated and that if they're missing certs, we
* get an error.
*/
@Ignore
public class MitmWithBadClientAuthenticationTCPChainedProxyTest extends
MitmWithChainedProxyTest {
private final SslEngineSource serverSslEngineSource = new SelfSignedSslEngineSource(
"chain_proxy_keystore_1.jks");

private final SslEngineSource clientSslEngineSource = new SelfSignedSslEngineSource(
"chain_proxy_keystore_1.jks", false, false);

@Override
protected boolean expectBadGatewayForEverything() {
return true;
}

@Override
protected HttpProxyServerBootstrap upstreamProxy() {
return super.upstreamProxy()
.withTransportProtocol(TCP)
.withSslEngineSource(serverSslEngineSource);
}

@Override
protected ChainedProxy newChainedProxy() {
return new BaseChainedProxy() {
@Override
public TransportProtocol getTransportProtocol() {
return TransportProtocol.TCP;
}

@Override
public boolean requiresEncryption() {
return true;
}

@Override
public SSLEngine newSslEngine() {
return clientSslEngineSource.newSslEngine();
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.littleshoot.proxy;

import static org.littleshoot.proxy.TransportProtocol.*;

import javax.net.ssl.SSLEngine;

import org.junit.Ignore;
import org.littleshoot.proxy.extras.SelfSignedSslEngineSource;

/**
* Tests that servers are authenticated and that if they're missing certs, we
* get an error.
*/
@Ignore
public class MitmWithBadServerAuthenticationTCPChainedProxyTest extends
BaseChainedProxyTest {
protected final SslEngineSource serverSslEngineSource = new SelfSignedSslEngineSource(
"chain_proxy_keystore_1.jks");

protected final SslEngineSource clientSslEngineSource = new SelfSignedSslEngineSource(
"chain_proxy_keystore_2.jks");

@Override
protected boolean expectBadGatewayForEverything() {
return true;
}

@Override
protected HttpProxyServerBootstrap upstreamProxy() {
return super.upstreamProxy()
.withTransportProtocol(TCP)
.withSslEngineSource(serverSslEngineSource);
}

@Override
protected ChainedProxy newChainedProxy() {
return new BaseChainedProxy() {
@Override
public TransportProtocol getTransportProtocol() {
return TransportProtocol.TCP;
}

@Override
public boolean requiresEncryption() {
return true;
}

@Override
public SSLEngine newSslEngine() {
return clientSslEngineSource.newSslEngine();
}
};
}
}
Loading