Skip to content

Commit ec6815d

Browse files
committed
fix: send CONNECT first when recovering a HTTPS request
Signed-off-by: Jason Joo <[email protected]>
1 parent 8189c92 commit ec6815d

File tree

2 files changed

+50
-9
lines changed

2 files changed

+50
-9
lines changed

client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java

+22-7
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ public NettyRequestSender(AsyncHttpClientConfig config, ChannelManager channelMa
9797
requestFactory = new NettyRequestFactory(config);
9898
}
9999

100+
// needConnect returns true if the request is secure/websocket and a HTTP proxy is set
101+
private boolean needConnect(final Request request, final ProxyServer proxyServer) {
102+
return proxyServer != null
103+
&& proxyServer.getProxyType().isHttp()
104+
&& (request.getUri().isSecured() || request.getUri().isWebSocket());
105+
}
106+
100107
public <T> ListenableFuture<T> sendRequest(final Request request, final AsyncHandler<T> asyncHandler, NettyResponseFuture<T> future) {
101108
if (isClosed()) {
102109
throw new IllegalStateException("Closed");
@@ -106,9 +113,7 @@ public <T> ListenableFuture<T> sendRequest(final Request request, final AsyncHan
106113
ProxyServer proxyServer = getProxyServer(config, request);
107114

108115
// WebSockets use connect tunneling to work with proxies
109-
if (proxyServer != null && proxyServer.getProxyType().isHttp() &&
110-
(request.getUri().isSecured() || request.getUri().isWebSocket()) &&
111-
!isConnectAlreadyDone(request, future)) {
116+
if (needConnect(request, proxyServer) && !isConnectAlreadyDone(request, future)) {
112117
// Proxy with HTTPS or WebSocket: CONNECT for sure
113118
if (future != null && future.isConnectAllowed()) {
114119
// Perform CONNECT
@@ -125,6 +130,8 @@ public <T> ListenableFuture<T> sendRequest(final Request request, final AsyncHan
125130

126131
private static boolean isConnectAlreadyDone(Request request, NettyResponseFuture<?> future) {
127132
return future != null
133+
// If the channel can't be reused or closed, a CONNECT is still required
134+
&& future.isReuseChannel() && Channels.isChannelActive(future.channel())
128135
&& future.getNettyRequest() != null
129136
&& future.getNettyRequest().getHttpRequest().method() == HttpMethod.CONNECT
130137
&& !request.getMethod().equals(CONNECT);
@@ -137,11 +144,19 @@ private static boolean isConnectAlreadyDone(Request request, NettyResponseFuture
137144
*/
138145
private <T> ListenableFuture<T> sendRequestWithCertainForceConnect(Request request, AsyncHandler<T> asyncHandler, NettyResponseFuture<T> future,
139146
ProxyServer proxyServer, boolean performConnectRequest) {
140-
NettyResponseFuture<T> newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future, proxyServer, performConnectRequest);
141147
Channel channel = getOpenChannel(future, request, proxyServer, asyncHandler);
142-
return Channels.isChannelActive(channel)
143-
? sendRequestWithOpenChannel(newFuture, asyncHandler, channel)
144-
: sendRequestWithNewChannel(request, proxyServer, newFuture, asyncHandler);
148+
if (Channels.isChannelActive(channel)) {
149+
NettyResponseFuture<T> newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future,
150+
proxyServer, performConnectRequest);
151+
return sendRequestWithOpenChannel(newFuture, asyncHandler, channel);
152+
} else {
153+
// A new channel is not expected when performConnectRequest is false. We need to
154+
// revisit the condition of sending
155+
// the CONNECT request to the new channel.
156+
NettyResponseFuture<T> newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future,
157+
proxyServer, needConnect(request, proxyServer));
158+
return sendRequestWithNewChannel(request, proxyServer, newFuture, asyncHandler);
159+
}
145160
}
146161

147162
/**

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

+28-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package org.asynchttpclient.proxy;
1414

1515
import io.github.artsok.RepeatedIfExceptionsTest;
16+
import io.netty.handler.codec.http.DefaultHttpHeaders;
1617
import jakarta.servlet.ServletException;
1718
import jakarta.servlet.http.HttpServletRequest;
1819
import jakarta.servlet.http.HttpServletResponse;
@@ -43,8 +44,10 @@
4344
import static org.asynchttpclient.test.TestUtils.addHttpConnector;
4445
import static org.asynchttpclient.test.TestUtils.addHttpsConnector;
4546
import static org.junit.jupiter.api.Assertions.assertEquals;
47+
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
4648

4749
import java.io.IOException;
50+
import java.util.concurrent.ExecutionException;
4851

4952
/**
5053
* Proxy usage tests.
@@ -156,7 +159,7 @@ public void testPooledConnectionsWithProxy() throws Exception {
156159
public void testFailedConnectWithProxy() throws Exception {
157160
try (AsyncHttpClient asyncHttpClient = asyncHttpClient(config().setFollowRedirect(true).setUseInsecureTrustManager(true).setKeepAlive(true))) {
158161
Builder proxyServer = proxyServer("localhost", port1);
159-
proxyServer.setCustomHeaders(r -> r.getHeaders().add(ProxyHandler.HEADER_FORBIDDEN, "1"));
162+
proxyServer.setCustomHeaders(r -> new DefaultHttpHeaders().set(ProxyHandler.HEADER_FORBIDDEN, "1"));
160163
RequestBuilder rb = get(getTargetUrl2()).setProxyServer(proxyServer);
161164

162165
Response response1 = asyncHttpClient.executeRequest(rb.build()).get();
@@ -170,16 +173,39 @@ public void testFailedConnectWithProxy() throws Exception {
170173
}
171174
}
172175

176+
@RepeatedIfExceptionsTest(repeats = 5)
177+
public void testClosedConnectionWithProxy() throws Exception {
178+
try (AsyncHttpClient asyncHttpClient = asyncHttpClient(
179+
config().setFollowRedirect(true).setUseInsecureTrustManager(true).setKeepAlive(true))) {
180+
Builder proxyServer = proxyServer("localhost", port1);
181+
proxyServer.setCustomHeaders(r -> new DefaultHttpHeaders().set(ProxyHandler.HEADER_FORBIDDEN, "2"));
182+
RequestBuilder rb = get(getTargetUrl2()).setProxyServer(proxyServer);
183+
184+
assertThrowsExactly(ExecutionException.class, () -> asyncHttpClient.executeRequest(rb.build()).get());
185+
assertThrowsExactly(ExecutionException.class, () -> asyncHttpClient.executeRequest(rb.build()).get());
186+
assertThrowsExactly(ExecutionException.class, () -> asyncHttpClient.executeRequest(rb.build()).get());
187+
}
188+
}
189+
173190
public static class ProxyHandler extends ConnectHandler {
174191
final static String HEADER_FORBIDDEN = "X-REJECT-REQUEST";
175192

176193
@Override
177194
public void handle(String s, Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
178195
if (HttpConstants.Methods.CONNECT.equalsIgnoreCase(request.getMethod())) {
179-
if (request.getHeader(HEADER_FORBIDDEN) != null) {
196+
String headerValue = request.getHeader(HEADER_FORBIDDEN);
197+
if (headerValue == null) {
198+
headerValue = "";
199+
}
200+
switch (headerValue) {
201+
case "1":
180202
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
181203
r.setHandled(true);
182204
return;
205+
case "2":
206+
r.getHttpChannel().getConnection().close();
207+
r.setHandled(true);
208+
return;
183209
}
184210
}
185211
super.handle(s, r, request, response);

0 commit comments

Comments
 (0)