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

Possibility to respond to https requests without connecting upstream servers #230

Closed
Closed
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![Build Status](https://travis-ci.org/adamfisk/LittleProxy.png?branch=master)](https://travis-ci.org/adamfisk/LittleProxy)
[![Build Status](https://travis-ci.org/ganskef/LittleProxy-parent.png?branch=master)](https://travis-ci.org/ganskef/LittleProxy-parent)

LittleProxy is a high performance HTTP proxy written in Java atop Trustin Lee's excellent [Netty](netty.io) event-based networking library. It's quite stable, performs well, and is easy to integrate into your projects.

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<groupId>org.littleshoot</groupId>
<artifactId>littleproxy</artifactId>
<packaging>jar</packaging>
<version>1.1.0-beta2-SNAPSHOT</version>
<version>1.1.0-beta2-offline</version>
<name>LittleProxy</name>
<description>
LittleProxy is a high performance HTTP proxy written in Java and using the Netty networking framework.
Expand Down
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();
}
}
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");
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");
channel.config().setAutoRead(true);
}
}

/**
Expand Down
108 changes: 53 additions & 55 deletions src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,11 @@
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;
import org.slf4j.spi.LocationAwareLogger;

import javax.net.ssl.SSLSession;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.LinkedList;
Expand Down Expand Up @@ -129,11 +126,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 @@ -544,55 +536,63 @@ private void connectAndWrite(final 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.
*/
private void initializeConnectionFlow() {
this.connectionFlow = new ConnectionFlow(clientConnection, this,
connectLock)
.then(ConnectChannel);

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) {
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 = 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 (ProxyUtils.isCONNECT(initialRequest)) {
// If we're chaining, forward the CONNECT request
if (hasUpstreamChainedProxy()) {
connectionFlow.then(serverConnection.HTTPCONNECTWithChainedProxy);
}
if (isMitmEnabled()) {
String host;
int port;
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);
host = parsedHostAndPort.getHostText();
port = parsedHostAndPort.getPort();
} else {
host = remoteAddress.getHostName();
port = remoteAddress.getPort();
}
connectionFlow.then(serverConnection.EncryptChannel(proxyServer.getMitmManager()
.serverSslEngine(remoteAddress.getHostName(),
remoteAddress.getPort())));
}

connectionFlow
.then(clientConnection.RespondCONNECTSuccessful)
.then(serverConnection.MitmEncryptClientChannel);
} else {
connectionFlow.then(serverConnection.StartTunneling)
.then(clientConnection.RespondCONNECTSuccessful)
.then(clientConnection.StartTunneling);
.serverSslEngine(host, port)));

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 @@ -653,8 +653,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 @@ -664,7 +662,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 @@ -675,7 +673,7 @@ public void operationComplete(ChannelFuture arg0) throws Exception {
}
}
});
return future;
return future;
} else {
return writeToChannel(initialRequest);
}
Expand Down Expand Up @@ -731,7 +729,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
14 changes: 10 additions & 4 deletions src/main/java/org/littleshoot/proxy/impl/ProxyUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -514,10 +514,16 @@ public static HttpResponse duplicateHttpResponse(HttpResponse originalResponse)
public static String getHostName() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
LOG.warn("Could not lookup localhost", e);
return null;
} catch (IOException e) {
LOG.debug("Ignored exception", e);
} catch (RuntimeException e) {
// An exception here must not stop the proxy. Android could throw a
// runtime exception, since it not allows network access in the main
// process.
LOG.debug("Ignored exception", e);
}
LOG.info("Could not lookup localhost");
return null;
}

/**
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);
}

}