|
32 | 32 | import io.netty.channel.group.DefaultChannelGroup; |
33 | 33 | import io.netty.channel.nio.NioEventLoopGroup; |
34 | 34 | import io.netty.handler.codec.http.HttpClientCodec; |
| 35 | +import io.netty.handler.codec.http.HttpMethod; |
35 | 36 | import io.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder; |
36 | 37 | import io.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder; |
37 | 38 | import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator; |
|
81 | 82 | import org.asynchttpclient.netty.handler.Http2PingHandler; |
82 | 83 | import org.asynchttpclient.netty.handler.HttpHandler; |
83 | 84 | import org.asynchttpclient.netty.handler.WebSocketHandler; |
| 85 | +import org.asynchttpclient.netty.request.NettyRequest; |
84 | 86 | import org.asynchttpclient.netty.request.NettyRequestSender; |
85 | 87 | import org.asynchttpclient.netty.ssl.DefaultSslEngineFactory; |
86 | 88 | import org.asynchttpclient.proxy.ProxyServer; |
@@ -447,6 +449,36 @@ private Http1ContentDecompressor newHttpContentDecompressor() { |
447 | 449 | return new Http1ContentDecompressor(config.isKeepEncodingHeader(), config.getMaxDecompressedResponseSize()); |
448 | 450 | } |
449 | 451 |
|
| 452 | + /** |
| 453 | + * Whether the exchange being finished was a CONNECT that never became a tunnel. A CONNECT the proxy |
| 454 | + * accepted is consumed by {@link org.asynchttpclient.netty.handler.intercept.ConnectSuccessInterceptor}, |
| 455 | + * which takes the channel over, so a CONNECT still on the future here means the proxy refused it and the |
| 456 | + * socket is a plaintext hop to the proxy. Pooling it would let the next exchange for that origin send the |
| 457 | + * origin request, {@code Authorization} included, down a hop the proxy is still reading in the clear. |
| 458 | + * <p> |
| 459 | + * The status check in {@code HttpHandler} closes this on the ordinary path, but a {@code ResponseFilter} |
| 460 | + * asking for a replay short-circuits {@code exitAfterIntercept} before that check runs, so the guard has |
| 461 | + * to live at the pool boundary too. |
| 462 | + */ |
| 463 | + private static boolean isRefusedTunnel(NettyResponseFuture<?> future) { |
| 464 | + NettyRequest nettyRequest = future.getNettyRequest(); |
| 465 | + return nettyRequest != null && nettyRequest.getHttpRequest().method() == HttpMethod.CONNECT; |
| 466 | + } |
| 467 | + |
| 468 | + public void tryToOfferChannelToPool(Channel channel, NettyResponseFuture<?> future, boolean keepAlive, Object partitionKey) { |
| 469 | + tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, partitionKey, isRefusedTunnel(future)); |
| 470 | + } |
| 471 | + |
| 472 | + private void tryToOfferChannelToPool(Channel channel, AsyncHandler<?> asyncHandler, boolean keepAlive, Object partitionKey, |
| 473 | + boolean refusedTunnel) { |
| 474 | + if (refusedTunnel) { |
| 475 | + LOGGER.debug("Not offering channel {} to the pool: the CONNECT on it was never established", channel); |
| 476 | + closeChannel(channel); |
| 477 | + return; |
| 478 | + } |
| 479 | + tryToOfferChannelToPool(channel, asyncHandler, keepAlive, partitionKey); |
| 480 | + } |
| 481 | + |
450 | 482 | public final void tryToOfferChannelToPool(Channel channel, AsyncHandler<?> asyncHandler, boolean keepAlive, Object partitionKey) { |
451 | 483 | if (channel.isActive() && keepAlive) { |
452 | 484 | LOGGER.debug("Adding key: {} for channel {}", partitionKey, channel); |
@@ -1172,10 +1204,16 @@ public void upgradePipelineForWebSockets(ChannelPipeline pipeline) { |
1172 | 1204 | } |
1173 | 1205 |
|
1174 | 1206 | private OnLastHttpContentCallback newDrainCallback(final NettyResponseFuture<?> future, final Channel channel, final boolean keepAlive, final Object partitionKey) { |
| 1207 | + |
| 1208 | + // Sampled here rather than in call(), for the same reason keepAlive and partitionKey are: callers |
| 1209 | + // hand the drain over and immediately move the future on to the NEXT request, so by the time the |
| 1210 | + // last chunk arrives the future no longer describes the response being drained off this channel. |
| 1211 | + final boolean refusedTunnel = isRefusedTunnel(future); |
| 1212 | + |
1175 | 1213 | return new OnLastHttpContentCallback(future) { |
1176 | 1214 | @Override |
1177 | 1215 | public void call() { |
1178 | | - tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, partitionKey); |
| 1216 | + tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, partitionKey, refusedTunnel); |
1179 | 1217 | } |
1180 | 1218 | }; |
1181 | 1219 | } |
|
0 commit comments