diff --git a/pom.xml b/pom.xml index 09c407e29..52691c1c9 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.littleshoot littleproxy jar - 1.1.3-VGS-SNAPSHOT + 1.1.3.1-VGS-SNAPSHOT LittleProxy 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