diff --git a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java index 367dc8dd1..68dc5231b 100644 --- a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java +++ b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java @@ -311,6 +311,16 @@ HttpProxyServerBootstrap withConnectTimeout( */ HttpProxyServerBootstrap withAllowRequestToOriginServer(boolean allowRequestToOriginServer); + /** + * When true, the proxy mimics the server handshake during the CONNECT request using the MITM SSLSession. + * This is useful when the proxy should not make connection with the remote server during CONNECT and the following + * GET or POST request is intercepted and handled accordingly. Note: Use this only when proxy's GET or POST request + * is intercepted using HttpFiltersAdapter and MITM is enabled, as it doesn't make sense in other cases + * + * @param mimicServerHandshake + */ + HttpProxyServerBootstrap withMimicServerHandshake(boolean mimicServerHandshake); + /** * Sets the alias to use when adding Via headers to incoming and outgoing HTTP messages. The alias may be any * pseudonym, or if not specified, defaults to the hostname of the local machine. See RFC 7230, section 5.7.1. diff --git a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java index 23a65f08e..35018eea8 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java +++ b/src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java @@ -179,26 +179,35 @@ void succeed() { void fail(final Throwable cause) { final ConnectionState lastStateBeforeFailure = serverConnection .getCurrentState(); - serverConnection.disconnect().addListener( - new GenericFutureListener() { - @Override - public void operationComplete(Future future) - throws Exception { - synchronized (connectLock) { - if (!clientConnection.serverConnectionFailed( - serverConnection, - lastStateBeforeFailure, - cause)) { - // the connection to the server failed and we are not retrying, so transition to the - // DISCONNECTED state - serverConnection.become(ConnectionState.DISCONNECTED); - - // We are not retrying our connection, let anyone waiting for a connection know that we're done - notifyThreadsWaitingForConnection(); - } + Future disconnect = serverConnection.disconnect(); + if(disconnect != null) { + disconnect.addListener( + new GenericFutureListener() { + @Override + public void operationComplete(Future future) + throws Exception { + checkForRetry(lastStateBeforeFailure, cause); } - } - }); + }); + } else { + checkForRetry(lastStateBeforeFailure, cause); + } + } + + private void checkForRetry(ConnectionState lastStateBeforeFailure, Throwable cause) { + synchronized (connectLock) { + if (!clientConnection.serverConnectionFailed( + serverConnection, + lastStateBeforeFailure, + cause)) { + // the connection to the server failed and we are not retrying, so transition to the + // DISCONNECTED state + serverConnection.become(ConnectionState.DISCONNECTED); + + // We are not retrying our connection, let anyone waiting for a connection know that we're done + notifyThreadsWaitingForConnection(); + } + } } /** diff --git a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java index 1891532e4..3367ecbee 100644 --- a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java +++ b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java @@ -117,6 +117,7 @@ public class DefaultHttpProxyServer implements HttpProxyServer { private final int maxHeaderSize; private final int maxChunkSize; private final boolean allowRequestsToOriginServer; + private final boolean mimicServerHandshake; /** * The alias or pseudonym for this proxy, used when adding the Via header. @@ -252,7 +253,8 @@ private DefaultHttpProxyServer(ServerGroup serverGroup, int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, - boolean allowRequestsToOriginServer) { + boolean allowRequestsToOriginServer, + boolean mimicServerHandshake) { this.serverGroup = serverGroup; this.transportProtocol = transportProtocol; this.requestedAddress = requestedAddress; @@ -291,6 +293,7 @@ private DefaultHttpProxyServer(ServerGroup serverGroup, this.maxHeaderSize = maxHeaderSize; this.maxChunkSize = maxChunkSize; this.allowRequestsToOriginServer = allowRequestsToOriginServer; + this.mimicServerHandshake = mimicServerHandshake; } /** @@ -384,6 +387,10 @@ public boolean isAllowRequestsToOriginServer() { return allowRequestsToOriginServer; } + public boolean isMimicServerHandshake(){ + return mimicServerHandshake; + } + @Override public HttpProxyServerBootstrap clone() { return new DefaultHttpProxyServerBootstrap(serverGroup, @@ -408,7 +415,8 @@ public HttpProxyServerBootstrap clone() { maxInitialLineLength, maxHeaderSize, maxChunkSize, - allowRequestsToOriginServer); + allowRequestsToOriginServer, + mimicServerHandshake); } @Override @@ -624,6 +632,7 @@ private static class DefaultHttpProxyServerBootstrap implements HttpProxyServerB private int maxHeaderSize = MAX_HEADER_SIZE_DEFAULT; private int maxChunkSize = MAX_CHUNK_SIZE_DEFAULT; private boolean allowRequestToOriginServer = false; + private boolean mimicServerHandshake = false; private DefaultHttpProxyServerBootstrap() { } @@ -648,7 +657,8 @@ private DefaultHttpProxyServerBootstrap( int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, - boolean allowRequestToOriginServer) { + boolean allowRequestToOriginServer, + boolean mimicServerHandshake) { this.serverGroup = serverGroup; this.transportProtocol = transportProtocol; this.requestedAddress = requestedAddress; @@ -674,6 +684,7 @@ private DefaultHttpProxyServerBootstrap( this.maxHeaderSize = maxHeaderSize; this.maxChunkSize = maxChunkSize; this.allowRequestToOriginServer = allowRequestToOriginServer; + this.mimicServerHandshake = mimicServerHandshake; } private DefaultHttpProxyServerBootstrap(Properties props) { @@ -873,6 +884,12 @@ public HttpProxyServerBootstrap withAllowRequestToOriginServer(boolean allowRequ return this; } + @Override + public HttpProxyServerBootstrap withMimicServerHandshake(boolean mimicServerHandshake) { + this.mimicServerHandshake = mimicServerHandshake; + return this; + } + @Override public HttpProxyServer start() { return build().start(); @@ -904,7 +921,7 @@ transportProtocol, determineListenAddress(), idleConnectionTimeout, activityTrackers, connectTimeout, serverResolver, readThrottleBytesPerSecond, writeThrottleBytesPerSecond, localAddress, proxyAlias, maxInitialLineLength, maxHeaderSize, maxChunkSize, - allowRequestToOriginServer); + allowRequestToOriginServer, mimicServerHandshake); } private InetSocketAddress determineListenAddress() { diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java index 58c3eb240..d7c208eef 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java @@ -549,7 +549,8 @@ public SSLEngine getSslEngine() { */ protected void stopReading() { LOG.debug("Stopped reading"); - this.channel.config().setAutoRead(false); + if(this.channel != null) + this.channel.config().setAutoRead(false); } /** @@ -557,7 +558,8 @@ protected void stopReading() { */ protected void resumeReading() { LOG.debug("Resumed reading"); - this.channel.config().setAutoRead(true); + if(this.channel != null) + this.channel.config().setAutoRead(true); } /** diff --git a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java index 2c9cece42..fe2cf9741 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ProxyToServerConnection.java @@ -541,9 +541,18 @@ private void connectAndWrite(HttpRequest initialRequest) { */ private void initializeConnectionFlow() { this.connectionFlow = new ConnectionFlow(clientConnection, this, - connectLock) - .then(ConnectChannel); + connectLock); + + //If it's a CONNECT request and mimic server handshake is enabled, don't make remote + //server connection and use MITM server ssl engine to do the handshake + if(ProxyUtils.isCONNECT(initialRequest) && proxyServer.isMimicServerHandshake()){ + connectionFlow + .then(clientConnection.RespondCONNECTSuccessful) + .then(serverConnection.MitmMimicHSEncryptClientChannel); + return; + } + connectionFlow.then(ConnectChannel); if (chainedProxy != null && chainedProxy.requiresEncryption()) { connectionFlow.then(serverConnection.EncryptChannel(chainedProxy .newSslEngine())); @@ -736,6 +745,47 @@ public void operationComplete( } }; + /** + *

+ * Encrypts the client channel based on MITM SSL Engine {@link SSLSession}. + *

+ * + *

+ * This does not wait for the handshake to finish so that we can go on and + * respond to the CONNECT request. + *

+ */ + private ConnectionFlowStep MitmMimicHSEncryptClientChannel = new ConnectionFlowStep( + this, HANDSHAKING) { + @Override + boolean shouldExecuteOnEventLoop() { + return false; + } + + @Override + boolean shouldSuppressInitialRequest() { + return true; + } + + @Override + protected Future execute() { + return clientConnection + .encrypt(proxyServer.getMitmManager() + .clientSslEngineFor(initialRequest, proxyServer.getMitmManager().serverSslEngine().getSession()), false) + .addListener( + new GenericFutureListener>() { + @Override + public void operationComplete( + Future future) + throws Exception { + if (future.isSuccess()) { + clientConnection.setMitming(true); + } + } + }); + } + }; + /** * Called when the connection to the server or upstream chained proxy fails. This method may return true to indicate * that the connection should be retried. If returning true, this method must set up the connection itself.