Skip to content

Commit

Permalink
Respond to https requests without connecting upstream servers
Browse files Browse the repository at this point in the history
This enables a caching proxy for offline use, which is no problem with
HTTP at the moment. Additionally HTTPS needs the hostname to create a
certificate and must suppress the handshake to upstream without a
connection.
  • Loading branch information
ganskef committed Aug 2, 2015
1 parent d6ee6d0 commit 48e8088
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 3 deletions.
6 changes: 5 additions & 1 deletion src/main/java/org/littleshoot/proxy/MitmManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ public interface MitmManager {
*
* @param serverSslSession
* the {@link SSLSession} that's been established with the server
* @param serverHostAndPort
* the server host name, optionally with port, to create the
* dynamic certificate for
* @return
*/
SSLEngine clientSslEngineFor(SSLSession serverSslSession);
SSLEngine clientSslEngineFor(SSLSession serverSslSession,
String serverHostAndPort);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public SSLEngine serverSslEngine(String peerHost, int peerPort) {
}

@Override
public SSLEngine clientSslEngineFor(SSLSession serverSslSession) {
public SSLEngine clientSslEngineFor(SSLSession serverSslSession, String
serverHostAndPort) {
return selfSignedSslEngineSource.newSslEngine();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@ private void connectAndWrite(final HttpRequest initialRequest) {
/**
* This method initializes our {@link ConnectionFlow} based on however this
* connection has been configured.
*
* TODO clean up this method
*/
private void initializeConnectionFlow() {
this.connectionFlow = new ConnectionFlow(clientConnection, this,
Expand All @@ -548,12 +550,29 @@ private void initializeConnectionFlow() {
boolean isMitmEnabled = mitmManager != null;

if (isMitmEnabled) {
// A caching proxy needs to install a HostResolver which returns
// unresolved addresses in off line mode. An unresolved address
// here means a cached response is requested and the server
// shouldn't be connected. Don't connect/encrypt a channel to
// the server.
//
if (remoteAddress.isUnresolved()) {
// A new instance, since we can't use connectionFlow with
// ConnectChannel here
//
connectionFlow = new ConnectionFlow(clientConnection, this,
connectLock);
connectionFlow.then(
clientConnection.RespondCONNECTSuccessful).then(
serverConnection.MitmEncryptClientChannel);
} else {
connectionFlow
.then(serverConnection.EncryptChannel(mitmManager
.serverSslEngine(remoteAddress.getHostName(),
remoteAddress.getPort())))
.then(clientConnection.RespondCONNECTSuccessful)
.then(serverConnection.MitmEncryptClientChannel);
}
} else {
// If we're chaining, forward the CONNECT request
if (hasUpstreamChainedProxy()) {
Expand Down Expand Up @@ -680,7 +699,7 @@ boolean shouldSuppressInitialRequest() {
protected Future<?> execute() {
return clientConnection
.encrypt(proxyServer.getMitmManager()
.clientSslEngineFor(sslEngine.getSession()), false)
.clientSslEngineFor(sslEngine == null ? null : sslEngine.getSession(), serverHostAndPort), false)
.addListener(
new GenericFutureListener<Future<? super Channel>>() {
@Override
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 48e8088

Please sign in to comment.