From bdc9523c0295930c9cb3e59066a0e81fdcc48c5b Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Tue, 3 Mar 2026 05:56:28 +0100 Subject: [PATCH] HTTPCLIENT-2261 - Enable HTTP/2 CONNECT tunneling for HTTP/2 clients through HTTP/2 proxies Wire HTTP/2 tunnel establishment into InternalH2ConnPool for tunneled routes by using H2OverH2TunnelSupport to convert an existing proxy HTTP/2 connection into a stream-backed tunnel session --- .../http/impl/async/AsyncConnectExec.java | 10 + .../http/impl/async/H2AsyncClientBuilder.java | 18 +- .../impl/async/InternalH2AsyncClient.java | 6 +- .../http/impl/async/InternalH2ConnPool.java | 310 ++++++++++++++++-- .../http/impl/async/MinimalH2AsyncClient.java | 14 +- .../AsyncClientH2ViaH2ProxyTunnel.java | 150 +++++++++ 6 files changed, 472 insertions(+), 36 deletions(-) create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2ViaH2ProxyTunnel.java diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java index d8dd016339..0606783562 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java @@ -250,6 +250,16 @@ public void cancelled() { public void completed(final AsyncExecRuntime execRuntime) { final HttpHost proxy = route.getProxyHost(); tracker.connectProxy(proxy, route.isSecure() && !route.isTunnelled()); + if (route.isTunnelled() && execRuntime instanceof InternalH2AsyncExecRuntime) { + if (route.getHopCount() > 2) { + asyncExecCallback.failed(new HttpException("Proxy chains are not supported")); + return; + } + tracker.tunnelTarget(false); + if (route.isLayered()) { + tracker.layerProtocol(route.isSecure()); + } + } if (LOG.isDebugEnabled()) { LOG.debug("{} connected to proxy", exchangeId); } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java index 74648da764..57b19f6dfd 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java @@ -840,9 +840,12 @@ public CloseableHttpAsyncClient build() { new H2AsyncMainClientExec(httpProcessor), ChainElement.MAIN_TRANSPORT.name()); + final HttpProcessor proxyConnectHttpProcessor = + new DefaultHttpProcessor(new RequestTargetHost(), new RequestUserAgent(userAgentCopy)); + execChainDefinition.addFirst( new AsyncConnectExec( - new DefaultHttpProcessor(new RequestTargetHost(), new RequestUserAgent(userAgentCopy)), + proxyConnectHttpProcessor, proxyAuthStrategyCopy, schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, authCachingDisabled), @@ -971,7 +974,18 @@ public CloseableHttpAsyncClient build() { } final MultihomeConnectionInitiator connectionInitiator = new MultihomeConnectionInitiator(ioReactor, dnsResolver); - final InternalH2ConnPool connPool = new InternalH2ConnPool(connectionInitiator, host -> null, tlsStrategyCopy); + final InternalH2ConnPool connPool = new InternalH2ConnPool( + connectionInitiator, + host -> null, + tlsStrategyCopy, + ioEventHandlerFactory, + proxyConnectHttpProcessor, + proxyAuthStrategyCopy, + schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, + authCachingDisabled, + authSchemeRegistryCopy, + credentialsProviderCopy, + defaultRequestConfig); connPool.setConnectionConfigResolver(connectionConfigResolver); List closeablesCopy = closeables != null ? new ArrayList<>(closeables) : null; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalH2AsyncClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalH2AsyncClient.java index 4d4c056124..9934c10fc0 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalH2AsyncClient.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalH2AsyncClient.java @@ -108,11 +108,7 @@ HttpRoute determineRoute( final HttpHost httpHost, final HttpRequest request, final HttpClientContext clientContext) throws HttpException { - final HttpRoute route = routePlanner.determineRoute(httpHost, request, clientContext); - if (route.isTunnelled()) { - throw new HttpException("HTTP/2 tunneling not supported"); - } - return route; + return routePlanner.determineRoute(httpHost, request, clientContext); } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalH2ConnPool.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalH2ConnPool.java index 97c6981ff7..6d4b9b77df 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalH2ConnPool.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/InternalH2ConnPool.java @@ -29,24 +29,43 @@ import java.net.InetSocketAddress; import java.util.concurrent.Future; +import org.apache.hc.client5.http.AuthenticationStrategy; import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.SchemePortResolver; +import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthSchemeFactory; +import org.apache.hc.client5.http.auth.AuthenticationException; +import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper; +import org.apache.hc.client5.http.impl.auth.AuthenticationHandler; +import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.concurrent.CallbackContribution; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.function.Resolver; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http.config.Lookup; import org.apache.hc.core5.http.nio.command.ShutdownCommand; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; import org.apache.hc.core5.http2.nio.command.PingCommand; import org.apache.hc.core5.http2.nio.support.BasicPingHandler; +import org.apache.hc.core5.http2.nio.support.H2OverH2TunnelSupport; +import org.apache.hc.core5.http2.nio.support.TunnelRefusedException; +import org.apache.hc.core5.http.protocol.HttpProcessor; import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.io.ModalCloseable; import org.apache.hc.core5.net.NamedEndpoint; import org.apache.hc.core5.reactor.AbstractIOSessionPool; import org.apache.hc.core5.reactor.Command; import org.apache.hc.core5.reactor.ConnectionInitiator; +import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer; import org.apache.hc.core5.util.TimeValue; @@ -58,10 +77,46 @@ class InternalH2ConnPool implements ModalCloseable { private volatile Resolver connectionConfigResolver; - InternalH2ConnPool(final ConnectionInitiator connectionInitiator, - final Resolver addressResolver, - final TlsStrategy tlsStrategy) { - this.sessionPool = new SessionPool(connectionInitiator, addressResolver, tlsStrategy); + InternalH2ConnPool( + final ConnectionInitiator connectionInitiator, + final Resolver addressResolver, + final TlsStrategy tlsStrategy) { + this(connectionInitiator, addressResolver, tlsStrategy, null); + } + + InternalH2ConnPool( + final ConnectionInitiator connectionInitiator, + final Resolver addressResolver, + final TlsStrategy tlsStrategy, + final IOEventHandlerFactory tunnelProtocolStarter) { + this(connectionInitiator, addressResolver, tlsStrategy, tunnelProtocolStarter, + null, null, null, true, null, null, null); + } + + InternalH2ConnPool( + final ConnectionInitiator connectionInitiator, + final Resolver addressResolver, + final TlsStrategy tlsStrategy, + final IOEventHandlerFactory tunnelProtocolStarter, + final HttpProcessor proxyHttpProcessor, + final AuthenticationStrategy proxyAuthStrategy, + final SchemePortResolver schemePortResolver, + final boolean authCachingDisabled, + final Lookup authSchemeRegistry, + final CredentialsProvider credentialsProvider, + final RequestConfig defaultRequestConfig) { + this.sessionPool = new SessionPool( + connectionInitiator, + addressResolver, + tlsStrategy, + tunnelProtocolStarter, + proxyHttpProcessor, + proxyAuthStrategy, + schemePortResolver, + authCachingDisabled, + authSchemeRegistry, + credentialsProvider, + defaultRequestConfig); } @Override @@ -74,9 +129,10 @@ public void close() { sessionPool.close(); } - private ConnectionConfig resolveConnectionConfig(final HttpHost httpHost) { + private ConnectionConfig resolveConnectionConfig(final HttpRoute route) { + final HttpHost firstHop = route.getProxyHost() != null ? route.getProxyHost() : route.getTargetHost(); final Resolver resolver = this.connectionConfigResolver; - final ConnectionConfig connectionConfig = resolver != null ? resolver.resolve(httpHost) : null; + final ConnectionConfig connectionConfig = resolver != null ? resolver.resolve(firstHop) : null; return connectionConfig != null ? connectionConfig : ConnectionConfig.DEFAULT; } @@ -84,7 +140,7 @@ public Future getSession( final HttpRoute route, final Timeout connectTimeout, final FutureCallback callback) { - final ConnectionConfig connectionConfig = resolveConnectionConfig(route.getTargetHost()); + final ConnectionConfig connectionConfig = resolveConnectionConfig(route); return sessionPool.getSession( route, connectTimeout != null ? connectTimeout : connectionConfig.getConnectTimeout(), @@ -118,32 +174,64 @@ public void setValidateAfterInactivity(final TimeValue timeValue) { sessionPool.validateAfterInactivity = timeValue; } - static class SessionPool extends AbstractIOSessionPool { + private static final int MAX_TUNNEL_AUTH_ATTEMPTS = 3; + private final ConnectionInitiator connectionInitiator; private final Resolver addressResolver; private final TlsStrategy tlsStrategy; + private final IOEventHandlerFactory tunnelProtocolStarter; + private final HttpProcessor proxyHttpProcessor; + private final AuthenticationStrategy proxyAuthStrategy; + private final AuthenticationHandler authenticator; + private final AuthCacheKeeper authCacheKeeper; + private final Lookup authSchemeRegistry; + private final CredentialsProvider credentialsProvider; + private final RequestConfig defaultRequestConfig; private volatile TimeValue validateAfterInactivity = TimeValue.NEG_ONE_MILLISECOND; - SessionPool(final ConnectionInitiator connectionInitiator, - final Resolver addressResolver, - final TlsStrategy tlsStrategy) { + SessionPool( + final ConnectionInitiator connectionInitiator, + final Resolver addressResolver, + final TlsStrategy tlsStrategy, + final IOEventHandlerFactory tunnelProtocolStarter, + final HttpProcessor proxyHttpProcessor, + final AuthenticationStrategy proxyAuthStrategy, + final SchemePortResolver schemePortResolver, + final boolean authCachingDisabled, + final Lookup authSchemeRegistry, + final CredentialsProvider credentialsProvider, + final RequestConfig defaultRequestConfig) { this.connectionInitiator = connectionInitiator; this.addressResolver = addressResolver; this.tlsStrategy = tlsStrategy; + this.tunnelProtocolStarter = tunnelProtocolStarter; + this.proxyHttpProcessor = proxyHttpProcessor; + this.proxyAuthStrategy = proxyAuthStrategy; + this.authenticator = proxyHttpProcessor != null && proxyAuthStrategy != null ? new AuthenticationHandler() : null; + this.authCacheKeeper = proxyHttpProcessor != null && proxyAuthStrategy != null && !authCachingDisabled && schemePortResolver != null + ? new AuthCacheKeeper(schemePortResolver) + : null; + this.authSchemeRegistry = authSchemeRegistry; + this.credentialsProvider = credentialsProvider; + this.defaultRequestConfig = defaultRequestConfig != null ? defaultRequestConfig : RequestConfig.DEFAULT; } @Override - protected Future connectSession(final HttpRoute route, - final Timeout connectTimeout, - final FutureCallback callback) { + protected Future connectSession( + final HttpRoute route, + final Timeout connectTimeout, + final FutureCallback callback) { + final HttpHost proxy = route.getProxyHost(); final HttpHost target = route.getTargetHost(); + final HttpHost firstHop = proxy != null ? proxy : target; + final NamedEndpoint firstHopName = proxy == null && route.getTargetName() != null ? route.getTargetName() : firstHop; final InetSocketAddress localAddress = route.getLocalSocketAddress(); - final InetSocketAddress remoteAddress = addressResolver.resolve(target); + final InetSocketAddress remoteAddress = addressResolver.resolve(firstHop); return connectionInitiator.connect( - target, + firstHopName, remoteAddress, localAddress, connectTimeout, @@ -153,34 +241,205 @@ protected Future connectSession(final HttpRoute route, @Override public void completed(final IOSession ioSession) { if (tlsStrategy != null - && URIScheme.HTTPS.same(target.getSchemeName()) + && URIScheme.HTTPS.same(firstHop.getSchemeName()) && ioSession instanceof TransportSecurityLayer) { - final NamedEndpoint tlsName = route.getTargetName() != null ? route.getTargetName() : target; tlsStrategy.upgrade( (TransportSecurityLayer) ioSession, - tlsName, + firstHopName, null, connectTimeout, new CallbackContribution(callback) { @Override public void completed(final TransportSecurityLayer transportSecurityLayer) { - callback.completed(ioSession); + completeConnection(route, connectTimeout, ioSession, callback); } }); ioSession.setSocketTimeout(connectTimeout); } else { - callback.completed(ioSession); + completeConnection(route, connectTimeout, ioSession, callback); + } + } + + }); + } + + private void completeConnection( + final HttpRoute route, + final Timeout connectTimeout, + final IOSession ioSession, + final FutureCallback callback) { + if (!route.isTunnelled()) { + callback.completed(ioSession); + return; + } + if (tunnelProtocolStarter == null) { + callback.failed(new IllegalStateException("HTTP/2 tunnel protocol starter not configured")); + return; + } + if (route.isLayered() && tlsStrategy == null) { + callback.failed(new IllegalStateException("TLS strategy not configured")); + return; + } + final NamedEndpoint targetEndpoint = route.getTargetName() != null ? route.getTargetName() : route.getTargetHost(); + final HttpHost proxy = route.getProxyHost(); + if (proxy != null && proxyHttpProcessor != null && proxyAuthStrategy != null && authenticator != null) { + establishTunnelWithAuth(route, ioSession, targetEndpoint, proxy, connectTimeout, callback); + } else { + H2OverH2TunnelSupport.establish( + ioSession, + targetEndpoint, + connectTimeout, + route.isLayered(), + tlsStrategy, + tunnelProtocolStarter, + callback); + } + } + + private void establishTunnelWithAuth( + final HttpRoute route, + final IOSession ioSession, + final NamedEndpoint targetEndpoint, + final HttpHost proxy, + final Timeout connectTimeout, + final FutureCallback callback) { + final HttpClientContext tunnelContext = HttpClientContext.create(); + if (authSchemeRegistry != null) { + tunnelContext.setAuthSchemeRegistry(authSchemeRegistry); + } + if (credentialsProvider != null) { + tunnelContext.setCredentialsProvider(credentialsProvider); + } + tunnelContext.setRequestConfig(defaultRequestConfig); + + final AuthExchange proxyAuthExchange = tunnelContext.getAuthExchange(proxy); + if (authCacheKeeper != null) { + authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, tunnelContext); + } + establishTunnelWithAuthAttempt( + route, + ioSession, + targetEndpoint, + proxy, + connectTimeout, + callback, + tunnelContext, + proxyAuthExchange, + 1); + } + + private void establishTunnelWithAuthAttempt( + final HttpRoute route, + final IOSession ioSession, + final NamedEndpoint targetEndpoint, + final HttpHost proxy, + final Timeout connectTimeout, + final FutureCallback callback, + final HttpClientContext tunnelContext, + final AuthExchange proxyAuthExchange, + final int attemptCount) { + H2OverH2TunnelSupport.establish( + ioSession, + targetEndpoint, + connectTimeout, + route.isLayered(), + tlsStrategy, + (request, entityDetails, context) -> { + proxyHttpProcessor.process(request, null, tunnelContext); + authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, tunnelContext); + }, + tunnelProtocolStarter, + new FutureCallback() { + + @Override + public void completed(final IOSession result) { + callback.completed(result); + } + + @Override + public void failed(final Exception ex) { + if (!(ex instanceof TunnelRefusedException)) { + callback.failed(ex); + return; + } + final TunnelRefusedException tunnelRefusedException = (TunnelRefusedException) ex; + final HttpResponse response = tunnelRefusedException.getResponse(); + if (response.getCode() != HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED + || attemptCount >= MAX_TUNNEL_AUTH_ATTEMPTS) { + callback.failed(ex); + return; } + try { + proxyHttpProcessor.process(response, null, tunnelContext); + final boolean retry = needAuthentication(proxyAuthExchange, proxy, response, tunnelContext); + if (retry) { + establishTunnelWithAuthAttempt( + route, + ioSession, + targetEndpoint, + proxy, + connectTimeout, + callback, + tunnelContext, + proxyAuthExchange, + attemptCount + 1); + } else { + callback.failed(ex); + } + } catch (final AuthenticationException | MalformedChallengeException authEx) { + callback.failed(authEx); + } catch (final Exception ioEx) { + callback.failed(ioEx); + } + } + + @Override + public void cancelled() { + callback.cancelled(); } }); } + private boolean needAuthentication( + final AuthExchange proxyAuthExchange, + final HttpHost proxy, + final HttpResponse response, + final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { + final RequestConfig config = context.getRequestConfigOrDefault(); + if (config.isAuthenticationEnabled()) { + final boolean proxyAuthRequested = authenticator.isChallenged( + proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); + + if (authCacheKeeper != null) { + if (proxyAuthRequested) { + authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context); + } else { + authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context); + } + } + + if (proxyAuthRequested || proxyMutualAuthRequired) { + final boolean updated = authenticator.handleResponse( + proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); + + if (authCacheKeeper != null) { + authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context); + } + + return updated; + } + } + return false; + } + @Override - protected void validateSession(final IOSession ioSession, - final Callback callback) { + protected void validateSession( + final IOSession ioSession, + final Callback callback) { if (ioSession.isOpen()) { final TimeValue timeValue = validateAfterInactivity; if (TimeValue.isNonNegative(timeValue)) { @@ -202,8 +461,9 @@ protected void validateSession(final IOSession ioSession, } @Override - protected void closeSession(final IOSession ioSession, - final CloseMode closeMode) { + protected void closeSession( + final IOSession ioSession, + final CloseMode closeMode) { if (closeMode == CloseMode.GRACEFUL) { ioSession.enqueue(ShutdownCommand.GRACEFUL, Command.Priority.NORMAL); } else { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalH2AsyncClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalH2AsyncClient.java index da5a47410f..007bd536f5 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalH2AsyncClient.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/MinimalH2AsyncClient.java @@ -54,6 +54,7 @@ import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; import org.apache.hc.core5.http.nio.AsyncPushConsumer; import org.apache.hc.core5.http.nio.CapacityChannel; @@ -79,8 +80,7 @@ /** * Minimal implementation of HTTP/2 only {@link CloseableHttpAsyncClient}. This client * is optimized for HTTP/2 multiplexing message transport and does not support advanced - * HTTP protocol functionality such as request execution via a proxy, state management, - * authentication and request redirects. + * HTTP protocol functionality such as state management, authentication and request redirects. *

* Concurrent message exchanges with the same connection route executed by * this client will get automatically multiplexed over a single physical HTTP/2 @@ -115,7 +115,7 @@ public final class MinimalH2AsyncClient extends AbstractMinimalHttpAsyncClientBa pushConsumerRegistry, threadFactory); this.connectionInitiator = new MultihomeConnectionInitiator(getConnectionInitiator(), dnsResolver); - this.connPool = new InternalH2ConnPool(this.connectionInitiator, object -> null, tlsStrategy); + this.connPool = new InternalH2ConnPool(this.connectionInitiator, object -> null, tlsStrategy, eventHandlerFactory); } @Override @@ -143,8 +143,14 @@ public Cancellable execute( @SuppressWarnings("deprecation") final Timeout connectTimeout = requestConfig.getConnectTimeout(); final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority()); + final HttpHost proxy = requestConfig.getProxy(); + final HttpRoute route = proxy != null ? new HttpRoute( + target, + null, + proxy, + URIScheme.HTTPS.same(target.getSchemeName())) : new HttpRoute(target); - final Future sessionFuture = connPool.getSession(new HttpRoute(target), connectTimeout, + final Future sessionFuture = connPool.getSession(route, connectTimeout, new FutureCallback() { @Override diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2ViaH2ProxyTunnel.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2ViaH2ProxyTunnel.java new file mode 100644 index 0000000000..e32b599728 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientH2ViaH2ProxyTunnel.java @@ -0,0 +1,150 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.examples; + +import java.io.File; +import java.util.concurrent.CountDownLatch; + +import javax.net.ssl.SSLContext; + +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.async.methods.SimpleRequestProducer; +import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.H2AsyncClientBuilder; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.client5.http.routing.HttpRoutePlanner; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http.message.StatusLine; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.ssl.SSLContexts; + +/** + * Full example of pure HTTP/2 client execution through an HTTP/2 proxy tunnel. + * + *

+ * Requirements: + *

+ *
    + *
  • Proxy endpoint speaks HTTP/2.
  • + *
  • Proxy supports CONNECT for the requested target.
  • + *
  • Target endpoint supports HTTP/2.
  • + *
+ * + *

+ * This example configures a tunneled and layered route: + * {@code client -> (h2) proxy -> CONNECT tunnel -> TLS -> (h2) target}. + *

+ */ +public class AsyncClientH2ViaH2ProxyTunnel { + + private static TlsStrategy createTlsStrategy() throws Exception { + final String trustStore = System.getProperty("h2.truststore"); + if (trustStore == null || trustStore.isEmpty()) { + return new H2ClientTlsStrategy(); + } + final String trustStorePassword = System.getProperty("h2.truststore.password", "changeit"); + final SSLContext sslContext = SSLContexts.custom() + .loadTrustMaterial(new File(trustStore), trustStorePassword.toCharArray()) + .build(); + return new H2ClientTlsStrategy(sslContext); + } + + public static void main(final String[] args) throws Exception { + final String proxyScheme = System.getProperty("h2.proxy.scheme", "http"); + final String proxyHost = System.getProperty("h2.proxy.host", "localhost"); + final int proxyPort = Integer.parseInt(System.getProperty("h2.proxy.port", "8080")); + final String targetScheme = System.getProperty("h2.target.scheme", "https"); + final String targetHost = System.getProperty("h2.target.host", "origin"); + final int targetPort = Integer.parseInt(System.getProperty("h2.target.port", "9443")); + final String[] requestUris = System.getProperty("h2.paths", "/").split(","); + + final HttpHost proxy = new HttpHost(proxyScheme, proxyHost, proxyPort); + final HttpHost target = new HttpHost(targetScheme, targetHost, targetPort); + + final HttpRoutePlanner routePlanner = (final HttpHost routeTarget, final org.apache.hc.core5.http.protocol.HttpContext context) -> + new HttpRoute(routeTarget, null, proxy, URIScheme.HTTPS.same(routeTarget.getSchemeName())); + final TlsStrategy tlsStrategy = createTlsStrategy(); + + try (CloseableHttpAsyncClient client = H2AsyncClientBuilder.create() + .setRoutePlanner(routePlanner) + .setTlsStrategy(tlsStrategy) + .build()) { + + client.start(); + + final CountDownLatch latch = new CountDownLatch(requestUris.length); + + for (final String requestUri : requestUris) { + final String normalizedRequestUri = requestUri.trim(); + final SimpleHttpRequest request = SimpleRequestBuilder.get() + .setHttpHost(target) + .setPath(normalizedRequestUri) + .build(); + final HttpClientContext clientContext = HttpClientContext.create(); + + client.execute( + SimpleRequestProducer.create(request), + SimpleResponseConsumer.create(), + clientContext, + new FutureCallback() { + + @Override + public void completed(final SimpleHttpResponse response) { + latch.countDown(); + System.out.println(request + " -> " + new StatusLine(response)); + System.out.println("Protocol: " + clientContext.getProtocolVersion()); + System.out.println(response.getBodyText()); + } + + @Override + public void failed(final Exception ex) { + latch.countDown(); + System.out.println(request + " -> " + ex); + } + + @Override + public void cancelled() { + latch.countDown(); + System.out.println(request + " cancelled"); + } + + }); + } + + latch.await(); + client.close(CloseMode.GRACEFUL); + } + } +}