diff --git a/README.md b/README.md
index d512855e6..ded0c47ab 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
[](https://travis-ci.org/adamfisk/LittleProxy)
+[](https://circleci.com/gh/verygoodsecurity/LittleProxy)
LittleProxy is a high performance HTTP proxy written in Java atop Trustin Lee's excellent [Netty](http://netty.io) event-based networking library. It's quite stable, performs well, and is easy to integrate into your projects.
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 000000000..15e0d8fcc
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,33 @@
+---
+machine:
+ java:
+ version: oraclejdk8
+ environment:
+ AWS_PROFILE: vgs-dev
+ RELEASE_BRANCH: vgs-edition
+dependencies:
+ override:
+ - ./env.sh
+ - mvn clean dependency:go-offline install -Dmaven.test.skip=true --fail-never --threads 5 -B
+test:
+ override:
+ - ./scripts/run_circle_tests.sh
+deployment:
+ snapshot:
+ branch: vgs-edition
+ owner: verygoodsecurity
+ commands:
+ - mvn deploy -DskipTests=true
+ release:
+ tag: /.*/
+ commands:
+ - git config user.name "circleci"
+ - git config user.email "circleci@vgs.com"
+ - git fetch
+ - git checkout $RELEASE_BRANCH
+ - git pull origin $RELEASE_BRANCH
+ - git reset --hard
+ - git tag -d $CIRCLE_TAG
+ - mvn -B -X -e gitflow:release-start -DreleaseVersion=$CIRCLE_TAG
+ - mvn -B -X -e gitflow:release-finish -DreleaseVersion=$CIRCLE_TAG -DpostReleaseGoals='deploy -DskipTests'
+ - git push origin $RELEASE_BRANCH
diff --git a/env.sh b/env.sh
new file mode 100755
index 000000000..c5f147a03
--- /dev/null
+++ b/env.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+set -x
+
+mkdir -p ~/.aws
+touch ~/.aws/credentials
+
+echo "
+[vgs-dev]
+region = us-west-2
+role_arn = arn:aws:iam::883127560329:role/StageDeploy
+source_profile = default
+" | tee -a ~/.aws/credentials
+
diff --git a/pom.xml b/pom.xml
index a81bd358f..1008c952f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
org.littleshoot
littleproxy
jar
- 1.1.3-SNAPSHOT
+ 1.1.6.0-VGS-SNAPSHOT
LittleProxy
LittleProxy is a high performance HTTP proxy written in Java and using the Netty networking framework.
@@ -54,16 +54,65 @@
+
+
+ vg-release
+ VG Release Repository
+ s3://vault-dev-01-audits-01-artifact-19k6160zpr44j/software/release/
+
+
- ossrh
- https://oss.sonatype.org/content/repositories/snapshots
+ vg-snapshot
+ VG Snapshot Repository
+ s3://vault-dev-01-audits-01-artifact-19k6160zpr44j/software/snapshot/
+
+
+
+
+ jfog
+ jfrog
+ https://dl.bintray.com/vg/vgs-oss
+
+ true
+
+
+
+
+
+
- ossrh
- https://oss.sonatype.org/service/local/staging/deploy/maven2/
+ jcenter
+ jcenter
+ https://jcenter.bintray.com/
+
+
+
+ verygood-release-repo
+ Very Good Release Repository
+ s3://vault-dev-01-audits-01-artifact-19k6160zpr44j/software/release/
+
+ true
+
+
+ false
+
-
+
+
+ verygood-snapshot-repo
+ Very Good Snapshot Repository
+ s3://vault-dev-01-audits-01-artifact-19k6160zpr44j/software/snapshot/
+
+ false
+
+
+ true
+
+
+
+
2009
@@ -84,7 +133,7 @@
[,1.8)
-
+
@@ -160,15 +209,9 @@
- org.apache.maven.plugins
- maven-release-plugin
- 2.5.3
-
- true
- false
- release
- deploy
-
+ com.amashchenko.maven.plugin
+ gitflow-maven-plugin
+ 1.8.0
@@ -186,7 +229,7 @@
com.google.guava
guava
- 20.0
+ 23.0
@@ -454,6 +497,12 @@
2.5.2
+
+ org.codehaus.mojo
+ buildnumber-maven-plugin
+ 1.4
+
+
org.apache.maven.plugins
maven-jar-plugin
@@ -465,6 +514,12 @@
maven-resources-plugin
3.0.2
+
+
+ com.amashchenko.maven.plugin
+ gitflow-maven-plugin
+ 1.8.0
+
@@ -504,6 +559,59 @@
+
+ org.codehaus.mojo
+ buildnumber-maven-plugin
+ 1.4
+
+
+
+ create
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.0.2
+
+
+
+ true
+
+
+ ${buildNumber}
+
+
+
+
+
+
+ com.amashchenko.maven.plugin
+ gitflow-maven-plugin
+ 1.8.0
+
+ true
+ 2
+ true
+ false
+ true
+ true
+
+ vgs-edition
+ vgs-edition
+ release-
+
+
+
+ update versions for @{version} release
+ update for next development version @{version}
+
+
+
+
org.apache.maven.plugins
maven-shade-plugin
@@ -550,6 +658,15 @@
+
+
+
+ io.vgs.tools
+ aws-maven
+ 1.4.3
+
+
+
diff --git a/scripts/run_circle_tests.sh b/scripts/run_circle_tests.sh
new file mode 100755
index 000000000..21245dc4b
--- /dev/null
+++ b/scripts/run_circle_tests.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+if [ "$CIRCLE_TAG" == "" ]
+then
+ mvn test -T2C
+fi
\ No newline at end of file
diff --git a/src/main/java/org/littleshoot/proxy/DefaultFailureHttpResponseComposer.java b/src/main/java/org/littleshoot/proxy/DefaultFailureHttpResponseComposer.java
new file mode 100644
index 000000000..15b8c2572
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/DefaultFailureHttpResponseComposer.java
@@ -0,0 +1,47 @@
+package org.littleshoot.proxy;
+
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpVersion;
+import org.littleshoot.proxy.impl.ProxyUtils;
+
+public class DefaultFailureHttpResponseComposer implements FailureHttpResponseComposer {
+
+ /**
+ * Tells the client that something went wrong trying to proxy its request. If the Bad Gateway is a response to
+ * an HTTP HEAD request, the response will contain no body, but the Content-Length header will be set to the
+ * value it would have been if this 502 Bad Gateway were in response to a GET.
+ *
+ * @param httpRequest the HttpRequest that is resulting in the Bad Gateway response
+ * @param cause raised exception
+ * @return true if the connection will be kept open, or false if it will be disconnected
+ */
+ @Override
+ public FullHttpResponse compose(HttpRequest httpRequest, Throwable cause) {
+ String body = provideCustomMessage(httpRequest, cause);
+ HttpResponseStatus status = provideCustomStatus(httpRequest, cause);
+
+ FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1, status, body);
+
+ if (ProxyUtils.isHEAD(httpRequest)) {
+ // don't allow any body content in response to a HEAD request
+ response.content().clear();
+ }
+ return response;
+ }
+
+ /**
+ * The method can be overridden to provide a custom message along with 502 code
+ * @param httpRequest initial request
+ * @param cause an exception thrown on a failure
+ * @return custom message
+ */
+ protected String provideCustomMessage(HttpRequest httpRequest, Throwable cause) {
+ return "Bad Gateway: " + httpRequest.getUri();
+ }
+
+ protected HttpResponseStatus provideCustomStatus(HttpRequest httpRequest, Throwable cause) {
+ return HttpResponseStatus.BAD_GATEWAY;
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/ExceptionHandler.java b/src/main/java/org/littleshoot/proxy/ExceptionHandler.java
new file mode 100644
index 000000000..41fc38bdd
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/ExceptionHandler.java
@@ -0,0 +1,11 @@
+package org.littleshoot.proxy;
+
+public interface ExceptionHandler {
+
+ /**
+ * Handles proxy exceptions
+ *
+ * @param cause error cause
+ */
+ void handle(Throwable cause);
+}
diff --git a/src/main/java/org/littleshoot/proxy/FailureHttpResponseComposer.java b/src/main/java/org/littleshoot/proxy/FailureHttpResponseComposer.java
new file mode 100644
index 000000000..54e1e40fa
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/FailureHttpResponseComposer.java
@@ -0,0 +1,18 @@
+package org.littleshoot.proxy;
+
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpRequest;
+
+/**
+ * Interface for objects that can provide a custom http response on a specific failure.
+ */
+public interface FailureHttpResponseComposer {
+
+ /**
+ * Creates an {@link FullHttpResponse} based on initial request and failure cause
+ * @param httpRequest initial request
+ * @param cause an exception thrown during a failure
+ * @return failure http response
+ */
+ FullHttpResponse compose(HttpRequest httpRequest, Throwable cause);
+}
diff --git a/src/main/java/org/littleshoot/proxy/GlobalStateHandler.java b/src/main/java/org/littleshoot/proxy/GlobalStateHandler.java
new file mode 100644
index 000000000..2949f1ffb
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/GlobalStateHandler.java
@@ -0,0 +1,28 @@
+package org.littleshoot.proxy;
+
+import io.netty.channel.Channel;
+
+/**
+ * Netty is not designed for thread local or global state usage.
+ * It guarantees that a channel is handled by only one thread
+ * but that thread is constantly reused by other channels so
+ * the state can be messed up.
+ *
+ * This handler lets serialize the state to a channel so it
+ * can be deserialized when needed.
+ */
+public interface GlobalStateHandler {
+
+ /**
+ * Deserializes global state from channel.
+ *
+ * @param channel client connection channel
+ */
+ void restoreFromChannel(Channel channel);
+
+ /**
+ * Clears global state.
+ */
+ void clear();
+
+}
diff --git a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
index 367dc8dd1..1c064a140 100644
--- a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
+++ b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
@@ -102,7 +102,7 @@ HttpProxyServerBootstrap withTransportProtocol(
*
*
*
- * Note - This and {@link #withManInTheMiddle(MitmManager)} are
+ * Note - This and {@link #withManInTheMiddle(MitmManagerFactory)} are
* mutually exclusive.
*
*
@@ -179,7 +179,67 @@ HttpProxyServerBootstrap withChainProxyManager(
* @return
*/
HttpProxyServerBootstrap withManInTheMiddle(
- MitmManager mitmManager);
+ MitmManagerFactory mitmManager);
+
+ /**
+ *
+ * Specify an {@link ExceptionHandler} to handle client to proxy errors
+ *
+ *
+ *
+ * Default = null
+ *
+ *
+ * @param clientToProxyExHandler
+ * @return exception handler
+ */
+ HttpProxyServerBootstrap withClientToProxyExHandler(
+ ExceptionHandler clientToProxyExHandler);
+
+ /**
+ *
+ * Specify an {@link ExceptionHandler} to handle proxy to server errors
+ *
+ *
+ *
+ * Default = null
+ *
+ *
+ * @param proxyToServerExHandler
+ * @return exception handler
+ */
+ HttpProxyServerBootstrap withProxyToServerExHandler(
+ ExceptionHandler proxyToServerExHandler);
+
+
+ /**
+ *
+ * Specify a {@link RequestTracer} to trace proxy requests
+ *
+ *
+ *
+ * Default = null
+ *
+ *
+ * @param requestTracer
+ * @return proxy server bootstrap
+ */
+ HttpProxyServerBootstrap withRequestTracer(RequestTracer requestTracer);
+
+ /**
+ *
+ * Specify an {@link GlobalStateHandler} to customize a global state based on channel attributes
+ *
+ *
+ *
+ * Default = null
+ *
+ *
+ * @param globalStateHandler
+ * @return proxy server bootstrap
+ */
+ HttpProxyServerBootstrap withCustomGlobalState(
+ GlobalStateHandler globalStateHandler);
/**
*
@@ -197,6 +257,22 @@ HttpProxyServerBootstrap withManInTheMiddle(
HttpProxyServerBootstrap withFiltersSource(
HttpFiltersSource filtersSource);
+ /**
+ *
+ * Specify a {@link FailureHttpResponseComposer} to use for composing
+ * custom response message on unrecoverable failure
+ *
+ *
+ *
+ * Default = {@link DefaultFailureHttpResponseComposer}
+ *
+ *
+ * @param unrecoverableFailureHttpResponseComposer custom response message composer
+ * @return
+ */
+ HttpProxyServerBootstrap withUnrecoverableFailureHttpResponseComposer(
+ FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer);
+
/**
*
* Specify whether or not to use secure DNS lookups for outbound
diff --git a/src/main/java/org/littleshoot/proxy/Launcher.java b/src/main/java/org/littleshoot/proxy/Launcher.java
index f9b654a0e..79a558689 100755
--- a/src/main/java/org/littleshoot/proxy/Launcher.java
+++ b/src/main/java/org/littleshoot/proxy/Launcher.java
@@ -9,7 +9,7 @@
import org.apache.commons.cli.UnrecognizedOptionException;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.xml.DOMConfigurator;
-import org.littleshoot.proxy.extras.SelfSignedMitmManager;
+import org.littleshoot.proxy.extras.SelfSignedMitmManagerFactory;
import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
import org.littleshoot.proxy.impl.ProxyUtils;
import org.slf4j.Logger;
@@ -100,7 +100,7 @@ public static void main(final String... args) {
if (cmd.hasOption(OPTION_MITM)) {
LOG.info("Running as Man in the Middle");
- bootstrap.withManInTheMiddle(new SelfSignedMitmManager());
+ bootstrap.withManInTheMiddle(new SelfSignedMitmManagerFactory());
}
if (cmd.hasOption(OPTION_DNSSEC)) {
diff --git a/src/main/java/org/littleshoot/proxy/MitmManagerFactory.java b/src/main/java/org/littleshoot/proxy/MitmManagerFactory.java
new file mode 100644
index 000000000..21d0ff5e0
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/MitmManagerFactory.java
@@ -0,0 +1,14 @@
+package org.littleshoot.proxy;
+
+import io.netty.channel.Channel;
+
+public interface MitmManagerFactory {
+
+ /**
+ * Retrieves an instance of {@link MitmManager} based on specific attributes from cxt channel
+ *
+ * @param channel current channel from cxt
+ * @return concrete instance of {@link MitmManager}
+ */
+ MitmManager getInstance(Channel channel);
+}
diff --git a/src/main/java/org/littleshoot/proxy/RequestTracer.java b/src/main/java/org/littleshoot/proxy/RequestTracer.java
new file mode 100644
index 000000000..68505e293
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/RequestTracer.java
@@ -0,0 +1,18 @@
+package org.littleshoot.proxy;
+
+import io.netty.channel.Channel;
+
+public interface RequestTracer {
+
+ /**
+ * Start tracing proxy request
+ * @param channel
+ */
+ void start(Channel channel);
+
+ /**
+ * Request is served. Finish tracing.
+ * @param channel
+ */
+ void finish(Channel channel);
+}
diff --git a/src/main/java/org/littleshoot/proxy/extras/SelfSignedMitmManagerFactory.java b/src/main/java/org/littleshoot/proxy/extras/SelfSignedMitmManagerFactory.java
new file mode 100644
index 000000000..02a651596
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/extras/SelfSignedMitmManagerFactory.java
@@ -0,0 +1,15 @@
+package org.littleshoot.proxy.extras;
+
+import io.netty.channel.Channel;
+import org.littleshoot.proxy.MitmManager;
+import org.littleshoot.proxy.MitmManagerFactory;
+
+/**
+ * The factory for self signed mitm manager
+ */
+public class SelfSignedMitmManagerFactory implements MitmManagerFactory {
+ @Override
+ public MitmManager getInstance(Channel channel) {
+ return new SelfSignedMitmManager();
+ }
+}
diff --git a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
index 964858fbf..a4347e9c7 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
@@ -25,6 +25,9 @@
import io.netty.util.concurrent.GenericFutureListener;
import org.apache.commons.lang3.StringUtils;
import org.littleshoot.proxy.ActivityTracker;
+import org.littleshoot.proxy.DefaultFailureHttpResponseComposer;
+import org.littleshoot.proxy.ExceptionHandler;
+import org.littleshoot.proxy.FailureHttpResponseComposer;
import org.littleshoot.proxy.FlowContext;
import org.littleshoot.proxy.FullFlowContext;
import org.littleshoot.proxy.HttpFilters;
@@ -443,7 +446,7 @@ void respond(ProxyToServerConnection serverConnection, HttpFilters filters,
// if this HttpResponse does not have any means of signaling the end of the message body other than closing
// the connection, convert the message to a "Transfer-Encoding: chunked" HTTP response. This avoids the need
// to close the client connection to indicate the end of the message. (Responses to HEAD requests "must be" empty.)
- if (!ProxyUtils.isHEAD(currentHttpRequest) && !ProxyUtils.isResponseSelfTerminating(httpResponse)) {
+ if (currentHttpRequest != null && !ProxyUtils.isHEAD(currentHttpRequest) && !ProxyUtils.isResponseSelfTerminating(httpResponse)) {
// if this is not a FullHttpResponse, duplicate the HttpResponse from the server before sending it to
// the client. this allows us to set the Transfer-Encoding to chunked without interfering with netty's
// handling of the response from the server. if we modify the original HttpResponse from the server,
@@ -616,22 +619,25 @@ protected boolean serverConnectionFailed(
serverConnection.getRemoteAddress(),
lastStateBeforeFailure,
cause);
- connectionFailedUnrecoverably(initialRequest, serverConnection);
+ connectionFailedUnrecoverably(initialRequest, serverConnection, cause);
return false;
}
} catch (UnknownHostException uhe) {
- connectionFailedUnrecoverably(initialRequest, serverConnection);
+ connectionFailedUnrecoverably(initialRequest, serverConnection, cause);
return false;
}
}
- private void connectionFailedUnrecoverably(HttpRequest initialRequest, ProxyToServerConnection serverConnection) {
+ private void connectionFailedUnrecoverably(HttpRequest initialRequest, ProxyToServerConnection serverConnection, Throwable cause) {
// the connection to the server failed, so disconnect the server and remove the ProxyToServerConnection from the
// map of open server connections
serverConnection.disconnect();
this.serverConnectionsByHostAndPort.remove(serverConnection.getServerHostAndPort());
- boolean keepAlive = writeBadGateway(initialRequest);
+ FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer = proxyServer.getUnrecoverableFailureHttpResponseComposer();
+ FullHttpResponse failureResponse = unrecoverableFailureHttpResponseComposer.compose(initialRequest, cause);
+
+ boolean keepAlive = respondWithShortCircuitResponse(failureResponse);
if (keepAlive) {
become(AWAITING_INITIAL);
} else {
@@ -750,7 +756,13 @@ protected void exceptionCaught(Throwable cause) {
LOG.info("An executor rejected a read or write operation on the ClientToProxyConnection (this is normal if the proxy is shutting down). Message: " + cause.getMessage());
LOG.debug("A RejectedExecutionException occurred on ClientToProxyConnection", cause);
} else {
- LOG.error("Caught an exception on ClientToProxyConnection", cause);
+ ExceptionHandler exHandler = proxyServer.getClientToProxyExHandler();
+ if (exHandler != null) {
+ LOG.debug("Custom exception handler '" + exHandler.toString() + "' invoked", cause);
+ exHandler.handle(cause);
+ } else {
+ LOG.error("Caught an exception on ClientToProxyConnection", cause);
+ }
}
} finally {
// always disconnect the client when an exception occurs on the channel
@@ -779,6 +791,14 @@ protected void exceptionCaught(Throwable cause) {
private void initChannelPipeline(ChannelPipeline pipeline) {
LOG.debug("Configuring ChannelPipeline");
+ if (proxyServer.getRequestTracer() != null) {
+ pipeline.addLast("requestTracerHandler", new RequestTracerHandler(this));
+ }
+
+ if (proxyServer.getGlobalStateHandler() != null) {
+ pipeline.addLast("inboundGlobalStateHandler", new InboundGlobalStateHandler(this));
+ }
+
pipeline.addLast("bytesReadMonitor", bytesReadMonitor);
pipeline.addLast("bytesWrittenMonitor", bytesWrittenMonitor);
@@ -805,7 +825,12 @@ private void initChannelPipeline(ChannelPipeline pipeline) {
new IdleStateHandler(0, 0, proxyServer
.getIdleConnectionTimeout()));
+ if (proxyServer.getGlobalStateHandler() != null) {
+ pipeline.addLast("outboundGlobalStateHandler", new OutboundGlobalStateHandler(this));
+ }
+
pipeline.addLast("handler", this);
+
}
/**
@@ -866,7 +891,7 @@ private boolean shouldCloseClientConnection(HttpRequest req,
}
}
- if (!HttpHeaders.isKeepAlive(req)) {
+ if (req != null && !HttpHeaders.isKeepAlive(req)) {
LOG.debug("Closing client connection since request is not keep alive: {}", req);
// Here we simply want to close the connection because the
// client itself has requested it be closed in the request.
@@ -1203,15 +1228,8 @@ private void stripHopByHopHeaders(HttpHeaders headers) {
* @return true if the connection will be kept open, or false if it will be disconnected
*/
private boolean writeBadGateway(HttpRequest httpRequest) {
- String body = "Bad Gateway: " + httpRequest.getUri();
- FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY, body);
-
- if (ProxyUtils.isHEAD(httpRequest)) {
- // don't allow any body content in response to a HEAD request
- response.content().clear();
- }
-
- return respondWithShortCircuitResponse(response);
+ FullHttpResponse badGatewayResponse = new DefaultFailureHttpResponseComposer().compose(httpRequest, null);
+ return respondWithShortCircuitResponse(badGatewayResponse);
}
/**
diff --git a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java
index 1891532e4..32ad0195f 100644
--- a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java
+++ b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java
@@ -17,6 +17,8 @@
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.littleshoot.proxy.ActivityTracker;
+import org.littleshoot.proxy.GlobalStateHandler;
+import org.littleshoot.proxy.DefaultFailureHttpResponseComposer;
import org.littleshoot.proxy.ChainedProxyManager;
import org.littleshoot.proxy.DefaultHostResolver;
import org.littleshoot.proxy.DnsSecServerResolver;
@@ -26,8 +28,12 @@
import org.littleshoot.proxy.HttpFiltersSourceAdapter;
import org.littleshoot.proxy.HttpProxyServer;
import org.littleshoot.proxy.HttpProxyServerBootstrap;
+import org.littleshoot.proxy.FailureHttpResponseComposer;
import org.littleshoot.proxy.MitmManager;
+import org.littleshoot.proxy.MitmManagerFactory;
import org.littleshoot.proxy.ProxyAuthenticator;
+import org.littleshoot.proxy.ExceptionHandler;
+import org.littleshoot.proxy.RequestTracer;
import org.littleshoot.proxy.SslEngineSource;
import org.littleshoot.proxy.TransportProtocol;
import org.littleshoot.proxy.UnknownTransportProtocolException;
@@ -106,8 +112,13 @@ public class DefaultHttpProxyServer implements HttpProxyServer {
private final boolean authenticateSslClients;
private final ProxyAuthenticator proxyAuthenticator;
private final ChainedProxyManager chainProxyManager;
- private final MitmManager mitmManager;
+ private final MitmManagerFactory mitmManagerFactory;
+ private final ExceptionHandler clientToProxyExHandler;
+ private final ExceptionHandler proxyToServerExHandler;
+ private final RequestTracer requestTracer;
+ private final GlobalStateHandler globalStateHandler;
private final HttpFiltersSource filtersSource;
+ private final FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer;
private final boolean transparent;
private volatile int connectTimeout;
private volatile int idleConnectionTimeout;
@@ -202,7 +213,7 @@ public static HttpProxyServerBootstrap bootstrapFromFile(String path) {
* @param chainProxyManager
* The proxy to send requests to if chaining proxies. Typically
* null.
- * @param mitmManager
+ * @param mitmManagerFactory
* The {@link MitmManager} to use for man in the middle'ing
* CONNECT requests
* @param filtersSource
@@ -238,8 +249,13 @@ private DefaultHttpProxyServer(ServerGroup serverGroup,
boolean authenticateSslClients,
ProxyAuthenticator proxyAuthenticator,
ChainedProxyManager chainProxyManager,
- MitmManager mitmManager,
+ MitmManagerFactory mitmManagerFactory,
+ ExceptionHandler clientToProxyExHandler,
+ ExceptionHandler proxyToServerExHandler,
+ RequestTracer requestTracer,
+ GlobalStateHandler globalStateHandler,
HttpFiltersSource filtersSource,
+ FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer,
boolean transparent,
int idleConnectionTimeout,
Collection activityTrackers,
@@ -260,8 +276,13 @@ private DefaultHttpProxyServer(ServerGroup serverGroup,
this.authenticateSslClients = authenticateSslClients;
this.proxyAuthenticator = proxyAuthenticator;
this.chainProxyManager = chainProxyManager;
- this.mitmManager = mitmManager;
+ this.mitmManagerFactory = mitmManagerFactory;
+ this.clientToProxyExHandler = clientToProxyExHandler;
+ this.proxyToServerExHandler = proxyToServerExHandler;
+ this.requestTracer = requestTracer;
+ this.globalStateHandler = globalStateHandler;
this.filtersSource = filtersSource;
+ this.unrecoverableFailureHttpResponseComposer = unrecoverableFailureHttpResponseComposer;
this.transparent = transparent;
this.idleConnectionTimeout = idleConnectionTimeout;
if (activityTrackers != null) {
@@ -394,8 +415,13 @@ public HttpProxyServerBootstrap clone() {
authenticateSslClients,
proxyAuthenticator,
chainProxyManager,
- mitmManager,
+ mitmManagerFactory,
+ clientToProxyExHandler,
+ proxyToServerExHandler,
+ requestTracer,
+ globalStateHandler,
filtersSource,
+ unrecoverableFailureHttpResponseComposer,
transparent,
idleConnectionTimeout,
activityTrackers,
@@ -565,8 +591,27 @@ protected ChainedProxyManager getChainProxyManager() {
return chainProxyManager;
}
- protected MitmManager getMitmManager() {
- return mitmManager;
+ protected MitmManager getMitmManager(Channel channel) {
+ if (mitmManagerFactory != null) {
+ return mitmManagerFactory.getInstance(channel);
+ }
+ return null;
+ }
+
+ protected ExceptionHandler getClientToProxyExHandler() {
+ return clientToProxyExHandler;
+ }
+
+ protected ExceptionHandler getProxyToServerExHandler() {
+ return proxyToServerExHandler;
+ }
+
+ protected GlobalStateHandler getGlobalStateHandler() {
+ return globalStateHandler;
+ }
+
+ protected RequestTracer getRequestTracer() {
+ return requestTracer;
}
protected SslEngineSource getSslEngineSource() {
@@ -581,6 +626,10 @@ public HttpFiltersSource getFiltersSource() {
return filtersSource;
}
+ public FailureHttpResponseComposer getUnrecoverableFailureHttpResponseComposer() {
+ return unrecoverableFailureHttpResponseComposer;
+ }
+
protected Collection getActivityTrackers() {
return activityTrackers;
}
@@ -606,8 +655,13 @@ private static class DefaultHttpProxyServerBootstrap implements HttpProxyServerB
private boolean authenticateSslClients = true;
private ProxyAuthenticator proxyAuthenticator = null;
private ChainedProxyManager chainProxyManager = null;
- private MitmManager mitmManager = null;
+ private MitmManagerFactory mitmManagerFactory = null;
+ private ExceptionHandler clientToProxyExHandler = null;
+ private ExceptionHandler proxyToServerExHandler = null;
+ private RequestTracer requestTracer = null;
+ private GlobalStateHandler globalStateHandler = null;
private HttpFiltersSource filtersSource = new HttpFiltersSourceAdapter();
+ private FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer = new DefaultFailureHttpResponseComposer();
private boolean transparent = false;
private int idleConnectionTimeout = 70;
private Collection activityTrackers = new ConcurrentLinkedQueue();
@@ -636,8 +690,13 @@ private DefaultHttpProxyServerBootstrap(
boolean authenticateSslClients,
ProxyAuthenticator proxyAuthenticator,
ChainedProxyManager chainProxyManager,
- MitmManager mitmManager,
+ MitmManagerFactory mitmManagerFactory,
+ ExceptionHandler clientToProxyExHandler,
+ ExceptionHandler proxyToServerExHandler,
+ RequestTracer requestTracer,
+ GlobalStateHandler globalStateHandler,
HttpFiltersSource filtersSource,
+ FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer,
boolean transparent, int idleConnectionTimeout,
Collection activityTrackers,
int connectTimeout, HostResolver serverResolver,
@@ -657,8 +716,13 @@ private DefaultHttpProxyServerBootstrap(
this.authenticateSslClients = authenticateSslClients;
this.proxyAuthenticator = proxyAuthenticator;
this.chainProxyManager = chainProxyManager;
- this.mitmManager = mitmManager;
+ this.mitmManagerFactory = mitmManagerFactory;
+ this.clientToProxyExHandler = clientToProxyExHandler;
+ this.proxyToServerExHandler = proxyToServerExHandler;
+ this.requestTracer = requestTracer;
+ this.globalStateHandler = globalStateHandler;
this.filtersSource = filtersSource;
+ this.unrecoverableFailureHttpResponseComposer = unrecoverableFailureHttpResponseComposer;
this.transparent = transparent;
this.idleConnectionTimeout = idleConnectionTimeout;
if (activityTrackers != null) {
@@ -749,10 +813,10 @@ public HttpProxyServerBootstrap withListenOnAllAddresses(boolean listenOnAllAddr
public HttpProxyServerBootstrap withSslEngineSource(
SslEngineSource sslEngineSource) {
this.sslEngineSource = sslEngineSource;
- if (this.mitmManager != null) {
+ if (this.mitmManagerFactory != null) {
LOG.warn("Enabled encrypted inbound connections with man in the middle. "
+ "These are mutually exclusive - man in the middle will be disabled.");
- this.mitmManager = null;
+ this.mitmManagerFactory = null;
}
return this;
}
@@ -780,8 +844,8 @@ public HttpProxyServerBootstrap withChainProxyManager(
@Override
public HttpProxyServerBootstrap withManInTheMiddle(
- MitmManager mitmManager) {
- this.mitmManager = mitmManager;
+ MitmManagerFactory mitmManagerFactory) {
+ this.mitmManagerFactory = mitmManagerFactory;
if (this.sslEngineSource != null) {
LOG.warn("Enabled man in the middle with encrypted inbound connections. "
+ "These are mutually exclusive - encrypted inbound connections will be disabled.");
@@ -790,6 +854,34 @@ public HttpProxyServerBootstrap withManInTheMiddle(
return this;
}
+ @Override
+ public HttpProxyServerBootstrap withProxyToServerExHandler(
+ ExceptionHandler proxyToServerExHandler) {
+ this.proxyToServerExHandler = proxyToServerExHandler;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withClientToProxyExHandler(
+ ExceptionHandler clientToProxyExHandler) {
+ this.clientToProxyExHandler = clientToProxyExHandler;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withRequestTracer(
+ RequestTracer requestTracer) {
+ this.requestTracer = requestTracer;
+ return this;
+ }
+
+ @Override
+ public HttpProxyServerBootstrap withCustomGlobalState(
+ GlobalStateHandler globalStateHandler) {
+ this.globalStateHandler = globalStateHandler;
+ return this;
+ }
+
@Override
public HttpProxyServerBootstrap withFiltersSource(
HttpFiltersSource filtersSource) {
@@ -797,6 +889,12 @@ public HttpProxyServerBootstrap withFiltersSource(
return this;
}
+ public HttpProxyServerBootstrap withUnrecoverableFailureHttpResponseComposer(
+ FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer) {
+ this.unrecoverableFailureHttpResponseComposer = unrecoverableFailureHttpResponseComposer;
+ return this;
+ }
+
@Override
public HttpProxyServerBootstrap withUseDnsSec(boolean useDnsSec) {
if (useDnsSec) {
@@ -899,8 +997,9 @@ private DefaultHttpProxyServer build() {
return new DefaultHttpProxyServer(serverGroup,
transportProtocol, determineListenAddress(),
sslEngineSource, authenticateSslClients,
- proxyAuthenticator, chainProxyManager, mitmManager,
- filtersSource, transparent,
+ proxyAuthenticator, chainProxyManager, mitmManagerFactory,
+ clientToProxyExHandler, proxyToServerExHandler, requestTracer, globalStateHandler,
+ filtersSource, unrecoverableFailureHttpResponseComposer, transparent,
idleConnectionTimeout, activityTrackers, connectTimeout,
serverResolver, readThrottleBytesPerSecond, writeThrottleBytesPerSecond,
localAddress, proxyAlias, maxInitialLineLength, maxHeaderSize, maxChunkSize,
diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
index 58c3eb240..fc2d89b6e 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
@@ -62,6 +62,7 @@
*/
abstract class ProxyConnection extends
SimpleChannelInboundHandler {
+
protected final ProxyConnectionLogger LOG = new ProxyConnectionLogger(this);
protected final DefaultHttpProxyServer proxyServer;
@@ -742,6 +743,87 @@ public void channelRead(ChannelHandlerContext ctx, Object msg)
protected abstract void responseRead(HttpResponse httpResponse);
}
+ @Sharable
+ protected class RequestTracerHandler extends ChannelDuplexHandler {
+
+ private final ProxyConnection clientToProxyConnection;
+
+ RequestTracerHandler(ProxyConnection clientToProxyConnection) {
+ this.clientToProxyConnection = clientToProxyConnection;
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ try {
+ proxyServer.getRequestTracer().start(clientToProxyConnection.channel);
+ } catch (Throwable t) {
+ LOG.warn("Unable to start tracing request", t);
+ } finally {
+ super.channelRead(ctx, msg);
+ }
+ }
+
+ @Override
+ public void write(ChannelHandlerContext ctx,
+ Object msg, ChannelPromise promise) throws Exception {
+ try {
+ super.write(ctx, msg, promise);
+ } finally {
+ try {
+ proxyServer.getRequestTracer().finish(clientToProxyConnection.channel);
+ } catch (Throwable t) {
+ LOG.warn("Unable to finish request tracing", t);
+ }
+ }
+ }
+ }
+
+ @Sharable
+ protected class InboundGlobalStateHandler extends
+ ChannelInboundHandlerAdapter {
+
+ private final ProxyConnection clientToProxyConnection;
+
+ InboundGlobalStateHandler(ProxyConnection clientToProxyConnection) {
+ this.clientToProxyConnection = clientToProxyConnection;
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg)
+ throws Exception {
+ try {
+ proxyServer.getGlobalStateHandler().restoreFromChannel(clientToProxyConnection.channel);
+ super.channelRead(ctx, msg);
+ } finally {
+ proxyServer.getGlobalStateHandler().clear();
+ }
+ }
+ }
+
+ @Sharable
+ protected class OutboundGlobalStateHandler extends
+ ChannelOutboundHandlerAdapter {
+
+ private final ProxyConnection clientToProxyConnection;
+
+ OutboundGlobalStateHandler(ProxyConnection clientToProxyConnection) {
+ this.clientToProxyConnection = clientToProxyConnection;
+ }
+
+ @Override
+ public void write(ChannelHandlerContext ctx,
+ Object msg, ChannelPromise promise)
+ throws Exception {
+ try {
+ proxyServer.getGlobalStateHandler().restoreFromChannel(clientToProxyConnection.channel);
+ super.write(ctx, msg, promise);
+ } finally {
+ proxyServer.getGlobalStateHandler().clear();
+ }
+ }
+ }
+
+
/**
* Utility handler for monitoring bytes written on this connection.
*/
diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java
index 2c9cece42..c0ea3390b 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java
@@ -29,6 +29,7 @@
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
+import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCounted;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
@@ -39,9 +40,11 @@
import org.littleshoot.proxy.FullFlowContext;
import org.littleshoot.proxy.HttpFilters;
import org.littleshoot.proxy.MitmManager;
+import org.littleshoot.proxy.ExceptionHandler;
import org.littleshoot.proxy.TransportProtocol;
import org.littleshoot.proxy.UnknownTransportProtocolException;
+import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLProtocolException;
import javax.net.ssl.SSLSession;
import java.io.IOException;
@@ -137,7 +140,9 @@ public class ProxyToServerConnection extends ProxyConnection {
* Minimum size of the adaptive recv buffer when throttling is enabled.
*/
private static final int MINIMUM_RECV_BUFFER_SIZE_BYTES = 64;
-
+
+ public static final AttributeKey REMOTE_ADDRESS_ATTR_KEY = AttributeKey.valueOf("remoteAddressAttrKey");
+
/**
* Create a new ProxyToServerConnection.
*
@@ -439,7 +444,13 @@ protected void exceptionCaught(Throwable cause) {
LOG.info("An executor rejected a read or write operation on the ProxyToServerConnection (this is normal if the proxy is shutting down). Message: " + cause.getMessage());
LOG.debug("A RejectedExecutionException occurred on ProxyToServerConnection", cause);
} else {
- LOG.error("Caught an exception on ProxyToServerConnection", cause);
+ ExceptionHandler exHandler = proxyServer.getProxyToServerExHandler();
+ if (exHandler != null) {
+ LOG.debug("Custom exception handler '" + exHandler.toString() + "' invoked", cause);
+ exHandler.handle(cause);
+ } else {
+ LOG.error("Caught an exception on ProxyToServerConnection", cause);
+ }
}
} finally {
if (!is(DISCONNECTED)) {
@@ -528,6 +539,8 @@ private void respondWith(HttpObject httpObject) {
private void connectAndWrite(HttpRequest initialRequest) {
LOG.debug("Starting new connection to: {}", remoteAddress);
+ this.clientConnection.channel.attr(REMOTE_ADDRESS_ATTR_KEY).set(remoteAddress);
+
// Remember our initial request so that we can write it after connecting
this.initialRequest = initialRequest;
initializeConnectionFlow();
@@ -545,8 +558,11 @@ private void initializeConnectionFlow() {
.then(ConnectChannel);
if (chainedProxy != null && chainedProxy.requiresEncryption()) {
- connectionFlow.then(serverConnection.EncryptChannel(chainedProxy
- .newSslEngine()));
+ InetSocketAddress proxyAddress = chainedProxy.getChainedProxyAddress();
+
+ SSLEngine engine = proxyAddress == null || proxyAddress.isUnresolved() ? chainedProxy.newSslEngine() :
+ chainedProxy.newSslEngine(proxyAddress.getHostName(), proxyAddress.getPort());
+ connectionFlow.then(serverConnection.EncryptChannel(engine));
}
if (ProxyUtils.isCONNECT(initialRequest)) {
@@ -554,9 +570,9 @@ private void initializeConnectionFlow() {
if (hasUpstreamChainedProxy()) {
connectionFlow.then(
serverConnection.HTTPCONNECTWithChainedProxy);
- }
+ }
- MitmManager mitmManager = proxyServer.getMitmManager();
+ MitmManager mitmManager = proxyServer.getMitmManager(clientConnection.channel);
boolean isMitmEnabled = mitmManager != null;
if (isMitmEnabled) {
@@ -568,11 +584,11 @@ private void initializeConnectionFlow() {
// 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()
+ connectionFlow.then(serverConnection.EncryptChannel(proxyServer.getMitmManager(clientConnection.channel)
.serverSslEngine()));
} else {
- connectionFlow.then(serverConnection.EncryptChannel(proxyServer.getMitmManager()
- .serverSslEngine(parsedHostAndPort.getHostText(), parsedHostAndPort.getPort())));
+ connectionFlow.then(serverConnection.EncryptChannel(proxyServer.getMitmManager(clientConnection.channel)
+ .serverSslEngine(parsedHostAndPort.getHost(), parsedHostAndPort.getPort())));
}
connectionFlow
@@ -643,7 +659,7 @@ 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();
+ MitmManager mitmManager = proxyServer.getMitmManager(clientConnection.channel);
boolean isMitmEnabled = mitmManager != null;
/*
* We ignore the LastHttpContent which we read from the client
@@ -720,7 +736,7 @@ boolean shouldSuppressInitialRequest() {
@Override
protected Future> execute() {
return clientConnection
- .encrypt(proxyServer.getMitmManager()
+ .encrypt(proxyServer.getMitmManager(clientConnection.channel)
.clientSslEngineFor(initialRequest, sslEngine.getSession()), false)
.addListener(
new GenericFutureListener>() {
@@ -869,6 +885,10 @@ private void setupConnectionParameters() throws UnknownHostException {
private void initChannelPipeline(ChannelPipeline pipeline,
HttpRequest httpRequest) {
+ if (proxyServer.getGlobalStateHandler() != null) {
+ pipeline.addLast("inboundGlobalStateHandler", new InboundGlobalStateHandler(clientConnection));
+ }
+
if (trafficHandler != null) {
pipeline.addLast("global-traffic-shaping", trafficHandler);
}
@@ -898,6 +918,10 @@ private void initChannelPipeline(ChannelPipeline pipeline,
new IdleStateHandler(0, 0, proxyServer
.getIdleConnectionTimeout()));
+ if (proxyServer.getGlobalStateHandler() != null) {
+ pipeline.addLast("outboundGlobalStateHandler", new OutboundGlobalStateHandler(clientConnection));
+ }
+
pipeline.addLast("handler", this);
}
@@ -958,7 +982,7 @@ public static InetSocketAddress addressFor(String hostAndPort, DefaultHttpProxyS
throw new UnknownHostException(hostAndPort);
}
- String host = parsedHostAndPort.getHostText();
+ String host = parsedHostAndPort.getHost();
int port = parsedHostAndPort.getPortOrDefault(80);
return proxyServer.getServerResolver().resolve(host, port);
diff --git a/src/test/java/org/littleshoot/proxy/BadClientAuthenticationTCPChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/BadClientAuthenticationTCPChainedProxyTest.java
index 1ef321f95..e19aaa55b 100644
--- a/src/test/java/org/littleshoot/proxy/BadClientAuthenticationTCPChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/BadClientAuthenticationTCPChainedProxyTest.java
@@ -47,6 +47,11 @@ public boolean requiresEncryption() {
public SSLEngine newSslEngine() {
return clientSslEngineSource.newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return clientSslEngineSource.newSslEngine(peerHost, peerPort);
+ }
};
}
}
diff --git a/src/test/java/org/littleshoot/proxy/BadServerAuthenticationTCPChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/BadServerAuthenticationTCPChainedProxyTest.java
index e75c87d12..12a6a324e 100644
--- a/src/test/java/org/littleshoot/proxy/BadServerAuthenticationTCPChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/BadServerAuthenticationTCPChainedProxyTest.java
@@ -47,6 +47,11 @@ public boolean requiresEncryption() {
public SSLEngine newSslEngine() {
return clientSslEngineSource.newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return clientSslEngineSource.newSslEngine(peerHost, peerPort);
+ }
};
}
}
diff --git a/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToOtherChainedProxyDueToSSLTest.java b/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToOtherChainedProxyDueToSSLTest.java
index c16ffa9d1..48665d968 100644
--- a/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToOtherChainedProxyDueToSSLTest.java
+++ b/src/test/java/org/littleshoot/proxy/ChainedProxyWithFallbackToOtherChainedProxyDueToSSLTest.java
@@ -41,6 +41,11 @@ public boolean requiresEncryption() {
public SSLEngine newSslEngine() {
return serverSslEngineSource.newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return serverSslEngineSource.newSslEngine(peerHost, peerPort);
+ }
});
}
};
diff --git a/src/test/java/org/littleshoot/proxy/ClientAuthenticationNotRequiredTCPChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/ClientAuthenticationNotRequiredTCPChainedProxyTest.java
index 7a881311b..a05a540a1 100644
--- a/src/test/java/org/littleshoot/proxy/ClientAuthenticationNotRequiredTCPChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/ClientAuthenticationNotRequiredTCPChainedProxyTest.java
@@ -43,6 +43,11 @@ public boolean requiresEncryption() {
public SSLEngine newSslEngine() {
return clientSslEngineSource.newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return clientSslEngineSource.newSslEngine(peerHost, peerPort);
+ }
};
}
}
diff --git a/src/test/java/org/littleshoot/proxy/CustomClientToProxyExHandlerTest.java b/src/test/java/org/littleshoot/proxy/CustomClientToProxyExHandlerTest.java
new file mode 100644
index 000000000..3f07896b8
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/CustomClientToProxyExHandlerTest.java
@@ -0,0 +1,51 @@
+package org.littleshoot.proxy;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.HttpRequest;
+import org.apache.http.NoHttpResponseException;
+import org.junit.Assert;
+import org.junit.Test;
+import org.littleshoot.proxy.extras.SelfSignedMitmManagerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CustomClientToProxyExHandlerTest extends AbstractProxyTest {
+
+ private final List customExHandlerEntered = new ArrayList<>();
+
+ private static final String EXCEPTION_MESSAGE = "Error occurred in client to proxy connection";
+
+ @Override
+ protected void setUp() {
+ this.proxyServer = bootstrapProxy()
+ .withPort(0)
+ .withManInTheMiddle(new SelfSignedMitmManagerFactory())
+ .withClientToProxyExHandler(new ExceptionHandler() {
+ @Override
+ public void handle(Throwable cause) {
+ customExHandlerEntered.add(cause);
+ }
+ })
+ .withFiltersSource(new HttpFiltersSourceAdapter() {
+ @Override
+ public HttpFilters filterRequest(HttpRequest originalRequest,
+ ChannelHandlerContext ctx) {
+ throw new RuntimeException(EXCEPTION_MESSAGE);
+ }
+ })
+ .start();
+ }
+
+ @Test
+ public void testCustomClientToProxyExHandler() throws Exception {
+ try {
+ httpGetWithApacheClient(webHost, DEFAULT_RESOURCE, true, true);
+ } catch (NoHttpResponseException e) {
+ // expected
+ }
+ Assert.assertFalse("Custom ex handler was not called", customExHandlerEntered.isEmpty());
+ Assert.assertEquals("Incorrect exception was passed to custom ex handles",
+ customExHandlerEntered.get(0).getMessage(), EXCEPTION_MESSAGE);
+ }
+}
diff --git a/src/test/java/org/littleshoot/proxy/CustomProxyToServerExHandlerTest.java b/src/test/java/org/littleshoot/proxy/CustomProxyToServerExHandlerTest.java
new file mode 100644
index 000000000..0a791acda
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/CustomProxyToServerExHandlerTest.java
@@ -0,0 +1,44 @@
+package org.littleshoot.proxy;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.littleshoot.proxy.extras.SelfSignedMitmManagerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CustomProxyToServerExHandlerTest extends MitmWithBadServerAuthenticationTCPChainedProxyTest {
+
+ private final List customExHandlerEntered = new ArrayList<>();
+
+ @Override
+ protected void setUp() {
+ this.upstreamProxy = upstreamProxy().start();
+
+ this.proxyServer = bootstrapProxy()
+ .withPort(0)
+ .withChainProxyManager(chainedProxyManager())
+ .plusActivityTracker(DOWNSTREAM_TRACKER)
+ .withManInTheMiddle(new SelfSignedMitmManagerFactory())
+ .withProxyToServerExHandler(new ExceptionHandler() {
+ @Override
+ public void handle(Throwable cause) {
+ customExHandlerEntered.add(cause);
+ }
+ })
+ .start();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ this.upstreamProxy.abort();
+ }
+
+ @Test
+ public void testCustomProxyToServerExHandler() throws Exception {
+ super.testSimpleGetRequestOverHTTPS();
+ Assert.assertFalse("Custom ex handler was not called", customExHandlerEntered.isEmpty());
+ Assert.assertEquals("Incorrect exception was passed to custom ex handles",
+ customExHandlerEntered.get(0).getMessage(), "javax.net.ssl.SSLHandshakeException: General SSLEngine problem");
+ }
+}
diff --git a/src/test/java/org/littleshoot/proxy/DefaultFailureHttpResponseComposerTest.java b/src/test/java/org/littleshoot/proxy/DefaultFailureHttpResponseComposerTest.java
new file mode 100644
index 000000000..1c8466c14
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/DefaultFailureHttpResponseComposerTest.java
@@ -0,0 +1,80 @@
+package org.littleshoot.proxy;
+
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+public class DefaultFailureHttpResponseComposerTest {
+
+ private static final String REQUEST_URI = "https://localhost/hi";
+
+ @Test
+ public void testDefault() throws IOException {
+ FailureHttpResponseComposer badGatewayResponseComposer = new DefaultFailureHttpResponseComposer();
+
+ HttpRequest initialRequest = mock(HttpRequest.class);
+ when(initialRequest.getUri()).thenReturn(REQUEST_URI);
+
+ FullHttpResponse response = badGatewayResponseComposer.compose(initialRequest, new RuntimeException());
+
+ assertEquals(502, response.getStatus().code());
+ assertEquals("Bad Gateway", response.getStatus().reasonPhrase());
+ assertEquals("Bad Gateway: " + REQUEST_URI, new String(response.content().array()));
+ }
+
+ @Test
+ public void testCustomMessageAndStatus() throws IOException {
+ FailureHttpResponseComposer badGatewayResponseComposer = new DefaultFailureHttpResponseComposer() {
+ @Override
+ protected String provideCustomMessage(HttpRequest httpRequest, Throwable cause) {
+ return "Invalid certificate: " + httpRequest.getUri();
+ }
+
+ @Override
+ protected HttpResponseStatus provideCustomStatus(HttpRequest httpRequest, Throwable cause) {
+ return new HttpResponseStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), "Something is wrong");
+ }
+ };
+
+ HttpRequest initialRequest = mock(HttpRequest.class);
+ when(initialRequest.getUri()).thenReturn(REQUEST_URI);
+
+ FullHttpResponse response = badGatewayResponseComposer.compose(initialRequest, new RuntimeException());
+
+ assertEquals(500, response.getStatus().code());
+ assertEquals("Something is wrong", response.getStatus().reasonPhrase());
+ assertEquals("Invalid certificate: " + REQUEST_URI, new String(response.content().array()));
+ }
+
+ @Test
+ public void testClearedContent() throws IOException {
+ FailureHttpResponseComposer badGatewayResponseComposer = new DefaultFailureHttpResponseComposer();
+
+ HttpRequest initialRequest = mock(HttpRequest.class);
+ when(initialRequest.getUri()).thenReturn(REQUEST_URI);
+
+ FullHttpResponse response = badGatewayResponseComposer.compose(initialRequest, new RuntimeException());
+
+ assertEquals(502, response.getStatus().code());
+
+ assertEquals(0, response.content().readerIndex());
+ assertNotEquals(0, response.content().writerIndex());
+
+ when(initialRequest.getMethod()).thenReturn(HttpMethod.HEAD);
+
+ response = badGatewayResponseComposer.compose(initialRequest, new RuntimeException());
+
+ assertEquals(502, response.getStatus().code());
+
+ assertEquals(0, response.content().readerIndex());
+ assertEquals(0, response.content().writerIndex());
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/littleshoot/proxy/EncryptedTCPChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/EncryptedTCPChainedProxyTest.java
index 32261035b..ea5aad723 100644
--- a/src/test/java/org/littleshoot/proxy/EncryptedTCPChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/EncryptedTCPChainedProxyTest.java
@@ -34,6 +34,11 @@ public boolean requiresEncryption() {
public SSLEngine newSslEngine() {
return sslEngineSource.newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return sslEngineSource.newSslEngine(peerHost, peerPort);
+ }
};
}
}
diff --git a/src/test/java/org/littleshoot/proxy/EncryptedUDTChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/EncryptedUDTChainedProxyTest.java
index b5728caca..086da00b2 100644
--- a/src/test/java/org/littleshoot/proxy/EncryptedUDTChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/EncryptedUDTChainedProxyTest.java
@@ -34,6 +34,11 @@ public boolean requiresEncryption() {
public SSLEngine newSslEngine() {
return sslEngineSource.newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return sslEngineSource.newSslEngine(peerHost, peerPort);
+ }
};
}
}
diff --git a/src/test/java/org/littleshoot/proxy/HttpFilterTest.java b/src/test/java/org/littleshoot/proxy/HttpFilterTest.java
index 56e3a229e..ca786aa3c 100644
--- a/src/test/java/org/littleshoot/proxy/HttpFilterTest.java
+++ b/src/test/java/org/littleshoot/proxy/HttpFilterTest.java
@@ -633,6 +633,11 @@ public SSLEngine newSslEngine() {
// use the same "bad" keystore as BadServerAuthenticationTCPChainedProxyTest
return new SelfSignedSslEngineSource("chain_proxy_keystore_2.jks").newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return new SelfSignedSslEngineSource("chain_proxy_keystore_2.jks").newSslEngine(peerHost, peerPort);
+ }
});
}
})
diff --git a/src/test/java/org/littleshoot/proxy/MITMUsernamePasswordAuthenticatingProxyTest.java b/src/test/java/org/littleshoot/proxy/MITMUsernamePasswordAuthenticatingProxyTest.java
index 48369cdd0..3de5877b8 100644
--- a/src/test/java/org/littleshoot/proxy/MITMUsernamePasswordAuthenticatingProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/MITMUsernamePasswordAuthenticatingProxyTest.java
@@ -1,6 +1,6 @@
package org.littleshoot.proxy;
-import org.littleshoot.proxy.extras.SelfSignedMitmManager;
+import org.littleshoot.proxy.extras.SelfSignedMitmManagerFactory;
/**
* Tests a single proxy that requires username/password authentication and that
@@ -14,7 +14,7 @@ protected void setUp() {
this.proxyServer = bootstrapProxy()
.withPort(0)
.withProxyAuthenticator(this)
- .withManInTheMiddle(new SelfSignedMitmManager())
+ .withManInTheMiddle(new SelfSignedMitmManagerFactory())
.start();
}
diff --git a/src/test/java/org/littleshoot/proxy/MitmProxyTest.java b/src/test/java/org/littleshoot/proxy/MitmProxyTest.java
index 7b2853a19..2a2dda413 100644
--- a/src/test/java/org/littleshoot/proxy/MitmProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/MitmProxyTest.java
@@ -5,11 +5,10 @@
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
-import org.littleshoot.proxy.extras.SelfSignedMitmManager;
+import org.littleshoot.proxy.extras.SelfSignedMitmManagerFactory;
import java.nio.charset.Charset;
import java.util.HashSet;
-import java.util.Queue;
import java.util.Set;
import static org.hamcrest.Matchers.hasItem;
@@ -31,7 +30,7 @@ public class MitmProxyTest extends BaseProxyTest {
protected void setUp() {
this.proxyServer = bootstrapProxy()
.withPort(0)
- .withManInTheMiddle(new SelfSignedMitmManager())
+ .withManInTheMiddle(new SelfSignedMitmManagerFactory())
.withFiltersSource(new HttpFiltersSourceAdapter() {
@Override
public HttpFilters filterRequest(HttpRequest originalRequest) {
diff --git a/src/test/java/org/littleshoot/proxy/MitmWithBadClientAuthenticationTCPChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/MitmWithBadClientAuthenticationTCPChainedProxyTest.java
index e3b724c60..8789ce553 100644
--- a/src/test/java/org/littleshoot/proxy/MitmWithBadClientAuthenticationTCPChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/MitmWithBadClientAuthenticationTCPChainedProxyTest.java
@@ -48,6 +48,11 @@ public boolean requiresEncryption() {
public SSLEngine newSslEngine() {
return clientSslEngineSource.newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return clientSslEngineSource.newSslEngine(peerHost, peerPort);
+ }
};
}
}
diff --git a/src/test/java/org/littleshoot/proxy/MitmWithBadServerAuthenticationTCPChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/MitmWithBadServerAuthenticationTCPChainedProxyTest.java
index bc192db8a..342af1a6f 100644
--- a/src/test/java/org/littleshoot/proxy/MitmWithBadServerAuthenticationTCPChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/MitmWithBadServerAuthenticationTCPChainedProxyTest.java
@@ -48,6 +48,11 @@ public boolean requiresEncryption() {
public SSLEngine newSslEngine() {
return clientSslEngineSource.newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return clientSslEngineSource.newSslEngine(peerHost, peerPort);
+ }
};
}
}
diff --git a/src/test/java/org/littleshoot/proxy/MitmWithChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/MitmWithChainedProxyTest.java
index a29873eb6..07025de2d 100644
--- a/src/test/java/org/littleshoot/proxy/MitmWithChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/MitmWithChainedProxyTest.java
@@ -8,13 +8,12 @@
import java.util.HashSet;
import java.util.Set;
-import org.littleshoot.proxy.extras.SelfSignedMitmManager;
-
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
+import org.littleshoot.proxy.extras.SelfSignedMitmManagerFactory;
/**
* Tests a proxy that runs as a MITM and which is chained with
@@ -40,7 +39,7 @@ protected void setUp() {
.withPort(0)
.withChainProxyManager(chainedProxyManager())
.plusActivityTracker(DOWNSTREAM_TRACKER)
- .withManInTheMiddle(new SelfSignedMitmManager())
+ .withManInTheMiddle(new SelfSignedMitmManagerFactory())
.withFiltersSource(new HttpFiltersSourceAdapter() {
@Override
public HttpFilters filterRequest(HttpRequest originalRequest) {
diff --git a/src/test/java/org/littleshoot/proxy/MitmWithClientAuthenticationNotRequiredTCPChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/MitmWithClientAuthenticationNotRequiredTCPChainedProxyTest.java
index f748dddb7..3a17b25fa 100644
--- a/src/test/java/org/littleshoot/proxy/MitmWithClientAuthenticationNotRequiredTCPChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/MitmWithClientAuthenticationNotRequiredTCPChainedProxyTest.java
@@ -43,6 +43,11 @@ public boolean requiresEncryption() {
public SSLEngine newSslEngine() {
return clientSslEngineSource.newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return clientSslEngineSource.newSslEngine(peerHost, peerPort);
+ }
};
}
}
diff --git a/src/test/java/org/littleshoot/proxy/MitmWithEncryptedTCPChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/MitmWithEncryptedTCPChainedProxyTest.java
index 418e7e8d6..0625e9af6 100644
--- a/src/test/java/org/littleshoot/proxy/MitmWithEncryptedTCPChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/MitmWithEncryptedTCPChainedProxyTest.java
@@ -34,6 +34,11 @@ public boolean requiresEncryption() {
public SSLEngine newSslEngine() {
return sslEngineSource.newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return sslEngineSource.newSslEngine(peerHost, peerPort);
+ }
};
}
}
diff --git a/src/test/java/org/littleshoot/proxy/MitmWithEncryptedUDTChainedProxyTest.java b/src/test/java/org/littleshoot/proxy/MitmWithEncryptedUDTChainedProxyTest.java
index 0630149d6..4955a784e 100644
--- a/src/test/java/org/littleshoot/proxy/MitmWithEncryptedUDTChainedProxyTest.java
+++ b/src/test/java/org/littleshoot/proxy/MitmWithEncryptedUDTChainedProxyTest.java
@@ -34,6 +34,11 @@ public boolean requiresEncryption() {
public SSLEngine newSslEngine() {
return sslEngineSource.newSslEngine();
}
+
+ @Override
+ public SSLEngine newSslEngine(String peerHost, int peerPort) {
+ return sslEngineSource.newSslEngine(peerHost, peerPort);
+ }
};
}
}
diff --git a/src/test/java/org/littleshoot/proxy/impl/DefaultHttpProxyServerTest.java b/src/test/java/org/littleshoot/proxy/impl/DefaultHttpProxyServerTest.java
new file mode 100644
index 000000000..ba3148d26
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/impl/DefaultHttpProxyServerTest.java
@@ -0,0 +1,38 @@
+package org.littleshoot.proxy.impl;
+
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpRequest;
+import org.junit.Test;
+import org.littleshoot.proxy.DefaultFailureHttpResponseComposer;
+import org.littleshoot.proxy.FailureHttpResponseComposer;
+
+import static org.junit.Assert.assertTrue;
+
+public class DefaultHttpProxyServerTest {
+
+ @Test
+ public void testDefaultUnrecoverableFailureHttpResponseComposer() {
+ DefaultHttpProxyServer httpProxyServer = (DefaultHttpProxyServer) DefaultHttpProxyServer.bootstrap().start();
+ assertTrue(httpProxyServer.getUnrecoverableFailureHttpResponseComposer() instanceof DefaultFailureHttpResponseComposer);
+ httpProxyServer.stop();
+ }
+
+ @Test
+ public void testCustomUnrecoverableFailureHttpResponseComposer() {
+
+ class CustomUnrecoverableFailureHttpResponseComposer implements FailureHttpResponseComposer {
+ @Override
+ public FullHttpResponse compose(HttpRequest httpRequest, Throwable cause) {
+ return null;
+ }
+ }
+
+ DefaultHttpProxyServer httpProxyServer = (DefaultHttpProxyServer) DefaultHttpProxyServer
+ .bootstrap()
+ .withUnrecoverableFailureHttpResponseComposer(new CustomUnrecoverableFailureHttpResponseComposer())
+ .start();
+ assertTrue(httpProxyServer.getUnrecoverableFailureHttpResponseComposer() instanceof CustomUnrecoverableFailureHttpResponseComposer);
+ httpProxyServer.stop();
+ }
+
+}
\ No newline at end of file