Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ MANIFEST.MF
work
atlassian-ide-plugin.xml
/bom/.flattened-pom.xml

# Docker volumes and logs (but keep configuration)
docker/squid/logs/
docker/nginx/logs/
83 changes: 83 additions & 0 deletions client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,88 @@
<version>2.1.6</version>
<scope>test</scope>
</dependency>

<!-- Testcontainers for Docker-based integration tests -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<profiles>
<profile>
<id>docker-tests</id>
<activation>
<property>
<name>docker.tests</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<skip.docker.tests>false</skip.docker.tests>
<docker.available>true</docker.available>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>testcontainers-auto</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<skip.docker.tests>true</skip.docker.tests>
<!-- Let Testcontainers auto-detect Docker -->
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>no-docker-tests</id>
<activation>
<property>
<name>no.docker.tests</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<skip.docker.tests>true</skip.docker.tests>
<testcontainers.mode>disabled</testcontainers.mode>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public Object getPartitionKey(Uri uri, @Nullable String virtualHost, @Nullable P
targetHostBaseUrl,
virtualHost,
proxyServer.getHost(),
uri.isSecured() && proxyServer.getProxyType() == ProxyType.HTTP ?
uri.isSecured() && proxyServer.getProxyType().isHttp() ?
proxyServer.getSecuredPort() :
proxyServer.getPort(),
proxyServer.getProxyType());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
import org.asynchttpclient.netty.request.NettyRequestSender;
import org.asynchttpclient.netty.ssl.DefaultSslEngineFactory;
import org.asynchttpclient.proxy.ProxyServer;
import org.asynchttpclient.proxy.ProxyType;
import org.asynchttpclient.uri.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -386,14 +387,68 @@ public Future<Channel> updatePipelineForHttpTunneling(ChannelPipeline pipeline,
}

if (requestUri.isSecured()) {
if (!isSslHandlerConfigured(pipeline)) {
SslHandler sslHandler = createSslHandler(requestUri.getHost(), requestUri.getExplicitPort());
whenHandshaked = sslHandler.handshakeFuture();
pipeline.addBefore(INFLATER_HANDLER, SSL_HANDLER, sslHandler);
// For HTTPS targets, we always need to add/replace the SSL handler for the target connection
// even if there's already an SSL handler in the pipeline (which would be for an HTTPS proxy)
if (isSslHandlerConfigured(pipeline)) {
// Remove existing SSL handler (for proxy) and replace with SSL handler for target
pipeline.remove(SSL_HANDLER);
}
SslHandler sslHandler = createSslHandler(requestUri.getHost(), requestUri.getExplicitPort());
whenHandshaked = sslHandler.handshakeFuture();
pipeline.addBefore(INFLATER_HANDLER, SSL_HANDLER, sslHandler);
pipeline.addAfter(SSL_HANDLER, HTTP_CLIENT_CODEC, newHttpClientCodec());

} else {
// For HTTP targets, remove any existing SSL handler (from HTTPS proxy) since target is not secured
if (isSslHandlerConfigured(pipeline)) {
pipeline.remove(SSL_HANDLER);
}
pipeline.addBefore(AHC_HTTP_HANDLER, HTTP_CLIENT_CODEC, newHttpClientCodec());
}

if (requestUri.isWebSocket()) {
pipeline.addAfter(AHC_HTTP_HANDLER, AHC_WS_HANDLER, wsHandler);

if (config.isEnableWebSocketCompression()) {
pipeline.addBefore(AHC_WS_HANDLER, WS_COMPRESSOR_HANDLER, WebSocketClientCompressionHandler.INSTANCE);
}

pipeline.remove(AHC_HTTP_HANDLER);
}
return whenHandshaked;
}

public Future<Channel> updatePipelineForHttpsTunneling(ChannelPipeline pipeline, Uri requestUri, ProxyServer proxyServer) {
Future<Channel> whenHandshaked = null;

// Remove HTTP codec as tunnel is established
if (pipeline.get(HTTP_CLIENT_CODEC) != null) {
pipeline.remove(HTTP_CLIENT_CODEC);
}

if (requestUri.isSecured()) {
// For HTTPS proxy to HTTPS target, we need to establish target SSL over the proxy SSL tunnel
// The proxy SSL handler should remain as it provides the tunnel transport
// We need to add target SSL handler that will negotiate with the target through the tunnel

SslHandler sslHandler = createSslHandler(requestUri.getHost(), requestUri.getExplicitPort());
whenHandshaked = sslHandler.handshakeFuture();

// For HTTPS proxy tunnel, add target SSL handler after the existing proxy SSL handler
// 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);
} 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());

} else {
// For HTTPS proxy to HTTP target, just add HTTP codec
// The proxy SSL handler provides the tunnel and remains
pipeline.addBefore(AHC_HTTP_HANDLER, HTTP_CLIENT_CODEC, newHttpClientCodec());
}

Expand All @@ -406,6 +461,7 @@ public Future<Channel> updatePipelineForHttpTunneling(ChannelPipeline pipeline,

pipeline.remove(AHC_HTTP_HANDLER);
}

return whenHandshaked;
}

