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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.builder.ImageFromDockerfile;
Expand All @@ -53,11 +54,12 @@ public class HttpsProxyTestcontainersIntegrationTest {
private static final int SQUID_HTTP_PORT = 3128;
private static final int SQUID_HTTPS_PORT = 3129;

private static final String TARGET_HTTP_URL = "http://httpbin.org/get";
private static final String TARGET_HTTPS_URL = "https://www.example.com/";

private static boolean dockerAvailable = false;
private static Network network;
private static GenericContainer<?> targetServer;
private static GenericContainer<?> squidProxy;
private static String targetHttpUrl;
private static String targetHttpsUrl;

@BeforeAll
static void checkDockerAvailability() {
Expand All @@ -76,13 +78,21 @@ static void checkDockerAvailability() {
if ("true".equals(System.getProperty("no.docker.tests"))) {
assumeTrue(false, "Docker tests disabled via -Dno.docker.tests=true");
}
// Only start container if Docker is available
// Only start containers if Docker is available
if (dockerAvailable) {
network = Network.newNetwork();
targetServer = LocalProxyTestTarget.start(network);
String targetIp = LocalProxyTestTarget.networkIpOf(targetServer);
targetHttpUrl = "http://" + targetIp + "/get";
targetHttpsUrl = "https://" + targetIp + "/";
LOGGER.info("Local proxy target started at {} / {}", targetHttpUrl, targetHttpsUrl);

squidProxy = new GenericContainer<>(
new ImageFromDockerfile()
.withFileFromPath("Dockerfile", Path.of("src/test/resources/squid/Dockerfile"))
.withFileFromPath("squid.conf", Path.of("src/test/resources/squid/squid.conf"))
)
.withNetwork(network)
.withExposedPorts(SQUID_HTTP_PORT, SQUID_HTTPS_PORT)
.withLogConsumer(new Slf4jLogConsumer(LOGGER).withPrefix("SQUID"))
.waitingFor(Wait.forLogMessage(".*Accepting HTTP.*", 1)
Expand All @@ -96,6 +106,12 @@ static void stopContainer() {
if (squidProxy != null && squidProxy.isRunning()) {
squidProxy.stop();
}
if (targetServer != null && targetServer.isRunning()) {
targetServer.stop();
}
if (network != null) {
network.close();
}
}

@RepeatedIfExceptionsTest(repeats = 3)
Expand All @@ -110,9 +126,9 @@ public void testHttpProxyToHttpTarget() throws Exception {
.setRequestTimeout(Duration.ofMillis(30000))
.build();
try (AsyncHttpClient client = asyncHttpClient(config)) {
Response response = client.executeRequest(get(TARGET_HTTP_URL)).get(30, TimeUnit.SECONDS);
Response response = client.executeRequest(get(targetHttpUrl)).get(30, TimeUnit.SECONDS);
assertEquals(200, response.getStatusCode());
assertTrue(response.getResponseBody().contains("httpbin"));
assertTrue(response.getResponseBody().contains(LocalProxyTestTarget.BODY_MARKER));
LOGGER.info("HTTP proxy to HTTP target test passed");
}
}
Expand All @@ -130,9 +146,9 @@ public void testHttpsProxyToHttpTarget() throws Exception {
.setRequestTimeout(Duration.ofMillis(30000))
.build();
try (AsyncHttpClient client = asyncHttpClient(config)) {
Response response = client.executeRequest(get(TARGET_HTTP_URL)).get(30, TimeUnit.SECONDS);
Response response = client.executeRequest(get(targetHttpUrl)).get(30, TimeUnit.SECONDS);
assertEquals(200, response.getStatusCode());
assertTrue(response.getResponseBody().contains("httpbin"));
assertTrue(response.getResponseBody().contains(LocalProxyTestTarget.BODY_MARKER));
LOGGER.info("HTTPS proxy to HTTP target test passed");
}
}
Expand All @@ -150,10 +166,9 @@ public void testHttpProxyToHttpsTarget() throws Exception {
.setRequestTimeout(Duration.ofMillis(30000))
.build();
try (AsyncHttpClient client = asyncHttpClient(config)) {
Response response = client.executeRequest(get(TARGET_HTTPS_URL)).get(30, TimeUnit.SECONDS);
Response response = client.executeRequest(get(targetHttpsUrl)).get(30, TimeUnit.SECONDS);
assertEquals(200, response.getStatusCode());
assertTrue(response.getResponseBody().contains("Example Domain") ||
response.getResponseBody().contains("example"));
assertTrue(response.getResponseBody().contains("AHC Test Target"));
LOGGER.info("HTTP proxy to HTTPS target test passed");
}
}
Expand All @@ -171,10 +186,9 @@ public void testHttpsProxyToHttpsTarget() throws Exception {
.setRequestTimeout(Duration.ofMillis(30000))
.build();
try (AsyncHttpClient client = asyncHttpClient(config)) {
Response response = client.executeRequest(get(TARGET_HTTPS_URL)).get(30, TimeUnit.SECONDS);
Response response = client.executeRequest(get(targetHttpsUrl)).get(30, TimeUnit.SECONDS);
assertEquals(200, response.getStatusCode());
assertTrue(response.getResponseBody().contains("Example Domain") ||
response.getResponseBody().contains("example"));
assertTrue(response.getResponseBody().contains("AHC Test Target"));
LOGGER.info("HTTPS proxy to HTTPS target test passed - core issue #1907 RESOLVED!");
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
*
* Licensed 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.
*/
package org.asynchttpclient.proxy;

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;

final class LocalProxyTestTarget {

static final int HTTP_PORT = 80;
static final int HTTPS_PORT = 443;
static final String BODY_MARKER = "ahc-test-target";

private LocalProxyTestTarget() {
}

static GenericContainer<?> start(Network network) {
GenericContainer<?> target = new GenericContainer<>(DockerImageName.parse("nginx:1.27-alpine"))
.withNetwork(network)
.withNetworkAliases("ahc-test-target")
.withCopyFileToContainer(MountableFile.forClasspathResource("proxy-target/nginx.conf", 0644), "/etc/nginx/nginx.conf")
.withCopyFileToContainer(MountableFile.forClasspathResource("proxy-target/server.crt", 0644), "/etc/nginx/certs/server.crt")
.withCopyFileToContainer(MountableFile.forClasspathResource("proxy-target/server.key", 0644), "/etc/nginx/certs/server.key")
.withExposedPorts(HTTP_PORT, HTTPS_PORT)
.waitingFor(Wait.forListeningPorts(HTTP_PORT, HTTPS_PORT));
target.start();
return target;
}

static String networkIpOf(GenericContainer<?> container) {
return container.getContainerInfo().getNetworkSettings().getNetworks().values().stream()
.map(n -> n.getIpAddress())
.filter(ip -> ip != null && !ip.isEmpty())
.findFirst()
.orElseThrow(() -> new IllegalStateException("target container has no network IP"));
}
}
7 changes: 5 additions & 2 deletions client/src/test/java/org/asynchttpclient/proxy/ProxyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,10 @@ public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {

@RepeatedIfExceptionsTest(repeats = 5)
public void runSocksProxy() throws Exception {
SocksProxy socksProxy = new SocksProxy(60000);
new Thread(() -> {
try {
new SocksProxy(60000);
socksProxy.run();
} catch (IOException e) {
logger.error("Failed to establish SocksProxy", e);
}
Expand All @@ -330,10 +331,12 @@ public void runSocksProxy() throws Exception {
try (AsyncHttpClient client = asyncHttpClient()) {
String target = "http://localhost:" + port1 + '/';
Future<Response> f = client.prepareGet(target)
.setProxyServer(new ProxyServer.Builder("localhost", 8000).setProxyType(ProxyType.SOCKS_V4))
.setProxyServer(new ProxyServer.Builder("localhost", socksProxy.getPort()).setProxyType(ProxyType.SOCKS_V4))
.execute();

assertEquals(200, f.get(60, TimeUnit.SECONDS).getStatusCode());
} finally {
socksProxy.stop();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,28 +60,26 @@ private static int findFreePort() throws IOException {

@RepeatedIfExceptionsTest(repeats = 5)
public void testSocks4ProxyWithHttp() throws Exception {
// Start SOCKS proxy in background thread
Thread socksProxyThread = new Thread(() -> {
SocksProxy socksProxy = new SocksProxy(60000);
new Thread(() -> {
try {
new SocksProxy(60000);
socksProxy.run();
} catch (Exception e) {
logger.error("Failed to establish SocksProxy", e);
}
});
socksProxyThread.start();

// Give the proxy time to start
Thread.sleep(1000);
}).start();

try (AsyncHttpClient client = asyncHttpClient()) {
String target = "http://localhost:" + port1 + '/';
Future<Response> f = client.prepareGet(target)
.setProxyServer(new ProxyServer.Builder("localhost", 8000).setProxyType(ProxyType.SOCKS_V4))
.setProxyServer(new ProxyServer.Builder("localhost", socksProxy.getPort()).setProxyType(ProxyType.SOCKS_V4))
.execute();

Response response = f.get(60, TimeUnit.SECONDS);
assertNotNull(response);
assertEquals(200, response.getStatusCode());
} finally {
socksProxy.stop();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.builder.ImageFromDockerfile;
Expand Down Expand Up @@ -56,11 +57,12 @@ public class SocksProxyTestcontainersIntegrationTest {

private static final int SOCKS_PORT = 1080;

private static final String TARGET_HTTP_URL = "http://httpbin.org/get";
private static final String TARGET_HTTPS_URL = "https://www.example.com/";

private static boolean dockerAvailable = false;
private static Network network;
private static GenericContainer<?> targetServer;
private static GenericContainer<?> socksProxy;
private static String targetHttpUrl;
private static String targetHttpsUrl;

@BeforeAll
static void checkDockerAvailability() {
Expand All @@ -82,23 +84,31 @@ static void checkDockerAvailability() {
dockerAvailable = false;
return;
}
// Only start container if Docker is available
// Only start containers if Docker is available
if (dockerAvailable) {
try {
network = Network.newNetwork();
targetServer = LocalProxyTestTarget.start(network);
String targetIp = LocalProxyTestTarget.networkIpOf(targetServer);
targetHttpUrl = "http://" + targetIp + "/get";
targetHttpsUrl = "https://" + targetIp + "/";
LOGGER.info("Local proxy target started at {} / {}", targetHttpUrl, targetHttpsUrl);

socksProxy = new GenericContainer<>(
new ImageFromDockerfile()
.withFileFromPath("Dockerfile", Path.of("src/test/resources/dante/Dockerfile"))
.withFileFromPath("sockd.conf", Path.of("src/test/resources/dante/sockd.conf"))
)
.withNetwork(network)
.withExposedPorts(SOCKS_PORT)
.withLogConsumer(new Slf4jLogConsumer(LOGGER).withPrefix("DANTE"))
.waitingFor(Wait.forLogMessage(".*danted.*running.*", 1)
.withStartupTimeout(Duration.ofMinutes(2)));
socksProxy.start();
LOGGER.info("Dante SOCKS proxy started successfully on port {}", socksProxy.getMappedPort(SOCKS_PORT));
} catch (Exception e) {
LOGGER.warn("Failed to start Dante SOCKS proxy container: {}", e.getMessage());
dockerAvailable = false; // Mark as unavailable if container start fails
LOGGER.warn("Failed to start test containers: {}", e.getMessage());
dockerAvailable = false;
}
}
}
Expand All @@ -108,6 +118,12 @@ static void stopContainer() {
if (socksProxy != null && socksProxy.isRunning()) {
socksProxy.stop();
}
if (targetServer != null && targetServer.isRunning()) {
targetServer.stop();
}
if (network != null) {
network.close();
}
}

@RepeatedIfExceptionsTest(repeats = 3)
Expand All @@ -122,9 +138,9 @@ public void testSocks4ProxyToHttpTarget() throws Exception {
.setRequestTimeout(Duration.ofMillis(30000))
.build();
try (AsyncHttpClient client = asyncHttpClient(config)) {
Response response = client.executeRequest(get(TARGET_HTTP_URL)).get(30, TimeUnit.SECONDS);
Response response = client.executeRequest(get(targetHttpUrl)).get(30, TimeUnit.SECONDS);
assertEquals(200, response.getStatusCode());
assertTrue(response.getResponseBody().contains("httpbin"));
assertTrue(response.getResponseBody().contains(LocalProxyTestTarget.BODY_MARKER));
LOGGER.info("SOCKS4 proxy to HTTP target test passed");
}
}
Expand All @@ -141,9 +157,9 @@ public void testSocks5ProxyToHttpTarget() throws Exception {
.setRequestTimeout(Duration.ofMillis(30000))
.build();
try (AsyncHttpClient client = asyncHttpClient(config)) {
Response response = client.executeRequest(get(TARGET_HTTP_URL)).get(30, TimeUnit.SECONDS);
Response response = client.executeRequest(get(targetHttpUrl)).get(30, TimeUnit.SECONDS);
assertEquals(200, response.getStatusCode());
assertTrue(response.getResponseBody().contains("httpbin"));
assertTrue(response.getResponseBody().contains(LocalProxyTestTarget.BODY_MARKER));
LOGGER.info("SOCKS5 proxy to HTTP target test passed");
}
}
Expand All @@ -161,10 +177,9 @@ public void testSocks4ProxyToHttpsTarget() throws Exception {
.setRequestTimeout(Duration.ofMillis(30000))
.build();
try (AsyncHttpClient client = asyncHttpClient(config)) {
Response response = client.executeRequest(get(TARGET_HTTPS_URL)).get(30, TimeUnit.SECONDS);
Response response = client.executeRequest(get(targetHttpsUrl)).get(30, TimeUnit.SECONDS);
assertEquals(200, response.getStatusCode());
assertTrue(response.getResponseBody().contains("Example Domain") ||
response.getResponseBody().contains("example"));
assertTrue(response.getResponseBody().contains("AHC Test Target"));
LOGGER.info("SOCKS4 proxy to HTTPS target test passed - issue #1913 RESOLVED!");
}
}
Expand All @@ -182,10 +197,9 @@ public void testSocks5ProxyToHttpsTarget() throws Exception {
.setRequestTimeout(Duration.ofMillis(30000))
.build();
try (AsyncHttpClient client = asyncHttpClient(config)) {
Response response = client.executeRequest(get(TARGET_HTTPS_URL)).get(30, TimeUnit.SECONDS);
Response response = client.executeRequest(get(targetHttpsUrl)).get(30, TimeUnit.SECONDS);
assertEquals(200, response.getStatusCode());
assertTrue(response.getResponseBody().contains("Example Domain") ||
response.getResponseBody().contains("example"));
assertTrue(response.getResponseBody().contains("AHC Test Target"));
LOGGER.info("SOCKS5 proxy to HTTPS target test passed - issue #1913 RESOLVED!");
}
}
Expand All @@ -206,10 +220,9 @@ public void testIssue1913ReproductionWithRealProxy() throws Exception {
.setRequestTimeout(Duration.ofMillis(30000)))) {

// This would previously throw: java.util.NoSuchElementException: socks
var response = client.prepareGet("https://www.example.com/").execute().get(30, TimeUnit.SECONDS);
var response = client.prepareGet(targetHttpsUrl).execute().get(30, TimeUnit.SECONDS);
assertEquals(200, response.getStatusCode());
assertTrue(response.getResponseBody().contains("Example Domain") ||
response.getResponseBody().contains("example"));
assertTrue(response.getResponseBody().contains("AHC Test Target"));
LOGGER.info("Issue #1913 reproduction test PASSED - NoSuchElementException: socks is FIXED!");
}
}
Expand Down
Loading
Loading