Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public class PoolingHttpClientConnectionManagerBuilder {
private Resolver<HttpRoute, ConnectionConfig> connectionConfigResolver;
private Resolver<HttpHost, TlsConfig> tlsConfigResolver;

private boolean enableHttpsProxyTunnelling; // <— default false

private boolean systemProperties;

private int maxConnTotal;
Expand Down Expand Up @@ -304,11 +306,32 @@ public final PoolingHttpClientConnectionManagerBuilder useSystemProperties() {
return this;
}

/**
* Enables double-TLS tunnelling when the route contains an {@code https://} proxy.
*
* @return this builder for method chaining
* @since 5.6
*/
public PoolingHttpClientConnectionManagerBuilder useHttpsProxyTunnelling() {
this.enableHttpsProxyTunnelling = true;
return this;
}

@Internal
protected HttpClientConnectionOperator createConnectionOperator(
final SchemePortResolver schemePortResolver,
final DnsResolver dnsResolver,
final TlsSocketStrategy tlsSocketStrategy) {

if (enableHttpsProxyTunnelling) {
return new ProxyTlsConnectionOperator(
RegistryBuilder.<TlsSocketStrategy>create()
.register(URIScheme.HTTPS.id, tlsSocketStrategy)
.build(),
schemePortResolver,
dnsResolver);
}

return new DefaultHttpClientConnectionOperator(schemePortResolver, dnsResolver,
RegistryBuilder.<TlsSocketStrategy>create()
.register(URIScheme.HTTPS.id, tlsSocketStrategy)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* ====================================================================
* 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
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.io;

import java.io.IOException;
import java.net.Socket;

import javax.net.ssl.SSLSocket;

import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.SchemePortResolver;
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
import org.apache.hc.core5.http.ConnectionClosedException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.URIScheme;
import org.apache.hc.core5.http.config.Lookup;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.net.NamedEndpoint;

/**
* Connection-operator that supports “double-TLS” proxy tunnelling for the
* <strong>classic / blocking</strong> transport.
*
* <pre>
* client ── TLS#1 ──► HTTPS-proxy CONNECT origin ── TLS#2 ──►
* </pre>
*
* <p>The operator lets the default implementation build the plain tunnel,
* then layers a second TLS handshake on the already-encrypted socket and
* re-binds it to the pooled connection.</p>
*
* @since 5.6
*/
public final class ProxyTlsConnectionOperator extends DefaultHttpClientConnectionOperator {

private final TlsSocketStrategy tlsStrategy;

/**
* System-defaults constructor.
*/
public ProxyTlsConnectionOperator(final Lookup<TlsSocketStrategy> tlsLookup) {
this(tlsLookup, null, SystemDefaultDnsResolver.INSTANCE);
}

/**
* Full-control constructor.
*/
public ProxyTlsConnectionOperator(final Lookup<TlsSocketStrategy> tlsLookup,
final SchemePortResolver schemePortResolver,
final DnsResolver dnsResolver) {
super(schemePortResolver, dnsResolver, tlsLookup);
this.tlsStrategy = tlsLookup.lookup(URIScheme.HTTPS.id);
if (this.tlsStrategy == null) {
throw new IllegalArgumentException(
"Lookup must contain a TlsSocketStrategy for scheme 'https'");
}
}

@Override
public void upgrade(final ManagedHttpClientConnection conn,
final HttpHost endpointHost,
final NamedEndpoint endpointName,
final Object attachment,
final HttpContext context) throws IOException {

final Socket raw = conn.getSocket();
if (raw == null) {
throw new ConnectionClosedException("Connection already closed");
}

/* Layer TLS#2 on top of the proxy TLS session */
final SSLSocket layered = tlsStrategy.upgrade(
raw,
endpointHost.getHostName(),
endpointHost.getPort(),
attachment,
context);

/* Re-bind so the pool sees the secure socket */
conn.bind(layered, raw);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ public class PoolingAsyncClientConnectionManagerBuilder {
private Resolver<HttpHost, TlsConfig> tlsConfigResolver;
private boolean messageMultiplexing;

private boolean enableHttpsAyncProxyTunnelling; // <— default false


public static PoolingAsyncClientConnectionManagerBuilder create() {
return new PoolingAsyncClientConnectionManagerBuilder();
}
Expand Down Expand Up @@ -256,6 +259,16 @@ public final PoolingAsyncClientConnectionManagerBuilder useSystemProperties() {
return this;
}

/**
* Enables <strong>TLS-in-TLS tunnelling</strong> for the <em>reactor / async</em> transport.
*
* @since 5.6
*/
public PoolingAsyncClientConnectionManagerBuilder useHttpsAsyncProxyTunnelling() {
this.enableHttpsAyncProxyTunnelling = true;
return this;
}

/**
* Use experimental connections pool implementation that acts as a caching facade
* in front of a standard connection pool and shares already leased connections
Expand Down Expand Up @@ -306,8 +319,18 @@ public PoolingAsyncClientConnectionManager build() {
}
}
}

final AsyncClientConnectionOperator operator = enableHttpsAyncProxyTunnelling
? new ProxyTlsAsyncConnectionOperator(
RegistryBuilder.<TlsStrategy>create()
.register(URIScheme.HTTPS.id, tlsStrategyCopy)
.build(),
schemePortResolver,
dnsResolver)
: createConnectionOperator(tlsStrategyCopy, schemePortResolver, dnsResolver);

final PoolingAsyncClientConnectionManager poolingmgr = new PoolingAsyncClientConnectionManager(
createConnectionOperator(tlsStrategyCopy, schemePortResolver, dnsResolver),
operator,
poolConcurrencyPolicy,
poolReusePolicy,
null,
Expand All @@ -323,4 +346,5 @@ public PoolingAsyncClientConnectionManager build() {
return poolingmgr;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* ====================================================================
* 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
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.nio;

import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.SchemePortResolver;
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.impl.ConnPoolSupport;
import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
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.config.Lookup;
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.net.NamedEndpoint;
import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
import org.apache.hc.core5.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Connection-operator that enables <em>double-TLS</em> tunnelling for the
* <strong>reactor / async</strong> transport.
*
* <pre>
* client ── TLS#1 ──► HTTPS-proxy CONNECT origin ── TLS#2 ──►
* </pre>
*
* <p>The first hop (client → proxy) may already be protected by TLS when the
* proxy itself speaks HTTPS. If {@link ManagedAsyncClientConnection#getTlsDetails()}
* returns non-{@code null}, this operator skips the standard upgrade path and
* immediately starts a <em>second</em> TLS handshake to the target host inside
* the tunnel. For plain-HTTP proxies the logic falls back to
* {@link DefaultAsyncClientConnectionOperator#upgrade(ManagedAsyncClientConnection, HttpHost, NamedEndpoint, Object, HttpContext, FutureCallback)} unchanged.</p>
*
* @since 5.6
*/
public final class ProxyTlsAsyncConnectionOperator extends DefaultAsyncClientConnectionOperator {

private static final Logger LOG =
LoggerFactory.getLogger(ProxyTlsAsyncConnectionOperator.class);

private final TlsStrategy tlsStrategy;

/**
* Builds an operator that uses the system-default DNS resolver and
* scheme-port resolver.
*
* @param tlsLookup registry that <em>must</em> contain a {@link TlsStrategy}
* under the {@code "https"} scheme.
*/
public ProxyTlsAsyncConnectionOperator(final Lookup<TlsStrategy> tlsLookup) {
this(tlsLookup, null, SystemDefaultDnsResolver.INSTANCE);
}

/**
* Full-control constructor.
*
* @param tlsLookup registry containing the TLS strategy.
* @param schemePortResolver optional custom scheme-port resolver.
* @param dnsResolver optional custom DNS resolver.
*/
public ProxyTlsAsyncConnectionOperator(final Lookup<TlsStrategy> tlsLookup,
final SchemePortResolver schemePortResolver,
final DnsResolver dnsResolver) {
super(tlsLookup, schemePortResolver, dnsResolver);
this.tlsStrategy = tlsLookup.lookup(URIScheme.HTTPS.id);
}

@Override
public void upgrade(final ManagedAsyncClientConnection connection,
final HttpHost endpointHost,
final NamedEndpoint endpointName,
final Object attachment,
final HttpContext context,
final FutureCallback<ManagedAsyncClientConnection> callback) {

final NamedEndpoint tlsName =
endpointName != null ? endpointName : endpointHost;

if (connection.getTlsDetails() != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} proxy hop is already TLS, starting inner TLS to {}",
ConnPoolSupport.getId(connection), tlsName);
}
startInnerTls(connection, tlsName, attachment, callback);
return;
}

/* Plain HTTP proxy → standard upgrade path */
super.upgrade(connection, endpointHost, endpointName, attachment, context, callback);
}

/**
* Initiates TLS#2 inside the CONNECT tunnel.
*/
private void startInnerTls(final ManagedAsyncClientConnection connection,
final NamedEndpoint tlsName,
final Object attachment,
final FutureCallback<ManagedAsyncClientConnection> callback) {
try {
tlsStrategy.upgrade(
connection,
tlsName,
attachment,
Timeout.ofMinutes(1),
new FutureCallback<TransportSecurityLayer>() {
@Override
public void completed(final TransportSecurityLayer result) {
if (callback != null) {
callback.completed(connection);
}
}

@Override
public void failed(final Exception ex) {
if (callback != null) {
callback.failed(ex);
}
}

@Override
public void cancelled() {
if (callback != null) {
callback.cancelled();
}
}
});
} catch (final Exception ex) {
if (callback != null) {
callback.failed(ex);
}
}
}
}
Loading
Loading