22
33import io .netty .bootstrap .Bootstrap ;
44import io .netty .bootstrap .ServerBootstrap ;
5+ import io .netty .buffer .ByteBuf ;
56import io .netty .buffer .ByteBufUtil ;
67import io .netty .buffer .Unpooled ;
78import io .netty .channel .*;
2021import java .io .ByteArrayOutputStream ;
2122import java .net .URI ;
2223import java .net .URISyntaxException ;
24+ import java .util .List ;
2325
2426/**
2527 Hosts the jsoup HTTP proxy listeners on Netty.
@@ -97,7 +99,7 @@ public static int closeAuthedConnections() {
9799 }
98100
99101 /**
100- Binds one proxy listener with the supplied auth mode
102+ Binds a proxy listener with the supplied auth mode
101103 */
102104 private static Channel bind (boolean alwaysAuth ) throws InterruptedException {
103105 return new ServerBootstrap ()
@@ -110,7 +112,9 @@ protected void initChannel(SocketChannel ch) {
110112 if (alwaysAuth ) {
111113 AuthedConnections .add (ch );
112114 }
113- ch .pipeline ().addLast (new HttpServerCodec ());
115+ // split decode/encode so that CONNECT can pause request decoding while the 200 response is writing:
116+ ch .pipeline ().addLast (new ProxyRequestDecoder ());
117+ ch .pipeline ().addLast (new HttpResponseEncoder ());
114118 ch .pipeline ().addLast (new ProxyFrontendHandler (alwaysAuth ));
115119 }
116120 })
@@ -119,6 +123,27 @@ protected void initChannel(SocketChannel ch) {
119123 .channel ();
120124 }
121125
126+ /**
127+ Decodes proxy requests and stops after CONNECT, so tunnel bytes stay buffered for handoff
128+ */
129+ private static final class ProxyRequestDecoder extends HttpRequestDecoder {
130+ @ Override
131+ protected void decode (ChannelHandlerContext ctx , ByteBuf input , List <Object > output ) throws Exception {
132+ int firstOutput = output .size ();
133+ setSingleDecode (false );
134+ super .decode (ctx , input , output );
135+
136+ for (int i = firstOutput ; i < output .size (); i ++) {
137+ Object message = output .get (i );
138+ if (message instanceof HttpRequest && HttpMethod .CONNECT .equals (((HttpRequest ) message ).method ())) {
139+ // keep anyy already buffered TLS bytes here until the tunnel relay is in place:
140+ setSingleDecode (true );
141+ return ;
142+ }
143+ }
144+ }
145+ }
146+
122147 /**
123148 Handles parsed proxy requests on the client-facing listener
124149 */
@@ -212,6 +237,7 @@ private void startConnectRequest(ChannelHandlerContext ctx, HttpRequest request)
212237 return ;
213238 }
214239
240+ pauseClientReads (ctx );
215241 Bootstrap bootstrap = new Bootstrap ()
216242 .group (ioGroup )
217243 .channel (NioSocketChannel .class )
@@ -223,14 +249,29 @@ protected void initChannel(SocketChannel ch) {
223249 }
224250 });
225251
226- bootstrap .connect (target .host , target .port ).addListener ((ChannelFutureListener ) future -> {
227- if (!future .isSuccess ()) {
228- sendSimpleResponse (ctx , request , HttpResponseStatus .BAD_GATEWAY , "Bad Gateway" );
229- return ;
252+ bootstrap .connect (target .host , target .port )
253+ .addListener ((ChannelFutureListener ) future ->
254+ ctx .executor ().execute (() -> finishConnectRequest (ctx , request , future )));
255+ }
256+
257+ /**
258+ Finishes the asynchronous CONNECT upstream connection on the client channel event loop.
259+ */
260+ private void finishConnectRequest (ChannelHandlerContext ctx , HttpRequest request , ChannelFuture future ) {
261+ if (!ctx .channel ().isActive ()) {
262+ if (future .isSuccess ()) {
263+ future .channel ().close ();
230264 }
265+ return ;
266+ }
231267
232- establishTunnel (ctx , future .channel ());
233- });
268+ if (!future .isSuccess ()) {
269+ resumeClientReads (ctx );
270+ sendSimpleResponse (ctx , request , HttpResponseStatus .BAD_GATEWAY , "Bad Gateway" );
271+ return ;
272+ }
273+
274+ establishTunnel (ctx , future .channel ());
234275 }
235276
236277 /**
@@ -285,14 +326,29 @@ private void establishTunnel(ChannelHandlerContext ctx, Channel upstream) {
285326 }
286327
287328 ChannelPipeline pipeline = ctx .pipeline ();
288- if (pipeline .get (HttpServerCodec .class ) != null ) {
289- pipeline .remove (HttpServerCodec .class );
290- }
329+ // install the relay before removing the decoder. removal forwards buffered tunnel bytes:
291330 pipeline .replace (this , "proxyTunnelRelay" , new TunnelRelayHandler (upstream ));
331+ pipeline .remove (ProxyRequestDecoder .class );
332+ pipeline .remove (HttpResponseEncoder .class );
333+ resumeClientReads (ctx );
292334 });
293335 }
294336 }
295337
338+ /**
339+ Pauses future client reads while a CONNECT tunnel is being installed
340+ */
341+ private static void pauseClientReads (ChannelHandlerContext ctx ) {
342+ ctx .channel ().config ().setAutoRead (false );
343+ }
344+
345+ /**
346+ Resumes client reads after a paused CONNECT handoff is complete
347+ */
348+ private static void resumeClientReads (ChannelHandlerContext ctx ) {
349+ ctx .channel ().config ().setAutoRead (true );
350+ }
351+
296352 /**
297353 Buffers a parsed HTTP proxy request body before forwarding it upstream
298354 */
0 commit comments