Skip to content

Commit

Permalink
feat: Proper HTTPS proxies support, close #4514
Browse files Browse the repository at this point in the history
Motivation:

Currently, Gatling actually only supports HTTP proxies.
HTTPS proxies work similarly, but require the connection with the proxy itself to be encrypted prior to establishing the tunnel.

Proxies should have only one port. `Proxy(hostname, port).httpsdPort(securedPort)` doesn't make any sense. Instead, we should have `Proxy(hostname, port).https()` (with http() still being the default).

Modifications:

* introduce `Proxy#https()`
* remove `Proxy#httpsPort()`
* HTTPS proxies first perform a TLS handshake with the proxy itselft
  • Loading branch information
slandelle committed Jan 29, 2024
1 parent 68c9b2e commit 77c7a0c
Show file tree
Hide file tree
Showing 18 changed files with 194 additions and 221 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
import io.gatling.http.client.pool.ChannelPool;
import io.gatling.http.client.pool.ChannelPoolKey;
import io.gatling.http.client.pool.RemoteKey;
import io.gatling.http.client.proxy.HttpProxyServer;
import io.gatling.http.client.proxy.ProxyServer;
import io.gatling.http.client.proxy.SockProxyServer;
import io.gatling.http.client.ssl.Tls;
import io.gatling.http.client.uri.Uri;
import io.gatling.http.client.util.Pair;
Expand Down Expand Up @@ -74,6 +74,7 @@ public class DefaultHttpClient implements HttpClient {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHttpClient.class);

private static final String PINNED_HANDLER = "pinned";
private static final String PROXY_SSL_HANDLER = "ssl-proxy";
private static final String PROXY_HANDLER = "proxy";
private static final String SSL_HANDLER = "ssl";
public static final String HTTP_CLIENT_CODEC = "http";
Expand Down Expand Up @@ -119,6 +120,28 @@ private void addWsHandlers(Channel channel) {
.addLast(APP_WS_HANDLER, new WebSocketHandler());
}

private void addProxyHandlers(Channel ch, HttpTx tx, ProxyServer proxyServer) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(PINNED_HANDLER, NoopHandler.INSTANCE);

if (proxyServer instanceof HttpProxyServer) {
HttpProxyServer httpProxyServer = (HttpProxyServer) proxyServer;
if (httpProxyServer.isSecured()) {
installSslHandler(tx, ch, httpProxyServer.getUri(), null, PROXY_SSL_HANDLER)
.addListener(
f -> {
if (tx.requestTimeout.isDone() || !f.isSuccess()) {
ch.close();
}
});
}
}

if (proxyHandlerSupportsUri(proxyServer, tx.request.getUri())) {
pipeline.addLast(PROXY_HANDLER, proxyServer.newProxyHandler());
}
}

private EventLoopResources(EventLoop eventLoop) {
channelPool = new ChannelPool();
long channelPoolIdleCleanerPeriod = config.getChannelPoolIdleCleanerPeriod();
Expand Down Expand Up @@ -173,31 +196,27 @@ protected void initChannel(Channel channel) {
});
}

private Bootstrap getHttp1BootstrapWithProxy(ProxyServer proxy) {
private Bootstrap getHttp1BootstrapWithProxy(HttpTx tx, ProxyServer proxy) {
return http1Bootstrap
.clone()
.handler(
new ChannelInitializer<Channel>() {
new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline()
.addLast(PINNED_HANDLER, NoopHandler.INSTANCE)
.addLast(PROXY_HANDLER, proxy.newHandler());
addProxyHandlers(ch, tx, proxy);
addHttpHandlers(ch);
}
});
}