Expand Down Expand Up @@ -486,6 +542,10 @@ protected void initChannel(Channel channel) throws Exception {
}
});

} else if (proxy != null && ProxyType.HTTPS.equals(proxy.getProxyType())) {
// For HTTPS proxies, use HTTP bootstrap but ensure SSL connection to proxy
// The SSL handler for connecting to the proxy will be added in the connect phase
promise.setSuccess(httpBootstrap);
} else {
promise.setSuccess(httpBootstrap);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.asynchttpclient.netty.request.NettyRequestSender;
import org.asynchttpclient.netty.timeout.TimeoutsHolder;
import org.asynchttpclient.proxy.ProxyServer;
import org.asynchttpclient.proxy.ProxyType;
import org.asynchttpclient.uri.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -100,8 +101,57 @@ public void onSuccess(Channel channel, InetSocketAddress remoteAddress) {
timeoutsHolder.setResolvedRemoteAddress(remoteAddress);
ProxyServer proxyServer = future.getProxyServer();

// For HTTPS proxies, establish SSL connection to the proxy server first
if (proxyServer != null && ProxyType.HTTPS.equals(proxyServer.getProxyType())) {
SslHandler sslHandler;
try {
sslHandler = channelManager.addSslHandler(channel.pipeline(),
Uri.create("https://" + proxyServer.getHost() + ":" + proxyServer.getSecuredPort()),
null, false);
} catch (Exception sslError) {
onFailure(channel, sslError);
return;
}

final AsyncHandler<?> asyncHandler = future.getAsyncHandler();

try {
asyncHandler.onTlsHandshakeAttempt();
} catch (Exception e) {
LOGGER.error("onTlsHandshakeAttempt crashed", e);
onFailure(channel, e);
return;
}

sslHandler.handshakeFuture().addListener(new SimpleFutureListener<Channel>() {
@Override
protected void onSuccess(Channel value) {
try {
asyncHandler.onTlsHandshakeSuccess(sslHandler.engine().getSession());
} catch (Exception e) {
LOGGER.error("onTlsHandshakeSuccess crashed", e);
NettyConnectListener.this.onFailure(channel, e);
return;
}
// After SSL handshake to proxy, continue with normal proxy request
writeRequest(channel);
}

@Override
protected void onFailure(Throwable cause) {
try {
asyncHandler.onTlsHandshakeFailure(cause);
} catch (Exception e) {
LOGGER.error("onTlsHandshakeFailure crashed", e);
NettyConnectListener.this.onFailure(channel, e);
return;
}
NettyConnectListener.this.onFailure(channel, cause);
}
});

// in case of proxy tunneling, we'll add the SslHandler later, after the CONNECT request
if ((proxyServer == null || proxyServer.getProxyType().isSocks()) && uri.isSecured()) {
} else if ((proxyServer == null || proxyServer.getProxyType().isSocks()) && uri.isSecured()) {
SslHandler sslHandler;
try {
sslHandler = channelManager.addSslHandler(channel.pipeline(), uri, request.getVirtualHost(), proxyServer != null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.asynchttpclient.netty.channel.ChannelManager;
import org.asynchttpclient.netty.request.NettyRequestSender;
import org.asynchttpclient.proxy.ProxyServer;
import org.asynchttpclient.proxy.ProxyType;
import org.asynchttpclient.uri.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -45,7 +46,18 @@ public boolean exitAfterHandlingConnect(Channel channel, NettyResponseFuture<?>

Uri requestUri = request.getUri();
LOGGER.debug("Connecting to proxy {} for scheme {}", proxyServer, requestUri.getScheme());
final Future<Channel> whenHandshaked = channelManager.updatePipelineForHttpTunneling(channel.pipeline(), requestUri);

final Future<Channel> whenHandshaked;

// Special handling for HTTPS proxy tunneling
if (proxyServer != null && ProxyType.HTTPS.equals(proxyServer.getProxyType())) {
// For HTTPS proxy, we need special tunnel pipeline management
whenHandshaked = channelManager.updatePipelineForHttpsTunneling(channel.pipeline(), requestUri, proxyServer);
} else {
// Standard HTTP proxy or SOCKS proxy tunneling
whenHandshaked = channelManager.updatePipelineForHttpTunneling(channel.pipeline(), requestUri);
}

future.setReuseChannel(true);
future.setConnectAllowed(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.asynchttpclient.netty.channel.NettyConnectListener;
import org.asynchttpclient.netty.timeout.TimeoutsHolder;
import org.asynchttpclient.proxy.ProxyServer;
import org.asynchttpclient.proxy.ProxyType;
import org.asynchttpclient.resolver.RequestHostnameResolver;
import org.asynchttpclient.uri.Uri;
import org.asynchttpclient.ws.WebSocketUpgradeHandler;
Expand Down Expand Up @@ -337,7 +338,7 @@ private <T> Future<List<InetSocketAddress>> resolveAddresses(Request request, Pr
final Promise<List<InetSocketAddress>> promise = ImmediateEventExecutor.INSTANCE.newPromise();

if (proxy != null && !proxy.isIgnoredForHost(uri.getHost()) && proxy.getProxyType().isHttp()) {
int port = uri.isSecured() ? proxy.getSecuredPort() : proxy.getPort();
int port = ProxyType.HTTPS.equals(proxy.getProxyType()) || uri.isSecured() ? proxy.getSecuredPort() : proxy.getPort();
InetSocketAddress unresolvedRemoteAddress = InetSocketAddress.createUnresolved(proxy.getHost(), port);
scheduleRequestTimeout(future, unresolvedRemoteAddress);
return RequestHostnameResolver.INSTANCE.resolve(request.getNameResolver(), unresolvedRemoteAddress, asyncHandler);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package org.asynchttpclient.proxy;

public enum ProxyType {
HTTP(true), SOCKS_V4(false), SOCKS_V5(false);
HTTP(true), HTTPS(true), SOCKS_V4(false), SOCKS_V5(false);

private final boolean http;

Expand Down
Loading
Loading