Skip to content
This repository was archived by the owner on Feb 24, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a83d24b
Initial commit
viacheslav-fomin-main Jan 3, 2019
8155561
Adds cached executor
viacheslav-fomin-main Jan 3, 2019
02fc440
try to fix build
viacheslav-fomin-main Jan 3, 2019
ad56e61
Remove executor for now
viacheslav-fomin-main Jan 3, 2019
58fc61d
attempt to fix the build
viacheslav-fomin-main Jan 3, 2019
dab7744
attempt 2 to fix the build
viacheslav-fomin-main Jan 3, 2019
e291eae
Try with executor
viacheslav-fomin-main Jan 4, 2019
de5ac21
Try proxy to client response in the processor
viacheslav-fomin-main Jan 4, 2019
ac1ea26
Revert proxy to client response in the processor
viacheslav-fomin-main Jan 4, 2019
f38c65b
Wrap processor in global state
viacheslav-fomin-main Jan 4, 2019
f2c7ef2
Global state wrapper
viacheslav-fomin-main Jan 4, 2019
6d58bc3
Try to use async executor for response
viacheslav-fomin-main Jan 4, 2019
c9e9958
Retain before async execution
viacheslav-fomin-main Jan 4, 2019
81efa84
Global state handler wrapper for proxy to server connection
viacheslav-fomin-main Jan 4, 2019
bb1e3e9
Fix build
viacheslav-fomin-main Jan 4, 2019
e9cadd7
build fix
viacheslav-fomin-main Jan 4, 2019
083a300
remove hardcoded processing executor
viacheslav-fomin-main Jan 4, 2019
087d4ae
build fix
viacheslav-fomin-main Jan 4, 2019
cddbc1e
Cached default executor, correct statee
viacheslav-fomin-main Jan 6, 2019
35346f9
Test build 1
viacheslav-fomin-main Jan 6, 2019
beee58a
Try fixing build
viacheslav-fomin-main Jan 7, 2019
44a3278
Try fixing build 3
viacheslav-fomin-main Jan 7, 2019
006935e
Uncomment async response
viacheslav-fomin-main Jan 7, 2019
bfe4ee1
Try adding global state wrapper
viacheslav-fomin-main Jan 7, 2019
7cf4313
Improve global state wrapper
viacheslav-fomin-main Jan 7, 2019
c99ca94
Remove commented code
viacheslav-fomin-main Jan 7, 2019
ca235d5
Returns InboundGlobalStateHandler back
viacheslav-fomin-main Jan 7, 2019
a73d0bf
Code refactoring
viacheslav-fomin-main Jan 10, 2019
5066c8e
Code refactoring 2
viacheslav-fomin-main Jan 10, 2019
4fd951c
Fix ref counting
viacheslav-fomin-main Jan 10, 2019
aed95cf
Fix the build
viacheslav-fomin-main Jan 11, 2019
a0636c6
inline handler
viacheslav-fomin-main Jan 11, 2019
62156df
Fix build 2
viacheslav-fomin-main Jan 11, 2019
0c9757a
Cleanup code
viacheslav-fomin-main Jan 11, 2019
c841821
Return after exception caught
viacheslav-fomin-main Jan 11, 2019
cb07871
First unit test
viacheslav-fomin-main Jan 15, 2019
7f33692
Foundation for tests
viacheslav-fomin-main Jan 15, 2019
33ec33e
More tests
viacheslav-fomin-main Jan 15, 2019
adea07c
Renames handler
viacheslav-fomin-main Jan 15, 2019
32b7572
Handle proxy to client exception
viacheslav-fomin-main Jan 16, 2019
9d700f4
Fix reference counting
viacheslav-fomin-main Jan 18, 2019
b28a17e
Code cleanup
viacheslav-fomin-main Feb 4, 2019
ed3d90a
Fix executor service shutdown
viacheslav-fomin-main Feb 4, 2019
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.littleshoot.proxy.ratelimit.RateLimiter;

import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;

/**
* Configures and starts an {@link HttpProxyServer}. The HttpProxyServer is
Expand Down Expand Up @@ -242,6 +243,14 @@ HttpProxyServerBootstrap withProxyToServerExHandler(
HttpProxyServerBootstrap withCustomGlobalState(
GlobalStateHandler globalStateHandler);

/**
*
* @param messageProcessorExecutor
* @return
*/
HttpProxyServerBootstrap withMessageProcessingExecutor(
ExecutorService messageProcessorExecutor);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will be provided from outside, possibly in a wrapper with added metrics

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like the idea of a specialized wrapper.