private Bootstrap getWsBootstrapWithProxy(ProxyServer proxy) {
private Bootstrap getWsBootstrapWithProxy(HttpTx tx, ProxyServer proxy) {
return wsBootstrap
.clone()
.handler(
new ChannelInitializer<Channel>() {
new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline()
.addLast(PINNED_HANDLER, NoopHandler.INSTANCE)
.addLast(PROXY_HANDLER, proxy.newHandler());
addProxyHandlers(ch, tx, proxy);
addWsHandlers(ch);
}
});
Expand Down Expand Up @@ -369,16 +388,12 @@ private void sendTx(HttpTx tx, EventLoop eventLoop) {
sendTxWithChannel(tx, pooledChannel);

} else {
InetSocketAddress unresolvedRemoteAddressThroughTunnelling =
unresolvedRemoteAddressThroughTunnelling(request.getProxyServer(), requestUri);
boolean logProxyAddress = unresolvedRemoteAddressThroughTunnelling != null;

resolveRemoteAddresses(
request,
eventLoop,
unresolvedRemoteAddressThroughTunnelling,
listener,
requestTimeout)
InetSocketAddress proxyHandlerUnresolvedRemoteAddress =
proxyHandlerUnresolvedRemoteAddress(request.getProxyServer(), requestUri);
boolean logProxyAddress = proxyHandlerUnresolvedRemoteAddress != null;

resolveChannelRemoteAddresses(
request, eventLoop, proxyHandlerUnresolvedRemoteAddress, listener, requestTimeout)
.addListener(
(Future<List<InetSocketAddress>> whenRemoteAddresses) -> {
if (requestTimeout.isDone()) {
Expand Down Expand Up @@ -408,7 +423,6 @@ private void sendTx(HttpTx tx, EventLoop eventLoop) {
}

private void sendHttp2Txs(List<HttpTx> txs, EventLoop eventLoop) {

HttpTx tx = txs.get(0);
EventLoopResources resources = eventLoopResources(eventLoop);
Request request = tx.request;
Expand All @@ -422,12 +436,12 @@ private void sendHttp2Txs(List<HttpTx> txs, EventLoop eventLoop) {
}

ProxyServer proxyServer = request.getProxyServer();
InetSocketAddress unresolvedRemoteAddressThroughTunnelling =
unresolvedRemoteAddressThroughTunnelling(proxyServer, requestUri);
boolean logProxyAddress = unresolvedRemoteAddressThroughTunnelling != null;
InetSocketAddress proxyHandlerUnresolvedRemoteAddress =
proxyHandlerUnresolvedRemoteAddress(proxyServer, requestUri);
boolean logProxyAddress = proxyHandlerUnresolvedRemoteAddress != null;

resolveRemoteAddresses(
request, eventLoop, unresolvedRemoteAddressThroughTunnelling, listener, requestTimeout)
resolveChannelRemoteAddresses(
request, eventLoop, proxyHandlerUnresolvedRemoteAddress, listener, requestTimeout)
.addListener(
(Future<List<InetSocketAddress>> whenRemoteAddresses) -> {
if (requestTimeout.isDone()) {
Expand Down Expand Up @@ -455,7 +469,6 @@ private void sendHttp2Txs(List<HttpTx> txs, EventLoop eventLoop) {
}

private void sendTxWithChannel(HttpTx tx, Channel channel) {

if (isClosed()) {
return;
}
Expand All @@ -470,7 +483,6 @@ private void sendTxWithChannel(HttpTx tx, Channel channel) {
}

private void sendHttp2TxsWithChannel(List<HttpTx> txs, Channel channel) {

if (isClosed()) {
return;
}
Expand All @@ -482,34 +494,39 @@ private void sendHttp2TxsWithChannel(List<HttpTx> txs, Channel channel) {
}
}

private InetSocketAddress unresolvedRemoteAddressThroughTunnelling(
private static boolean proxyHandlerSupportsUri(ProxyServer proxyServer, Uri uri) {
// HttpProxyServer only supports CONNECT requests, hence secured requests
return !(proxyServer instanceof HttpProxyServer && !uri.isSecured());
}

private InetSocketAddress proxyHandlerUnresolvedRemoteAddress(
ProxyServer proxyServer, Uri requestUri) {
return proxyServer != null
&& (proxyServer instanceof SockProxyServer
|| requestUri.isSecured()
|| requestUri.isWebSocket())
// HttpProxyHandler doesn't handle clear HTTP requests (absolute URI)
return proxyServer != null && proxyHandlerSupportsUri(proxyServer, requestUri)
? InetSocketAddress.createUnresolved(requestUri.getHost(), requestUri.getExplicitPort())
: null;
}

private Future<List<InetSocketAddress>> resolveRemoteAddresses(
private Future<List<InetSocketAddress>> resolveChannelRemoteAddresses(
Request request,
EventLoop eventLoop,
InetSocketAddress unresolvedRemoteAddressThroughTunnelling,
InetSocketAddress proxyHandlerUnresolvedRemoteAddress,
HttpListener listener,
RequestTimeout requestTimeout) {
ProxyServer proxyServer = request.getProxyServer();
if (proxyServer != null) {
InetSocketAddress remoteAddress =
unresolvedRemoteAddressThroughTunnelling != null
InetSocketAddress channelRemoteAddress =
proxyHandlerUnresolvedRemoteAddress != null
?
// ProxyHandler will take care of the connect logic
unresolvedRemoteAddressThroughTunnelling
proxyHandlerUnresolvedRemoteAddress
:
// directly connect to proxy over clear HTTP
// HttpProxyHandler only handles CONNECT requests, not clear HTTP requests with an
// absolute URL that we have to handle ourselves
proxyServer.getAddress();

return ImmediateEventExecutor.INSTANCE.newSucceededFuture(singletonList(remoteAddress));
return ImmediateEventExecutor.INSTANCE.newSucceededFuture(
singletonList(channelRemoteAddress));

} else {
Promise<List<InetSocketAddress>> p = eventLoop.newPromise();
Expand Down Expand Up @@ -550,6 +567,7 @@ private void sendTxWithNewChannel(
boolean logProxyAddress) {
tx.channelState = HttpTx.ChannelState.NEW;
openNewChannel(
tx,
tx.request,
logProxyAddress,
eventLoop,
Expand All @@ -570,8 +588,12 @@ private void sendTxWithNewChannel(
ChannelPool.registerPoolKey(channel, tx.key);

if (tx.request.getUri().isSecured()) {
LOGGER.debug("Installing SslHandler for {}", tx.request.getUri());
installSslHandler(tx, channel)
installSslHandler(
tx,
channel,
tx.request.getUri(),
tx.request.getVirtualHost(),
SSL_HANDLER)
.addListener(
f -> {
if (tx.requestTimeout.isDone() || !f.isSuccess()) {
Expand Down Expand Up @@ -611,6 +633,7 @@ private void sendHttp2TxsWithNewChannel(
boolean logProxyAddress) {
HttpTx tx = txs.get(0);
openNewChannel(
tx,
tx.request,
logProxyAddress,
eventLoop,
Expand All @@ -631,7 +654,8 @@ private void sendHttp2TxsWithNewChannel(
ChannelPool.registerPoolKey(channel, tx.key);

LOGGER.debug("Installing SslHandler for {}", tx.request.getUri());
installSslHandler(tx, channel)
installSslHandler(
tx, channel, tx.request.getUri(), tx.request.getVirtualHost(), SSL_HANDLER)
.addListener(
f -> {
if (tx.requestTimeout.isDone() || !f.isSuccess()) {
Expand All @@ -653,16 +677,17 @@ private void sendHttp2TxsWithNewChannel(
});
}

private Bootstrap bootstrap(Request request, EventLoopResources resources) {
private Bootstrap bootstrap(HttpTx tx, Request request, EventLoopResources resources) {
Uri uri = request.getUri();
ProxyServer proxyServer = request.getProxyServer();

if (proxyServer != null) {
if (uri.isWebSocket()) {
return resources.getWsBootstrapWithProxy(proxyServer);
} else if (proxyServer instanceof SockProxyServer || uri.isSecured()) {
return resources.getWsBootstrapWithProxy(tx, proxyServer);
} else {
// HttpProxyHandler doesn't handle clear HTTP requests, only CONNECT ones
// FIXME HTTP/2 with proxy
return resources.getHttp1BootstrapWithProxy(proxyServer);
return resources.getHttp1BootstrapWithProxy(tx, proxyServer);
}
}

Expand All @@ -682,6 +707,7 @@ private static InetSocketAddress localAddressWithRandomPort(InetAddress localAdd
}

private Future<Channel> openNewChannel(
HttpTx tx,
Request request,
boolean logProxyAddress,
EventLoop eventLoop,
Expand All @@ -690,7 +716,7 @@ private Future<Channel> openNewChannel(
HttpListener listener,
RequestTimeout requestTimeout) {
LOGGER.debug("Opening new channel");
Bootstrap bootstrap = bootstrap(request, resources);
Bootstrap bootstrap = bootstrap(tx, request, resources);
Promise<Channel> channelPromise = eventLoop.newPromise();
InetSocketAddress loggedProxyAddress =
logProxyAddress ? request.getProxyServer().getAddress() : null;
Expand Down Expand Up @@ -837,23 +863,20 @@ private void openNewChannelRec(
}
}

private Future<Channel> installSslHandler(HttpTx tx, Channel channel) {
private Future<Channel> installSslHandler(
HttpTx tx, Channel channel, Uri uri, String virtualHost, String sslHandlerName) {
LOGGER.debug("Installing SslHandler for {}", tx.request.getUri());
// [e]
//
// [e]

try {
SslHandler sslHandler =
SslHandlers.newSslHandler(
tx.sslContext(),
channel.alloc(),
tx.request.getUri(),
tx.request.getVirtualHost(),
config);
SslHandlers.newSslHandler(tx.sslContext(), channel.alloc(), uri, virtualHost, config);

ChannelPipeline pipeline = channel.pipeline();
String after = pipeline.get(PROXY_HANDLER) != null ? PROXY_HANDLER : PINNED_HANDLER;
pipeline.addAfter(after, SSL_HANDLER, sslHandler);
pipeline.addAfter(after, sslHandlerName, sslHandler);

return sslHandler
.handshakeFuture()
Expand All @@ -866,7 +889,9 @@ private Future<Channel> installSslHandler(HttpTx tx, Channel channel) {
if (f.isSuccess()) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"TLS handshake successful: protocol={} cipher suite={}",
"TLS handshake successful: peerHost={} peerPort={} protocol={} cipher suite={}",
sslHandler.engine().getSession().getPeerHost(),
sslHandler.engine().getSession().getPeerPort(),
sslHandler.engine().getSession().getProtocol(),
sslHandler.engine().getSession().getCipherSuite());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package io.gatling.http.client.pool;

import io.gatling.http.client.proxy.HttpProxyServer;
import io.gatling.http.client.proxy.ProxyServer;
import io.gatling.http.client.uri.Uri;
import java.util.Objects;
Expand All @@ -29,12 +28,7 @@ public static RemoteKey newKey(Uri uri, String virtualHost, ProxyServer proxySer
return new RemoteKey(targetHostBaseUrl, virtualHost, null, 0);
} else {
return new RemoteKey(
targetHostBaseUrl,
virtualHost,
proxyServer.getHost(),
uri.isSecured() && proxyServer instanceof HttpProxyServer
? ((HttpProxyServer) proxyServer).getSecuredPort()
: proxyServer.getPort());
targetHostBaseUrl, virtualHost, proxyServer.getHost(), proxyServer.getPort());
}
}

Expand Down

0 comments on commit 77c7a0c

Please sign in to comment.