diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index e047d215e..4204b1768 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -51,6 +51,7 @@ import io.netty.handler.proxy.ProxyHandler; import io.netty.handler.proxy.Socks4ProxyHandler; import io.netty.handler.proxy.Socks5ProxyHandler; +import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.SslHandler; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.handler.timeout.IdleStateHandler; @@ -121,6 +122,7 @@ public class ChannelManager { public static final String HTTP2_FRAME_CODEC = "http2-frame-codec"; public static final String HTTP2_MULTIPLEX = "http2-multiplex"; public static final String AHC_HTTP2_HANDLER = "ahc-http2"; + private static final String TARGET_SSL_HANDLER = "target-ssl"; private static final Logger LOGGER = LoggerFactory.getLogger(ChannelManager.class); // Guards the one-time WARN emitted when a native transport was requested but is unavailable and we // fall back to NIO. Logged once per JVM to avoid spamming logs when many clients are created. @@ -687,13 +689,13 @@ public Future updatePipelineForHttpsTunneling(ChannelPipeline pipeline, // This creates a nested SSL setup: Target SSL -> Proxy SSL -> Network if (isSslHandlerConfigured(pipeline)) { // Insert target SSL handler after the proxy SSL handler - pipeline.addAfter(SSL_HANDLER, "target-ssl", sslHandler); + pipeline.addAfter(SSL_HANDLER, TARGET_SSL_HANDLER, sslHandler); } else { // This shouldn't happen for HTTPS proxy, but fallback pipeline.addBefore(INFLATER_HANDLER, SSL_HANDLER, sslHandler); } - pipeline.addAfter("target-ssl", HTTP_CLIENT_CODEC, newHttpClientCodec()); + pipeline.addAfter(TARGET_SSL_HANDLER, HTTP_CLIENT_CODEC, newHttpClientCodec()); } else { // For HTTPS proxy to HTTP target, just add HTTP codec @@ -965,6 +967,23 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception } } + /** + * Upgrades and registers a proxy tunnel when the target TLS handshake negotiated HTTP/2. + * The caller controls when this runs so the upgrade can happen immediately before the + * tunneled request is sent. + */ + public void upgradePipelineToHttp2AfterProxyConnect(ChannelPipeline pipeline, Object partitionKey) { + SslHandler targetSslHandler = (SslHandler) pipeline.get(TARGET_SSL_HANDLER); + if (targetSslHandler == null) { + targetSslHandler = (SslHandler) pipeline.get(SSL_HANDLER); + } + if (targetSslHandler != null + && ApplicationProtocolNames.HTTP_2.equals(targetSslHandler.applicationProtocol())) { + upgradePipelineToHttp2(pipeline); + registerHttp2Connection(partitionKey, pipeline.channel()); + } + } + public void upgradePipelineForWebSockets(ChannelPipeline pipeline) { pipeline.addAfter(HTTP_CLIENT_CODEC, WS_ENCODER_HANDLER, new WebSocket08FrameEncoder(true)); pipeline.addAfter(WS_ENCODER_HANDLER, WS_DECODER_HANDLER, new WebSocket08FrameDecoder(false, diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index 6df2e70f1..dfd97722b 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -1289,6 +1289,10 @@ public void drainChannelAndExecuteNextRequest(final Channel channel, final Netty public void call() { whenHandshaked.addListener(f -> { if (f.isSuccess()) { + if (!nextRequest.getUri().isWebSocket()) { + channelManager.upgradePipelineToHttp2AfterProxyConnect( + channel.pipeline(), future.getPartitionKey()); + } sendNextRequest(nextRequest, future); } else { future.abort(f.cause()); diff --git a/client/src/test/java/org/asynchttpclient/BasicHttp2Test.java b/client/src/test/java/org/asynchttpclient/BasicHttp2Test.java index 9e97a7b42..e4e9e8bd6 100644 --- a/client/src/test/java/org/asynchttpclient/BasicHttp2Test.java +++ b/client/src/test/java/org/asynchttpclient/BasicHttp2Test.java @@ -48,7 +48,12 @@ import io.netty.pkitesting.CertificateBuilder; import io.netty.pkitesting.X509Bundle; import io.netty.util.concurrent.GlobalEventExecutor; +import org.asynchttpclient.proxy.ProxyServer; +import org.asynchttpclient.proxy.ProxyType; import org.asynchttpclient.test.EventCollectingHandler; +import org.eclipse.jetty.proxy.ConnectHandler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -80,7 +85,10 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.asynchttpclient.Dsl.asyncHttpClient; import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.Dsl.proxyServer; import static org.asynchttpclient.test.TestUtils.AsyncCompletionHandlerAdapter; +import static org.asynchttpclient.test.TestUtils.addHttpConnector; +import static org.asynchttpclient.test.TestUtils.addHttpsConnector; import static org.asynchttpclient.util.DateUtils.unpreciseMillisTime; import static org.asynchttpclient.util.ThrowableUtil.unknownStackTrace; import static org.junit.jupiter.api.Assertions.*; @@ -430,6 +438,52 @@ private AsyncHttpClient http2ClientWithConfig(Consumer