/**
* <p>
* Specify a {@link HttpFiltersSource} to use for filtering requests and/or
Expand Down
164 changes: 120 additions & 44 deletions src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
Expand All @@ -21,13 +23,16 @@
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
import io.netty.util.ReferenceCounted;
import io.netty.util.concurrent.EventExecutorGroup;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;

import org.apache.commons.lang3.StringUtils;
import org.littleshoot.proxy.ActivityTracker;
import org.littleshoot.proxy.DefaultFailureHttpResponseComposer;
import org.littleshoot.proxy.ExceptionHandler;
import org.littleshoot.proxy.FailureHttpResponseComposer;
import org.littleshoot.proxy.GlobalStateHandler;
import org.littleshoot.proxy.ratelimit.RateLimiter;
import org.littleshoot.proxy.FlowContext;
import org.littleshoot.proxy.FullFlowContext;
Expand All @@ -44,6 +49,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RejectedExecutionException;
Expand Down Expand Up @@ -143,15 +149,17 @@ public class ClientToProxyConnection extends ProxyConnection<HttpRequest> {
final DefaultHttpProxyServer proxyServer,
SslEngineSource sslEngineSource,
boolean authenticateClients,
ChannelPipeline pipeline,
GlobalTrafficShapingHandler globalTrafficShapingHandler) {
GlobalTrafficShapingHandler globalTrafficShapingHandler,
Channel channel) {
Comment thread
osklyarenko marked this conversation as resolved.
super(AWAITING_INITIAL, proxyServer, false);

initChannelPipeline(pipeline);
this.channel = channel;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initial the channel is assigned on channelRegistered which happens a bit later but we need the channel right now to create global state wrapper event loop


initChannelPipeline(channel.pipeline());

if (sslEngineSource != null) {
LOG.debug("Enabling encryption of traffic from client to proxy");
encrypt(pipeline, sslEngineSource.newSslEngine(),
encrypt(channel.pipeline(), sslEngineSource.newSslEngine(),
authenticateClients)
.addListener(
new GenericFutureListener<Future<? super Channel>>() {
Expand All @@ -177,7 +185,8 @@ public void operationComplete(
**************************************************************************/

@Override
protected ConnectionState readHTTPInitial(HttpRequest httpRequest) {
protected ConnectionState readHTTPInitial(ChannelHandlerContext ctx, Object httpRequestObj) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did we change HttpRequest httpRequest to Object httpRequestObj ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I had some weird behavior with generic here..

HttpRequest httpRequest = (HttpRequest) httpRequestObj;
LOG.debug("Received raw request: {}", httpRequest);

// if we cannot parse the request, immediately return a 400 and close the connection, since we do not know what state
Expand All @@ -195,14 +204,9 @@ protected ConnectionState readHTTPInitial(HttpRequest httpRequest) {
return DISCONNECT_REQUESTED;
}

boolean authenticationRequired = authenticationRequired(httpRequest);
ctx.fireChannelRead(httpRequest);

if (authenticationRequired) {
LOG.debug("Not authenticated!!");
return AWAITING_PROXY_AUTHENTICATION;
} else {
return doReadHTTPInitial(httpRequest);
}
return getCurrentState();
}

