diff --git a/pom.xml b/pom.xml index e895f4e6c..69eb55325 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.littleshoot littleproxy jar - 1.1.3.2-VGS-SNAPSHOT + 1.1.3.3-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/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/HttpProxyServerBootstrap.java b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java index 118205a5e..7eb838673 100644 --- a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java +++ b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java @@ -181,6 +181,36 @@ HttpProxyServerBootstrap withChainProxyManager( HttpProxyServerBootstrap withManInTheMiddle( 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 HttpFiltersSource} to use for filtering requests and/or diff --git a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java index f5e6a908b..afef50a11 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java @@ -26,6 +26,7 @@ import org.apache.commons.lang3.StringUtils; import org.littleshoot.proxy.ActivityTracker; import org.littleshoot.proxy.BadGatewayFailureHttpResponseComposer; +import org.littleshoot.proxy.ExceptionHandler; import org.littleshoot.proxy.FailureHttpResponseComposer; import org.littleshoot.proxy.FlowContext; import org.littleshoot.proxy.FullFlowContext; @@ -755,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 diff --git a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java index 64bb78cbe..91a2b710e 100644 --- a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java +++ b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java @@ -31,6 +31,7 @@ import org.littleshoot.proxy.MitmManager; import org.littleshoot.proxy.MitmManagerFactory; import org.littleshoot.proxy.ProxyAuthenticator; +import org.littleshoot.proxy.ExceptionHandler; import org.littleshoot.proxy.SslEngineSource; import org.littleshoot.proxy.TransportProtocol; import org.littleshoot.proxy.UnknownTransportProtocolException; @@ -110,6 +111,8 @@ public class DefaultHttpProxyServer implements HttpProxyServer { private final ProxyAuthenticator proxyAuthenticator; private final ChainedProxyManager chainProxyManager; private final MitmManagerFactory mitmManagerFactory; + private final ExceptionHandler clientToProxyExHandler; + private final ExceptionHandler proxyToServerExHandler; private final HttpFiltersSource filtersSource; private final FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer; private final boolean transparent; @@ -243,6 +246,8 @@ private DefaultHttpProxyServer(ServerGroup serverGroup, ProxyAuthenticator proxyAuthenticator, ChainedProxyManager chainProxyManager, MitmManagerFactory mitmManagerFactory, + ExceptionHandler clientToProxyExHandler, + ExceptionHandler proxyToServerExHandler, HttpFiltersSource filtersSource, FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer, boolean transparent, @@ -266,6 +271,8 @@ private DefaultHttpProxyServer(ServerGroup serverGroup, this.proxyAuthenticator = proxyAuthenticator; this.chainProxyManager = chainProxyManager; this.mitmManagerFactory = mitmManagerFactory; + this.clientToProxyExHandler = clientToProxyExHandler; + this.proxyToServerExHandler = proxyToServerExHandler; this.filtersSource = filtersSource; this.unrecoverableFailureHttpResponseComposer = unrecoverableFailureHttpResponseComposer; this.transparent = transparent; @@ -401,6 +408,8 @@ public HttpProxyServerBootstrap clone() { proxyAuthenticator, chainProxyManager, mitmManagerFactory, + clientToProxyExHandler, + proxyToServerExHandler, filtersSource, unrecoverableFailureHttpResponseComposer, transparent, @@ -579,6 +588,14 @@ protected MitmManager getMitmManager(Channel channel) { return null; } + protected ExceptionHandler getClientToProxyExHandler() { + return clientToProxyExHandler; + } + + protected ExceptionHandler getProxyToServerExHandler() { + return proxyToServerExHandler; + } + protected SslEngineSource getSslEngineSource() { return sslEngineSource; } @@ -621,6 +638,8 @@ private static class DefaultHttpProxyServerBootstrap implements HttpProxyServerB private ProxyAuthenticator proxyAuthenticator = null; private ChainedProxyManager chainProxyManager = null; private MitmManagerFactory mitmManagerFactory = null; + private ExceptionHandler clientToProxyExHandler = null; + private ExceptionHandler proxyToServerExHandler = null; private HttpFiltersSource filtersSource = new HttpFiltersSourceAdapter(); private FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer = new BadGatewayFailureHttpResponseComposer(); private boolean transparent = false; @@ -652,6 +671,8 @@ private DefaultHttpProxyServerBootstrap( ProxyAuthenticator proxyAuthenticator, ChainedProxyManager chainProxyManager, MitmManagerFactory mitmManagerFactory, + ExceptionHandler clientToProxyExHandler, + ExceptionHandler proxyToServerExHandler, HttpFiltersSource filtersSource, FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer, boolean transparent, int idleConnectionTimeout, @@ -674,6 +695,8 @@ private DefaultHttpProxyServerBootstrap( this.proxyAuthenticator = proxyAuthenticator; this.chainProxyManager = chainProxyManager; this.mitmManagerFactory = mitmManagerFactory; + this.clientToProxyExHandler = clientToProxyExHandler; + this.proxyToServerExHandler = proxyToServerExHandler; this.filtersSource = filtersSource; this.unrecoverableFailureHttpResponseComposer = unrecoverableFailureHttpResponseComposer; this.transparent = transparent; @@ -807,6 +830,20 @@ 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 withFiltersSource( HttpFiltersSource filtersSource) { @@ -923,6 +960,7 @@ private DefaultHttpProxyServer build() { transportProtocol, determineListenAddress(), sslEngineSource, authenticateSslClients, proxyAuthenticator, chainProxyManager, mitmManagerFactory, + clientToProxyExHandler, proxyToServerExHandler, filtersSource, unrecoverableFailureHttpResponseComposer, transparent, idleConnectionTimeout, activityTrackers, connectTimeout, serverResolver, readThrottleBytesPerSecond, writeThrottleBytesPerSecond, diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java index bb5e09d5e..3bd22224a 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java @@ -39,6 +39,7 @@ 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; @@ -439,7 +440,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)) { 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"); + } +}