|  | 
|  | 1 | +/* | 
|  | 2 | + * Copyright (c) 2024 AsyncHttpClient Project. All rights reserved. | 
|  | 3 | + * | 
|  | 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 5 | + * you may not use this file except in compliance with the License. | 
|  | 6 | + * You may obtain a copy of the License at | 
|  | 7 | + * | 
|  | 8 | + *     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 9 | + * | 
|  | 10 | + * Unless required by applicable law or agreed to in writing, software | 
|  | 11 | + * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 13 | + * See the License for the specific language governing permissions and | 
|  | 14 | + * limitations under the License. | 
|  | 15 | + */ | 
|  | 16 | +package org.asynchttpclient.proxy; | 
|  | 17 | + | 
|  | 18 | +import io.github.artsok.RepeatedIfExceptionsTest; | 
|  | 19 | +import org.asynchttpclient.AbstractBasicTest; | 
|  | 20 | +import org.asynchttpclient.AsyncHttpClient; | 
|  | 21 | +import org.asynchttpclient.Response; | 
|  | 22 | +import org.asynchttpclient.testserver.SocksProxy; | 
|  | 23 | +import org.eclipse.jetty.server.handler.AbstractHandler; | 
|  | 24 | +import org.junit.jupiter.api.Test; | 
|  | 25 | + | 
|  | 26 | +import java.time.Duration; | 
|  | 27 | +import java.util.concurrent.ExecutionException; | 
|  | 28 | +import java.util.concurrent.Future; | 
|  | 29 | +import java.util.concurrent.TimeUnit; | 
|  | 30 | + | 
|  | 31 | +import static org.asynchttpclient.Dsl.asyncHttpClient; | 
|  | 32 | +import static org.asynchttpclient.Dsl.config; | 
|  | 33 | +import static org.junit.jupiter.api.Assertions.assertEquals; | 
|  | 34 | +import static org.junit.jupiter.api.Assertions.assertNotNull; | 
|  | 35 | + | 
|  | 36 | +/** | 
|  | 37 | + * Tests for SOCKS proxy support with both HTTP and HTTPS. | 
|  | 38 | + */ | 
|  | 39 | +public class SocksProxyTest extends AbstractBasicTest { | 
|  | 40 | + | 
|  | 41 | +    @Override | 
|  | 42 | +    public AbstractHandler configureHandler() throws Exception { | 
|  | 43 | +        return new ProxyTest.ProxyHandler(); | 
|  | 44 | +    } | 
|  | 45 | + | 
|  | 46 | +    @RepeatedIfExceptionsTest(repeats = 5) | 
|  | 47 | +    public void testSocks4ProxyWithHttp() throws Exception { | 
|  | 48 | +        // Start SOCKS proxy in background thread | 
|  | 49 | +        Thread socksProxyThread = new Thread(() -> { | 
|  | 50 | +            try { | 
|  | 51 | +                new SocksProxy(60000); | 
|  | 52 | +            } catch (Exception e) { | 
|  | 53 | +                logger.error("Failed to establish SocksProxy", e); | 
|  | 54 | +            } | 
|  | 55 | +        }); | 
|  | 56 | +        socksProxyThread.start(); | 
|  | 57 | + | 
|  | 58 | +        // Give the proxy time to start | 
|  | 59 | +        Thread.sleep(1000); | 
|  | 60 | + | 
|  | 61 | +        try (AsyncHttpClient client = asyncHttpClient()) { | 
|  | 62 | +            String target = "http://localhost:" + port1 + '/'; | 
|  | 63 | +            Future<Response> f = client.prepareGet(target) | 
|  | 64 | +                    .setProxyServer(new ProxyServer.Builder("localhost", 8000).setProxyType(ProxyType.SOCKS_V4)) | 
|  | 65 | +                    .execute(); | 
|  | 66 | + | 
|  | 67 | +            Response response = f.get(60, TimeUnit.SECONDS); | 
|  | 68 | +            assertNotNull(response); | 
|  | 69 | +            assertEquals(200, response.getStatusCode()); | 
|  | 70 | +        } | 
|  | 71 | +    } | 
|  | 72 | + | 
|  | 73 | +    @RepeatedIfExceptionsTest(repeats = 5) | 
|  | 74 | +    public void testSocks5ProxyWithHttp() throws Exception { | 
|  | 75 | +        // Start SOCKS proxy in background thread | 
|  | 76 | +        Thread socksProxyThread = new Thread(() -> { | 
|  | 77 | +            try { | 
|  | 78 | +                new SocksProxy(60000); | 
|  | 79 | +            } catch (Exception e) { | 
|  | 80 | +                logger.error("Failed to establish SocksProxy", e); | 
|  | 81 | +            } | 
|  | 82 | +        }); | 
|  | 83 | +        socksProxyThread.start(); | 
|  | 84 | + | 
|  | 85 | +        // Give the proxy time to start | 
|  | 86 | +        Thread.sleep(1000); | 
|  | 87 | + | 
|  | 88 | +        try (AsyncHttpClient client = asyncHttpClient()) { | 
|  | 89 | +            String target = "http://localhost:" + port1 + '/'; | 
|  | 90 | +            Future<Response> f = client.prepareGet(target) | 
|  | 91 | +                    .setProxyServer(new ProxyServer.Builder("localhost", 8000).setProxyType(ProxyType.SOCKS_V5)) | 
|  | 92 | +                    .execute(); | 
|  | 93 | + | 
|  | 94 | +            Response response = f.get(60, TimeUnit.SECONDS); | 
|  | 95 | +            assertNotNull(response); | 
|  | 96 | +            assertEquals(200, response.getStatusCode()); | 
|  | 97 | +        } | 
|  | 98 | +    } | 
|  | 99 | + | 
|  | 100 | +    @Test | 
|  | 101 | +    public void testSocks5ProxyWithHttpsDoesNotThrowException() throws Exception { | 
|  | 102 | +        // This test specifically verifies that HTTPS requests through SOCKS5 proxy | 
|  | 103 | +        // do not throw NoSuchElementException: socks anymore | 
|  | 104 | + | 
|  | 105 | +        // Start SOCKS proxy in background thread | 
|  | 106 | +        Thread socksProxyThread = new Thread(() -> { | 
|  | 107 | +            try { | 
|  | 108 | +                new SocksProxy(10000); // shorter time for test | 
|  | 109 | +            } catch (Exception e) { | 
|  | 110 | +                logger.error("Failed to establish SocksProxy", e); | 
|  | 111 | +            } | 
|  | 112 | +        }); | 
|  | 113 | +        socksProxyThread.start(); | 
|  | 114 | + | 
|  | 115 | +        // Give the proxy time to start | 
|  | 116 | +        Thread.sleep(1000); | 
|  | 117 | + | 
|  | 118 | +        try (AsyncHttpClient client = asyncHttpClient(config() | 
|  | 119 | +                .setProxyServer(new ProxyServer.Builder("localhost", 8000).setProxyType(ProxyType.SOCKS_V5)) | 
|  | 120 | +                .setConnectTimeout(Duration.ofMillis(5000)) | 
|  | 121 | +                .setRequestTimeout(Duration.ofMillis(10000)))) { | 
|  | 122 | + | 
|  | 123 | +            // This would previously throw: java.util.NoSuchElementException: socks | 
|  | 124 | +            // We expect this to fail with connection timeout (since we don't have a real HTTPS target) | 
|  | 125 | +            // but NOT with NoSuchElementException | 
|  | 126 | + | 
|  | 127 | +            try { | 
|  | 128 | +                Future<Response> f = client.prepareGet("https://httpbin.org/get").execute(); | 
|  | 129 | +                f.get(8, TimeUnit.SECONDS); | 
|  | 130 | +                // If we reach here, great! The SOCKS proxy worked | 
|  | 131 | +            } catch (Exception e) { | 
|  | 132 | +                // We should NOT see NoSuchElementException: socks anymore | 
|  | 133 | +                String message = e.getMessage(); | 
|  | 134 | +                if (message != null && message.contains("socks") && message.contains("NoSuchElementException")) { | 
|  | 135 | +                    throw new AssertionError("NoSuchElementException: socks still occurs", e); | 
|  | 136 | +                } | 
|  | 137 | +                // Other exceptions like connection timeout are expected since we don't have a real working SOCKS proxy setup | 
|  | 138 | +                logger.info("Expected exception (not the SOCKS handler bug): " + e.getClass().getSimpleName() + ": " + message); | 
|  | 139 | +            } | 
|  | 140 | +        } | 
|  | 141 | +    } | 
|  | 142 | + | 
|  | 143 | +    @Test | 
|  | 144 | +    public void testSocks4ProxyWithHttpsDoesNotThrowException() throws Exception { | 
|  | 145 | +        // This test specifically verifies that HTTPS requests through SOCKS4 proxy | 
|  | 146 | +        // do not throw NoSuchElementException: socks anymore | 
|  | 147 | + | 
|  | 148 | +        // Start SOCKS proxy in background thread | 
|  | 149 | +        Thread socksProxyThread = new Thread(() -> { | 
|  | 150 | +            try { | 
|  | 151 | +                new SocksProxy(10000); // shorter time for test | 
|  | 152 | +            } catch (Exception e) { | 
|  | 153 | +                logger.error("Failed to establish SocksProxy", e); | 
|  | 154 | +            } | 
|  | 155 | +        }); | 
|  | 156 | +        socksProxyThread.start(); | 
|  | 157 | + | 
|  | 158 | +        // Give the proxy time to start | 
|  | 159 | +        Thread.sleep(1000); | 
|  | 160 | + | 
|  | 161 | +        try (AsyncHttpClient client = asyncHttpClient(config() | 
|  | 162 | +                .setProxyServer(new ProxyServer.Builder("localhost", 8000).setProxyType(ProxyType.SOCKS_V4)) | 
|  | 163 | +                .setConnectTimeout(Duration.ofMillis(5000)) | 
|  | 164 | +                .setRequestTimeout(Duration.ofMillis(10000)))) { | 
|  | 165 | + | 
|  | 166 | +            // This would previously throw: java.util.NoSuchElementException: socks | 
|  | 167 | +            // We expect this to fail with connection timeout (since we don't have a real HTTPS target) | 
|  | 168 | +            // but NOT with NoSuchElementException | 
|  | 169 | + | 
|  | 170 | +            try { | 
|  | 171 | +                Future<Response> f = client.prepareGet("https://httpbin.org/get").execute(); | 
|  | 172 | +                f.get(8, TimeUnit.SECONDS); | 
|  | 173 | +                // If we reach here, great! The SOCKS proxy worked | 
|  | 174 | +            } catch (Exception e) { | 
|  | 175 | +                // We should NOT see NoSuchElementException: socks anymore | 
|  | 176 | +                String message = e.getMessage(); | 
|  | 177 | +                if (message != null && message.contains("socks") && message.contains("NoSuchElementException")) { | 
|  | 178 | +                    throw new AssertionError("NoSuchElementException: socks still occurs", e); | 
|  | 179 | +                } | 
|  | 180 | +                // Other exceptions like connection timeout are expected since we don't have a real working SOCKS proxy setup | 
|  | 181 | +                logger.info("Expected exception (not the SOCKS handler bug): " + e.getClass().getSimpleName() + ": " + message); | 
|  | 182 | +            } | 
|  | 183 | +        } | 
|  | 184 | +    } | 
|  | 185 | + | 
|  | 186 | +    @Test | 
|  | 187 | +    public void testIssue1913NoSuchElementExceptionSocks5() throws Exception { | 
|  | 188 | +        // Reproduces the exact issue from GitHub issue #1913 with SOCKS5 | 
|  | 189 | +        // This uses the exact code pattern from the issue report | 
|  | 190 | +        var proxyServer = new ProxyServer.Builder("127.0.0.1", 1081) | 
|  | 191 | +                .setProxyType(ProxyType.SOCKS_V5); | 
|  | 192 | + | 
|  | 193 | +        try (var client = asyncHttpClient(config() | 
|  | 194 | +                .setProxyServer(proxyServer.build()) | 
|  | 195 | +                .setConnectTimeout(Duration.ofMillis(2000)) | 
|  | 196 | +                .setRequestTimeout(Duration.ofMillis(5000)))) { | 
|  | 197 | + | 
|  | 198 | +            // This would previously throw: java.util.NoSuchElementException: socks | 
|  | 199 | +            // We expect this to fail with connection timeout (since proxy doesn't exist) | 
|  | 200 | +            // but NOT with NoSuchElementException | 
|  | 201 | + | 
|  | 202 | +            try { | 
|  | 203 | +                var response = client.prepareGet("https://cloudflare.com/cdn-cgi/trace").execute().get(); | 
|  | 204 | +                // If we reach here, great! The fix worked and proxy connection succeeded | 
|  | 205 | +                logger.info("Connection successful: " + response.getStatusCode()); | 
|  | 206 | +            } catch (Exception e) { | 
|  | 207 | +                // Check that we don't get the NoSuchElementException: socks anymore | 
|  | 208 | +                Throwable cause = e.getCause(); | 
|  | 209 | +                String message = cause != null ? cause.getMessage() : e.getMessage(); | 
|  | 210 | + | 
|  | 211 | +                // This should NOT contain the original error | 
|  | 212 | +                if (message != null && message.contains("socks") &&  | 
|  | 213 | +                    (e.toString().contains("NoSuchElementException") || cause != null && cause.toString().contains("NoSuchElementException"))) { | 
|  | 214 | +                    throw new AssertionError("NoSuchElementException: socks still occurs - fix didn't work: " + e.toString()); | 
|  | 215 | +                } | 
|  | 216 | + | 
|  | 217 | +                // Other exceptions like connection timeout are expected since we don't have a working SOCKS proxy | 
|  | 218 | +                logger.info("Expected exception (not the SOCKS handler bug): " + e.getClass().getSimpleName() + ": " + message); | 
|  | 219 | +            } | 
|  | 220 | +        } | 
|  | 221 | +    } | 
|  | 222 | + | 
|  | 223 | +    @Test  | 
|  | 224 | +    public void testIssue1913NoSuchElementExceptionSocks4() throws Exception { | 
|  | 225 | +        // Reproduces the exact issue from GitHub issue #1913 with SOCKS4 | 
|  | 226 | +        // This uses the exact code pattern from the issue report | 
|  | 227 | +        var proxyServer = new ProxyServer.Builder("127.0.0.1", 1081) | 
|  | 228 | +                .setProxyType(ProxyType.SOCKS_V4); | 
|  | 229 | + | 
|  | 230 | +        try (var client = asyncHttpClient(config() | 
|  | 231 | +                .setProxyServer(proxyServer.build()) | 
|  | 232 | +                .setConnectTimeout(Duration.ofMillis(2000)) | 
|  | 233 | +                .setRequestTimeout(Duration.ofMillis(5000)))) { | 
|  | 234 | + | 
|  | 235 | +            try { | 
|  | 236 | +                var response = client.prepareGet("https://cloudflare.com/cdn-cgi/trace").execute().get(); | 
|  | 237 | +                logger.info("Connection successful: " + response.getStatusCode()); | 
|  | 238 | +            } catch (Exception e) { | 
|  | 239 | +                // Check that we don't get the NoSuchElementException: socks anymore | 
|  | 240 | +                Throwable cause = e.getCause(); | 
|  | 241 | +                String message = cause != null ? cause.getMessage() : e.getMessage(); | 
|  | 242 | + | 
|  | 243 | +                if (message != null && message.contains("socks") &&  | 
|  | 244 | +                    (e.toString().contains("NoSuchElementException") || cause != null && cause.toString().contains("NoSuchElementException"))) { | 
|  | 245 | +                    throw new AssertionError("NoSuchElementException: socks still occurs - fix didn't work: " + e.toString()); | 
|  | 246 | +                } | 
|  | 247 | + | 
|  | 248 | +                logger.info("Expected exception (not the SOCKS handler bug): " + e.getClass().getSimpleName() + ": " + message); | 
|  | 249 | +            } | 
|  | 250 | +        } | 
|  | 251 | +    } | 
|  | 252 | +} | 
0 commit comments