Skip to content

Commit f194152

Browse files
authored
fix: inappropriate connection reuse when using HTTP proxy if the initial CONNECT failed (#2072)
# What This MR Resolves A CONNECT request is needed to sent to the HTTP proxy first before the actual client request to establish the tunnel on the proxy. A `HTTP/1.1 200 Connection established` is expected for the initial CONNECT request. Only when the CONNECT is successful, the client continues sending the actual request through the "tunnel". And when CONNECT failed, the connection remains the initial state `unconnected`. There are following circumstances that a CONNECT fails under but not limited to following situations: - The destination is not whitelisted. - The dest domain can't be resolved(timeout/SERVFAIL/NX/etc.). - The dest IP can't be connected(timeout/unreachable/etc.). There could be 2 following strategies to deal with CONNECT failures on the client side: 1. Close the connection before return to the caller. 2. Mark this connection "unconnected" and put it into the pool. Then retry the CONNECT next time it's picked out of the pool. The 2nd one needs to add extra state to Channel in the manager which brings bigger change to the code. This MR employs the 1st strategy to resolve it. The issue is described in #2071 . # Readings The CONNECT is documented in `Section 5.3` in RFC2871: https://www.ietf.org/rfc/rfc2817.txt The proxy won't actively terminate the connection if the CONNECT failed if keep-alive is enabled. Unless the tunnel is established and there is any communication failures in the middle. Therefore the client needs to deal with this error by its own. Signed-off-by: Jason Joo <[email protected]>
1 parent 600520c commit f194152

File tree

2 files changed

+52
-4
lines changed

2 files changed

+52
-4
lines changed

client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.netty.handler.codec.DecoderResultProvider;
2222
import io.netty.handler.codec.http.HttpContent;
2323
import io.netty.handler.codec.http.HttpHeaders;
24+
import io.netty.handler.codec.http.HttpMethod;
2425
import io.netty.handler.codec.http.HttpRequest;
2526
import io.netty.handler.codec.http.HttpResponse;
2627
import io.netty.handler.codec.http.LastHttpContent;
@@ -32,6 +33,7 @@
3233
import org.asynchttpclient.netty.NettyResponseStatus;
3334
import org.asynchttpclient.netty.channel.ChannelManager;
3435
import org.asynchttpclient.netty.request.NettyRequestSender;
36+
import org.asynchttpclient.util.HttpConstants.ResponseStatusCodes;
3537

3638
import java.io.IOException;
3739
import java.net.InetSocketAddress;
@@ -43,8 +45,11 @@ public HttpHandler(AsyncHttpClientConfig config, ChannelManager channelManager,
4345
super(config, channelManager, requestSender);
4446
}
4547

46-
private static boolean abortAfterHandlingStatus(AsyncHandler<?> handler, NettyResponseStatus status) throws Exception {
47-
return handler.onStatusReceived(status) == State.ABORT;
48+
private static boolean abortAfterHandlingStatus(AsyncHandler<?> handler, HttpMethod httpMethod, NettyResponseStatus status) throws Exception {
49+
// For non-200 response of a CONNECT request, it's still unconnected.
50+
// We need to either close the connection or reuse it but send CONNECT request again.
51+
// The former one is easier or we have to attach more state to Channel.
52+
return handler.onStatusReceived(status) == State.ABORT || httpMethod == HttpMethod.CONNECT && status.getStatusCode() != ResponseStatusCodes.OK_200;
4853
}
4954

5055
private static boolean abortAfterHandlingHeaders(AsyncHandler<?> handler, HttpHeaders responseHeaders) throws Exception {
@@ -61,7 +66,7 @@ private void handleHttpResponse(final HttpResponse response, final Channel chann
6166
HttpHeaders responseHeaders = response.headers();
6267

6368
if (!interceptors.exitAfterIntercept(channel, future, handler, response, status, responseHeaders)) {
64-
boolean abort = abortAfterHandlingStatus(handler, status) || abortAfterHandlingHeaders(handler, responseHeaders);
69+
boolean abort = abortAfterHandlingStatus(handler, httpRequest.method(), status) || abortAfterHandlingHeaders(handler, responseHeaders);
6570
if (abort) {
6671
finishUpdate(future, channel, true);
6772
}

client/src/test/java/org/asynchttpclient/proxy/HttpsProxyTest.java

+44-1
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,21 @@
1313
package org.asynchttpclient.proxy;
1414

1515
import io.github.artsok.RepeatedIfExceptionsTest;
16+
import jakarta.servlet.ServletException;
17+
import jakarta.servlet.http.HttpServletRequest;
18+
import jakarta.servlet.http.HttpServletResponse;
19+
1620
import org.asynchttpclient.AbstractBasicTest;
1721
import org.asynchttpclient.AsyncHttpClient;
1822
import org.asynchttpclient.AsyncHttpClientConfig;
1923
import org.asynchttpclient.RequestBuilder;
2024
import org.asynchttpclient.Response;
25+
import org.asynchttpclient.proxy.ProxyServer.Builder;
2126
import org.asynchttpclient.request.body.generator.ByteArrayBodyGenerator;
2227
import org.asynchttpclient.test.EchoHandler;
28+
import org.asynchttpclient.util.HttpConstants;
2329
import org.eclipse.jetty.proxy.ConnectHandler;
30+
import org.eclipse.jetty.server.Request;
2431
import org.eclipse.jetty.server.Server;
2532
import org.eclipse.jetty.server.ServerConnector;
2633
import org.eclipse.jetty.server.handler.AbstractHandler;
@@ -37,6 +44,8 @@
3744
import static org.asynchttpclient.test.TestUtils.addHttpsConnector;
3845
import static org.junit.jupiter.api.Assertions.assertEquals;
3946

47+
import java.io.IOException;
48+
4049
/**
4150
* Proxy usage tests.
4251
*/
@@ -46,7 +55,7 @@ public class HttpsProxyTest extends AbstractBasicTest {
4655

4756
@Override
4857
public AbstractHandler configureHandler() throws Exception {
49-
return new ConnectHandler();
58+
return new ProxyHandler();
5059
}
5160

5261
@Override
@@ -142,4 +151,38 @@ public void testPooledConnectionsWithProxy() throws Exception {
142151
assertEquals(200, response2.getStatusCode());
143152
}
144153
}
154+
155+
@RepeatedIfExceptionsTest(repeats = 5)
156+
public void testFailedConnectWithProxy() throws Exception {
157+
try (AsyncHttpClient asyncHttpClient = asyncHttpClient(config().setFollowRedirect(true).setUseInsecureTrustManager(true).setKeepAlive(true))) {
158+
Builder proxyServer = proxyServer("localhost", port1);
159+
proxyServer.setCustomHeaders(r -> r.getHeaders().add(ProxyHandler.HEADER_FORBIDDEN, "1"));
160+
RequestBuilder rb = get(getTargetUrl2()).setProxyServer(proxyServer);
161+
162+
Response response1 = asyncHttpClient.executeRequest(rb.build()).get();
163+
assertEquals(403, response1.getStatusCode());
164+
165+
Response response2 = asyncHttpClient.executeRequest(rb.build()).get();
166+
assertEquals(403, response2.getStatusCode());
167+
168+
Response response3 = asyncHttpClient.executeRequest(rb.build()).get();
169+
assertEquals(403, response3.getStatusCode());
170+
}
171+
}
172+
173+
public static class ProxyHandler extends ConnectHandler {
174+
final static String HEADER_FORBIDDEN = "X-REJECT-REQUEST";
175+
176+
@Override
177+
public void handle(String s, Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
178+
if (HttpConstants.Methods.CONNECT.equalsIgnoreCase(request.getMethod())) {
179+
if (request.getHeader(HEADER_FORBIDDEN) != null) {
180+
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
181+
r.setHandled(true);
182+
return;
183+
}
184+
}
185+
super.handle(s, r, request, response);
186+
}
187+
}
145188
}

0 commit comments

Comments
 (0)