LittleProxy is a high performance HTTP proxy written in Java and using the Netty networking framework.
diff --git a/src/main/java/org/littleshoot/proxy/BadGatewayFailureHttpResponseComposer.java b/src/main/java/org/littleshoot/proxy/BadGatewayFailureHttpResponseComposer.java
new file mode 100644
index 000000000..f115332cb
--- /dev/null
+++ b/src/main/java/org/littleshoot/proxy/BadGatewayFailureHttpResponseComposer.java
@@ -0,0 +1,42 @@
+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 BadGatewayFailureHttpResponseComposer 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);
+
+ 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 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();
+ }
+}
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/HttpProxyServerBootstrap.java b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
index 367dc8dd1..3993d420d 100644
--- a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
+++ b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
@@ -197,6 +197,22 @@ HttpProxyServerBootstrap withManInTheMiddle(
HttpProxyServerBootstrap withFiltersSource(
HttpFiltersSource filtersSource);
+ /**
+ *
+ * Specify a {@link FailureHttpResponseComposer} to use for composing
+ * custom response message on unrecoverable failure
+ *
+ *
+ *
+ * Default = {@link BadGatewayFailureHttpResponseComposer}
+ *
+ *
+ * @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/impl/ClientToProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
index 32d8d01ec..8bf49b107 100644
--- a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
+++ b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
@@ -25,6 +25,8 @@
import io.netty.util.concurrent.GenericFutureListener;
import org.apache.commons.lang3.StringUtils;
import org.littleshoot.proxy.ActivityTracker;
+import org.littleshoot.proxy.BadGatewayFailureHttpResponseComposer;
+import org.littleshoot.proxy.FailureHttpResponseComposer;
import org.littleshoot.proxy.FlowContext;
import org.littleshoot.proxy.FullFlowContext;
import org.littleshoot.proxy.HttpFilters;
@@ -616,22 +618,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 {
@@ -1203,15 +1208,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 BadGatewayFailureHttpResponseComposer().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..72d3c4a26 100644
--- a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java
+++ b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java
@@ -17,6 +17,7 @@
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.littleshoot.proxy.ActivityTracker;
+import org.littleshoot.proxy.BadGatewayFailureHttpResponseComposer;
import org.littleshoot.proxy.ChainedProxyManager;
import org.littleshoot.proxy.DefaultHostResolver;
import org.littleshoot.proxy.DnsSecServerResolver;
@@ -26,6 +27,7 @@
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.ProxyAuthenticator;
import org.littleshoot.proxy.SslEngineSource;
@@ -108,6 +110,7 @@ public class DefaultHttpProxyServer implements HttpProxyServer {
private final ChainedProxyManager chainProxyManager;
private final MitmManager mitmManager;
private final HttpFiltersSource filtersSource;
+ private final FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer;
private final boolean transparent;
private volatile int connectTimeout;
private volatile int idleConnectionTimeout;
@@ -240,6 +243,7 @@ private DefaultHttpProxyServer(ServerGroup serverGroup,
ChainedProxyManager chainProxyManager,
MitmManager mitmManager,
HttpFiltersSource filtersSource,
+ FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer,
boolean transparent,
int idleConnectionTimeout,
Collection activityTrackers,
@@ -262,6 +266,7 @@ private DefaultHttpProxyServer(ServerGroup serverGroup,
this.chainProxyManager = chainProxyManager;
this.mitmManager = mitmManager;
this.filtersSource = filtersSource;
+ this.unrecoverableFailureHttpResponseComposer = unrecoverableFailureHttpResponseComposer;
this.transparent = transparent;
this.idleConnectionTimeout = idleConnectionTimeout;
if (activityTrackers != null) {
@@ -396,6 +401,7 @@ public HttpProxyServerBootstrap clone() {
chainProxyManager,
mitmManager,
filtersSource,
+ unrecoverableFailureHttpResponseComposer,
transparent,
idleConnectionTimeout,
activityTrackers,
@@ -581,6 +587,10 @@ public HttpFiltersSource getFiltersSource() {
return filtersSource;
}
+ public FailureHttpResponseComposer getUnrecoverableFailureHttpResponseComposer() {
+ return unrecoverableFailureHttpResponseComposer;
+ }
+
protected Collection getActivityTrackers() {
return activityTrackers;
}
@@ -608,6 +618,7 @@ private static class DefaultHttpProxyServerBootstrap implements HttpProxyServerB
private ChainedProxyManager chainProxyManager = null;
private MitmManager mitmManager = null;
private HttpFiltersSource filtersSource = new HttpFiltersSourceAdapter();
+ private FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer = new BadGatewayFailureHttpResponseComposer();
private boolean transparent = false;
private int idleConnectionTimeout = 70;
private Collection activityTrackers = new ConcurrentLinkedQueue();
@@ -638,6 +649,7 @@ private DefaultHttpProxyServerBootstrap(
ChainedProxyManager chainProxyManager,
MitmManager mitmManager,
HttpFiltersSource filtersSource,
+ FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer,
boolean transparent, int idleConnectionTimeout,
Collection activityTrackers,
int connectTimeout, HostResolver serverResolver,
@@ -659,6 +671,7 @@ private DefaultHttpProxyServerBootstrap(
this.chainProxyManager = chainProxyManager;
this.mitmManager = mitmManager;
this.filtersSource = filtersSource;
+ this.unrecoverableFailureHttpResponseComposer = unrecoverableFailureHttpResponseComposer;
this.transparent = transparent;
this.idleConnectionTimeout = idleConnectionTimeout;
if (activityTrackers != null) {
@@ -797,6 +810,12 @@ public HttpProxyServerBootstrap withFiltersSource(
return this;
}
+ public HttpProxyServerBootstrap withUnrecoverableFailureHttpResponseComposer(
+ FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer) {
+ this.unrecoverableFailureHttpResponseComposer = unrecoverableFailureHttpResponseComposer;
+ return this;
+ }
+
@Override
public HttpProxyServerBootstrap withUseDnsSec(boolean useDnsSec) {
if (useDnsSec) {
@@ -900,7 +919,7 @@ private DefaultHttpProxyServer build() {
transportProtocol, determineListenAddress(),
sslEngineSource, authenticateSslClients,
proxyAuthenticator, chainProxyManager, mitmManager,
- filtersSource, transparent,
+ filtersSource, unrecoverableFailureHttpResponseComposer, transparent,
idleConnectionTimeout, activityTrackers, connectTimeout,
serverResolver, readThrottleBytesPerSecond, writeThrottleBytesPerSecond,
localAddress, proxyAlias, maxInitialLineLength, maxHeaderSize, maxChunkSize,
diff --git a/src/test/java/org/littleshoot/proxy/BadGatewayFailureHttpResponseComposerTest.java b/src/test/java/org/littleshoot/proxy/BadGatewayFailureHttpResponseComposerTest.java
new file mode 100644
index 000000000..8a7333e0c
--- /dev/null
+++ b/src/test/java/org/littleshoot/proxy/BadGatewayFailureHttpResponseComposerTest.java
@@ -0,0 +1,74 @@
+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 org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+public class BadGatewayFailureHttpResponseComposerTest {
+
+ private static final String REQUEST_URI = "https://localhost/hi";
+
+ @Test
+ public void testDefault() throws IOException {
+ FailureHttpResponseComposer badGatewayResponseComposer = new BadGatewayFailureHttpResponseComposer();
+
+ 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 testCustomMessage() throws IOException {
+ FailureHttpResponseComposer badGatewayResponseComposer = new BadGatewayFailureHttpResponseComposer() {
+ @Override
+ protected String provideCustomMessage(HttpRequest httpRequest, Throwable cause) {
+ return "Invalid certificate: " + httpRequest.getUri();
+ }
+ };
+
+ 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("Invalid certificate: " + REQUEST_URI, new String(response.content().array()));
+ }
+
+ @Test
+ public void testClearedContent() throws IOException {
+ FailureHttpResponseComposer badGatewayResponseComposer = new BadGatewayFailureHttpResponseComposer();
+
+ 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/impl/DefaultHttpProxyServerTest.java b/src/test/java/org/littleshoot/proxy/impl/DefaultHttpProxyServerTest.java
new file mode 100644
index 000000000..eb370fc51
--- /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.BadGatewayFailureHttpResponseComposer;
+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 BadGatewayFailureHttpResponseComposer);
+ 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