Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java
Original file line number Diff line number Diff line change
Expand Up @@ -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. <b>Note:</b> 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.
Expand Down
47 changes: 28 additions & 19 deletions src/main/java/org/littleshoot/proxy/impl/ConnectionFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void> 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();
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -291,6 +293,7 @@ private DefaultHttpProxyServer(ServerGroup serverGroup,
this.maxHeaderSize = maxHeaderSize;
this.maxChunkSize = maxChunkSize;
this.allowRequestsToOriginServer = allowRequestsToOriginServer;
this.mimicServerHandshake = mimicServerHandshake;
}

/**
Expand Down Expand Up @@ -384,6 +387,10 @@ public boolean isAllowRequestsToOriginServer() {
return allowRequestsToOriginServer;
}

public boolean isMimicServerHandshake(){
return mimicServerHandshake;
}

@Override
public HttpProxyServerBootstrap clone() {
return new DefaultHttpProxyServerBootstrap(serverGroup,
Expand All @@ -408,7 +415,8 @@ public HttpProxyServerBootstrap clone() {
maxInitialLineLength,
maxHeaderSize,
maxChunkSize,
allowRequestsToOriginServer);
allowRequestsToOriginServer,
mimicServerHandshake);
}

@Override
Expand Down Expand Up @@ -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() {
}
Expand All @@ -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;
Expand All @@ -674,6 +684,7 @@ private DefaultHttpProxyServerBootstrap(
this.maxHeaderSize = maxHeaderSize;
this.maxChunkSize = maxChunkSize;
this.allowRequestToOriginServer = allowRequestToOriginServer;
this.mimicServerHandshake = mimicServerHandshake;
}

private DefaultHttpProxyServerBootstrap(Properties props) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -904,7 +921,7 @@ transportProtocol, determineListenAddress(),
idleConnectionTimeout, activityTrackers, connectTimeout,
serverResolver, readThrottleBytesPerSecond, writeThrottleBytesPerSecond,
localAddress, proxyAlias, maxInitialLineLength, maxHeaderSize, maxChunkSize,
allowRequestToOriginServer);
allowRequestToOriginServer, mimicServerHandshake);
}

private InetSocketAddress determineListenAddress() {
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/org/littleshoot/proxy/impl/ProxyConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -549,15 +549,17 @@ public SSLEngine getSslEngine() {
*/
protected void stopReading() {
LOG.debug("Stopped reading");
this.channel.config().setAutoRead(false);
if(this.channel != null)
this.channel.config().setAutoRead(false);
}

/**
* Call this to resume reading.
*/
protected void resumeReading() {
LOG.debug("Resumed reading");
this.channel.config().setAutoRead(true);
if(this.channel != null)
this.channel.config().setAutoRead(true);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down Expand Up @@ -736,6 +745,47 @@ public void operationComplete(
}
};

/**
* <p>
* Encrypts the client channel based on MITM SSL Engine {@link SSLSession}.
* </p>
*
* <p>
* This does not wait for the handshake to finish so that we can go on and
* respond to the CONNECT request.
* </p>
*/
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<Future<? super Channel>>() {
@Override
public void operationComplete(
Future<? super Channel> 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.
Expand Down