/**
Expand All @@ -223,34 +227,11 @@ protected ConnectionState readHTTPInitial(HttpRequest httpRequest) {
* @param httpRequest
* @return
*/
private ConnectionState doReadHTTPInitial(HttpRequest httpRequest) {
// Make a copy of the original request
final HttpRequest currentRequest = copy(httpRequest);

// Set up our filters based on the original request. If the HttpFiltersSource returns null (meaning the request/response
// should not be filtered), fall back to the default no-op filter source.
HttpFilters filterInstance;
try {
filterInstance = proxyServer.getFiltersSource().filterRequest(currentRequest, ctx);
} finally {
// releasing a copied http request
if (currentRequest instanceof ReferenceCounted) {
((ReferenceCounted)currentRequest).release();
}
}
if (filterInstance != null) {
currentFilters = filterInstance;
} else {
currentFilters = HttpFiltersAdapter.NOOP_FILTER;
}

// Send the request through the clientToProxyRequest filter, and respond with the short-circuit response if required
HttpResponse clientToProxyFilterResponse = currentFilters.clientToProxyRequest(httpRequest);
public ConnectionState setupUpstreamConnection(HttpResponse shortCircuitResponse, HttpRequest httpRequest) {
if (shortCircuitResponse != null) {
LOG.debug("Responding to client with short-circuit response from filter: {}", shortCircuitResponse);

if (clientToProxyFilterResponse != null) {
LOG.debug("Responding to client with short-circuit response from filter: {}", clientToProxyFilterResponse);

boolean keepAlive = respondWithShortCircuitResponse(clientToProxyFilterResponse);
boolean keepAlive = respondWithShortCircuitResponse(shortCircuitResponse);
if (keepAlive) {
return AWAITING_INITIAL;
} else {
Expand Down Expand Up @@ -366,6 +347,96 @@ private ConnectionState doReadHTTPInitial(HttpRequest httpRequest) {
}
}

@Sharable
protected class ClientToProxyMessageProcessor extends ChannelInboundHandlerAdapter {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this handler contains authentication and payload transformation logic which is executed in a separate executor service

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we move it to a separate class / java file?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this class encapsulate some code of client to proxy connection.. it is easier to have it here..


@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final HttpRequest httpRequest = (HttpRequest) msg;

if (ProxyUtils.isChunked(httpRequest)) {
process(ctx, httpRequest);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} else {
if (httpRequest instanceof ReferenceCounted) {
LOG.debug("Retaining reference counted message");
((ReferenceCounted) msg).retain();
}

proxyServer.getMessageProcessingExecutor()
.execute(wrapTask(() -> {
try {
process(ctx, httpRequest);
} catch (Exception e) {
ctx.fireExceptionCaught(e);
} finally {
if (httpRequest instanceof ReferenceCounted) {
LOG.debug("Retaining reference counted message");
((ReferenceCounted) httpRequest).release();
}
}
}));
}
}

private void process(ChannelHandlerContext ctx, HttpRequest httpRequest) {

boolean authenticationRequired = false;
HttpResponse shortCircuitResponse = null;
authenticationRequired = authenticationRequired(httpRequest);

if (authenticationRequired) {
LOG.debug("Not authenticated!!");
become(AWAITING_PROXY_AUTHENTICATION);
} else {
// Make a copy of the original request
final HttpRequest currentRequest = copy(httpRequest);

// Set up our filters based on the original request. If the HttpFiltersSource returns null (meaning the request/response
// should not be filtered), fall back to the default no-op filter source.
HttpFilters filterInstance;
try {
filterInstance = proxyServer.getFiltersSource().filterRequest(currentRequest, ctx);
} finally {
// releasing a copied http request
if (currentRequest instanceof ReferenceCounted) {
((ReferenceCounted) currentRequest).release();
}
}
if (filterInstance != null) {
currentFilters = filterInstance;
} else {
currentFilters = HttpFiltersAdapter.NOOP_FILTER;
}

// Send the request through the clientToProxyRequest filter, and respond with the short-circuit response if required
shortCircuitResponse = currentFilters.clientToProxyRequest(httpRequest);

}

if (!authenticationRequired) {
if (httpRequest instanceof ReferenceCounted) {
LOG.debug("Retaining reference counted message");
((ReferenceCounted) httpRequest).retain();
}

ctx.fireChannelRead(new UpstreamConnectionHandler.Request(httpRequest, shortCircuitResponse));
}
}
}

Runnable wrapTask(Runnable task) {
return () -> {
final Optional<GlobalStateHandler> globalStateHandler =
Optional.ofNullable(proxyServer.getGlobalStateHandler());
try {
globalStateHandler.ifPresent(it -> it.restoreFromChannel(channel));
task.run();
Comment thread
osklyarenko marked this conversation as resolved.
} finally {
globalStateHandler.ifPresent(GlobalStateHandler::clear);
}
};
}

/**
* Returns true if the specified request is a request to an origin server, rather than to a proxy server. If this
* request is being MITM'd, this method always returns false. The format of requests to a proxy server are defined
Expand Down Expand Up @@ -800,8 +871,10 @@ private void initChannelPipeline(ChannelPipeline pipeline) {
pipeline.addLast("inboundGlobalStateHandler", new InboundGlobalStateHandler(this));
}

pipeline.addLast("bytesReadMonitor", bytesReadMonitor);
pipeline.addLast("bytesWrittenMonitor", bytesWrittenMonitor);
EventExecutorGroup globalStateWrapperEvenLoop = new GlobalStateWrapperEvenLoop(this);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this event loop will not always be used. Only when a call to fireChannel.. will be executed from another thread (when message processing thread was used). The main context is still handler by InboundGlobalStateHandler .

The reason is that in the same loop fireChannel.. calls are executed like recursion and this recursion starts before we have data in the context (from the request) so GlobalStateWrapperEvenLoop is not executed


pipeline.addLast(globalStateWrapperEvenLoop, "bytesReadMonitor", bytesReadMonitor);
pipeline.addLast(globalStateWrapperEvenLoop, "bytesWrittenMonitor", bytesWrittenMonitor);

pipeline.addLast("encoder", new HttpResponseEncoder());
// We want to allow longer request lines, headers, and chunks
Expand All @@ -818,8 +891,8 @@ private void initChannelPipeline(ChannelPipeline pipeline) {
aggregateContentForFiltering(pipeline, numberOfBytesToBuffer);
}

pipeline.addLast("requestReadMonitor", requestReadMonitor);
pipeline.addLast("responseWrittenMonitor", responseWrittenMonitor);
pipeline.addLast(globalStateWrapperEvenLoop, "requestReadMonitor", requestReadMonitor);
pipeline.addLast(globalStateWrapperEvenLoop, "responseWrittenMonitor", responseWrittenMonitor);

pipeline.addLast(
"idle",
Expand All @@ -830,7 +903,10 @@ private void initChannelPipeline(ChannelPipeline pipeline) {
pipeline.addLast("outboundGlobalStateHandler", new OutboundGlobalStateHandler(this));
}

pipeline.addLast("handler", this);
pipeline.addLast(globalStateWrapperEvenLoop, "router", this);
pipeline.addLast(globalStateWrapperEvenLoop, "httpInitialHandler", new HttpInitialHandler<>(this));
pipeline.addLast(globalStateWrapperEvenLoop, "clientToProxyMessageProcessor", new ClientToProxyMessageProcessor());
pipeline.addLast(globalStateWrapperEvenLoop, "upstreamConnectionHandler", new UpstreamConnectionHandler(this));

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import java.util.Collection;
import java.util.Properties;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

Expand Down Expand Up @@ -551,8 +552,8 @@ protected void initChannel(Channel ch) throws Exception {
DefaultHttpProxyServer.this,
sslEngineSource,
authenticateSslClients,
ch.pipeline(),
globalTrafficShapingHandler);
globalTrafficShapingHandler,
ch);
};
};
switch (transportProtocol) {
Expand Down Expand Up @@ -620,6 +621,10 @@ protected GlobalStateHandler getGlobalStateHandler() {
return globalStateHandler;
}

protected ExecutorService getMessageProcessingExecutor() {
return serverGroup.getMessageProcessingExecutor();
}

protected RequestTracer getRequestTracer() {
return requestTracer;
}
Expand Down Expand Up @@ -670,6 +675,7 @@ private static class DefaultHttpProxyServerBootstrap implements HttpProxyServerB
private ExceptionHandler proxyToServerExHandler = null;
private RequestTracer requestTracer = null;
private GlobalStateHandler globalStateHandler = null;
private ExecutorService messageProcessorExecutor = null;
private HttpFiltersSource filtersSource = new HttpFiltersSourceAdapter();
private FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer = new DefaultFailureHttpResponseComposer();
private boolean transparent = false;
Expand Down Expand Up @@ -895,6 +901,13 @@ public HttpProxyServerBootstrap withCustomGlobalState(
return this;
}

@Override
public HttpProxyServerBootstrap withMessageProcessingExecutor(
ExecutorService messageProcessorExecutor) {
this.messageProcessorExecutor = messageProcessorExecutor;
return this;
}

@Override
public HttpProxyServerBootstrap withFiltersSource(
HttpFiltersSource filtersSource) {
Expand Down Expand Up @@ -1010,7 +1023,8 @@ private DefaultHttpProxyServer build() {
serverGroup = this.serverGroup;
}
else {
serverGroup = new ServerGroup(name, clientToProxyAcceptorThreads, clientToProxyWorkerThreads, proxyToServerWorkerThreads);
serverGroup = new ServerGroup(name, clientToProxyAcceptorThreads,
clientToProxyWorkerThreads, proxyToServerWorkerThreads, messageProcessorExecutor);
}

return new DefaultHttpProxyServer(serverGroup,
Expand Down
Loading