Skip to content

Commit 0801680

Browse files
committed
Never pool a socket whose CONNECT the proxy refused
Motivation: A refused CONNECT leaves a plaintext hop to the proxy. The status check in HttpHandler closes it on the ordinary path, but response filters run ahead of that check, so a ResponseFilter asking for a replay - an everyday retry pattern - short-circuits the interceptor chain and the socket reaches the pool. The next exchange for that origin then polls it back and writes the origin request, Authorization included, down a hop the proxy is still reading in the clear. The 2.x line already guards this at the pool boundary; 3.x did not. Modification: Refuse to pool a channel whose exchange still carries a CONNECT. A CONNECT the proxy accepted is consumed by ConnectSuccessInterceptor, which takes the channel over, so a CONNECT still on the future here means the tunnel was never built. Sampled where keepAlive and the partition key are sampled, because callers hand the drain over and immediately move the future on to the next request. Result: A refused-CONNECT socket is closed rather than pooled, on the filter-replay path as well as the ordinary one, matching 2.16.1.
1 parent d1f45ab commit 0801680

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.netty.channel.group.DefaultChannelGroup;
3333
import io.netty.channel.nio.NioEventLoopGroup;
3434
import io.netty.handler.codec.http.HttpClientCodec;
35+
import io.netty.handler.codec.http.HttpMethod;
3536
import io.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder;
3637
import io.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder;
3738
import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
@@ -81,6 +82,7 @@
8182
import org.asynchttpclient.netty.handler.Http2PingHandler;
8283
import org.asynchttpclient.netty.handler.HttpHandler;
8384
import org.asynchttpclient.netty.handler.WebSocketHandler;
85+
import org.asynchttpclient.netty.request.NettyRequest;
8486
import org.asynchttpclient.netty.request.NettyRequestSender;
8587
import org.asynchttpclient.netty.ssl.DefaultSslEngineFactory;
8688
import org.asynchttpclient.proxy.ProxyServer;
@@ -447,6 +449,36 @@ private Http1ContentDecompressor newHttpContentDecompressor() {
447449
return new Http1ContentDecompressor(config.isKeepEncodingHeader(), config.getMaxDecompressedResponseSize());
448450
}
449451

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+
450482
public final void tryToOfferChannelToPool(Channel channel, AsyncHandler<?> asyncHandler, boolean keepAlive, Object partitionKey) {
451483
if (channel.isActive() && keepAlive) {
452484
LOGGER.debug("Adding key: {} for channel {}", partitionKey, channel);
@@ -1172,10 +1204,16 @@ public void upgradePipelineForWebSockets(ChannelPipeline pipeline) {
11721204
}
11731205

11741206
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+
11751213
return new OnLastHttpContentCallback(future) {
11761214
@Override
11771215
public void call() {
1178-
tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, partitionKey);
1216+
tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, partitionKey, refusedTunnel);
11791217
}
11801218
};
11811219
}

client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ void finishUpdate(NettyResponseFuture<?> future, Channel channel, boolean close)
200200
if (close) {
201201
channelManager.closeChannel(channel);
202202
} else {
203-
channelManager.tryToOfferChannelToPool(channel, future.getAsyncHandler(), true, future.getPartitionKey());
203+
channelManager.tryToOfferChannelToPool(channel, future, true, future.getPartitionKey());
204204
}
205205

206206
try {

0 commit comments

Comments
 (0)