diff --git a/README.md b/README.md index d512855e6..ded0c47ab 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build Status](https://travis-ci.org/adamfisk/LittleProxy.png?branch=master)](https://travis-ci.org/adamfisk/LittleProxy) +[![CircleCI](https://circleci.com/gh/verygoodsecurity/LittleProxy.svg?style=svg)](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