HTTPCLIENT-2326: Propagate original proxy response to the caller in case of HTTP CONNECT request failure

This commit is contained in:
Oleg Kalnichevski 2024-04-18 12:09:13 +02:00
parent def10b4c77
commit 215571a0bd
5 changed files with 54 additions and 26 deletions

View File

@ -28,25 +28,53 @@
package org.apache.hc.client5.http.impl;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.message.BasicHttpResponse;
import org.apache.hc.core5.http.message.StatusLine;
import org.conscrypt.Internal;
/**
* Signals that the tunnel request was rejected by the proxy host.
*
* @since 4.0
*
* @deprecated Do not use/
*/
@Deprecated
public class TunnelRefusedException extends HttpException {
private static final long serialVersionUID = -8646722842745617323L;
private final String responseMessage;
private final HttpResponse response;
/**
* @deprecated Do not use.
*/
@Deprecated
public TunnelRefusedException(final String message, final String responseMessage) {
super(message);
this.responseMessage = responseMessage;
this.response = new BasicHttpResponse(500);
}
@Internal
public TunnelRefusedException(final HttpResponse response) {
super("CONNECT refused by proxy: " + new StatusLine(response));
this.response = response;
}
/**
* @deprecated Use {@link #getResponse()}.
*/
@Deprecated
public String getResponseMessage() {
return this.responseMessage;
return "CONNECT refused by proxy: " + new StatusLine(response);
}
/**
* @since 5.4
*/
public HttpResponse getResponse() {
return response;
}
}

View File

@ -45,7 +45,6 @@ import org.apache.hc.client5.http.async.AsyncExecRuntime;
import org.apache.hc.client5.http.auth.AuthExchange;
import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.TunnelRefusedException;
import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
import org.apache.hc.client5.http.impl.routing.BasicRouteDirector;
@ -121,8 +120,8 @@ public final class AsyncConnectExec implements AsyncExecChainHandler {
final RouteTracker tracker;
volatile boolean challenged;
volatile HttpResponse response;
volatile boolean tunnelRefused;
volatile HttpResponse tunnelResponse;
}
@ -295,7 +294,7 @@ public final class AsyncConnectExec implements AsyncExecChainHandler {
if (LOG.isDebugEnabled()) {
LOG.debug("{} tunnel refused", exchangeId);
}
asyncExecCallback.failed(new TunnelRefusedException("CONNECT refused by proxy: " + new StatusLine(state.tunnelResponse), null));
asyncExecCallback.completed();
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("{} tunnel to target created", exchangeId);
@ -456,8 +455,7 @@ public final class AsyncConnectExec implements AsyncExecChainHandler {
state.challenged = false;
if (status >= HttpStatus.SC_REDIRECTION) {
state.tunnelRefused = true;
state.tunnelResponse = response;
entityConsumerRef.set(null);
entityConsumerRef.set(asyncExecCallback.handleResponse(response, entityDetails));
} else if (status == HttpStatus.SC_OK) {
asyncExecCallback.completed();
} else {

View File

@ -40,7 +40,6 @@ import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChainHandler;
import org.apache.hc.client5.http.classic.ExecRuntime;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.TunnelRefusedException;
import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
import org.apache.hc.client5.http.impl.routing.BasicRouteDirector;
@ -52,6 +51,7 @@ import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ConnectionReuseStrategy;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
@ -60,6 +60,7 @@ import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.message.StatusLine;
@ -149,11 +150,15 @@ public final class ConnectExec implements ExecChainHandler {
tracker.connectProxy(proxy, route.isSecure() && !route.isTunnelled());
break;
case HttpRouteDirector.TUNNEL_TARGET: {
final boolean secure = createTunnelToTarget(exchangeId, route, request, execRuntime, context);
final ClassicHttpResponse finalResponse = createTunnelToTarget(
exchangeId, route, request, execRuntime, context);
if (finalResponse != null) {
return finalResponse;
}
if (LOG.isDebugEnabled()) {
LOG.debug("{} tunnel to target created.", exchangeId);
}
tracker.tunnelTarget(secure);
tracker.tunnelTarget(false);
} break;
case HttpRouteDirector.TUNNEL_PROXY: {
@ -207,7 +212,7 @@ public final class ConnectExec implements ExecChainHandler {
* This method does <i>not</i> processChallenge the connection with
* information about the tunnel, that is left to the caller.
*/
private boolean createTunnelToTarget(
private ClassicHttpResponse createTunnelToTarget(
final String exchangeId,
final HttpRoute route,
final HttpRequest request,
@ -282,16 +287,16 @@ public final class ConnectExec implements ExecChainHandler {
final int status = response.getCode();
if (status != HttpStatus.SC_OK) {
EntityUtils.consume(response.getEntity());
execRuntime.disconnectEndpoint();
throw new TunnelRefusedException("CONNECT refused by proxy: " + new StatusLine(response), null);
final HttpEntity entity = response.getEntity();
if (entity != null) {
response.setEntity(new ByteArrayEntity(
EntityUtils.toByteArray(entity, 4096),
ContentType.parseLenient(entity.getContentType())));
execRuntime.disconnectEndpoint();
}
return response;
}
// How to decide on security of the tunnelled connection?
// The socket factory knows only about the segment to the proxy.
// Even if that is secure, the hop to the target may be insecure.
// Leave it to derived classes, consider insecure by default here.
return false;
return null;
}
/**

View File

@ -43,7 +43,6 @@ import org.apache.hc.client5.http.auth.StandardAuthScheme;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
import org.apache.hc.client5.http.impl.DefaultClientConnectionReuseStrategy;
import org.apache.hc.client5.http.impl.TunnelRefusedException;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory;
@ -202,7 +201,7 @@ public class ProxyClient {
if (status > 299) {
EntityUtils.consume(response.getEntity());
conn.close();
throw new TunnelRefusedException("CONNECT refused by proxy: " + new StatusLine(response), null);
throw new HttpException("Tunnel refused: " + new StatusLine(response));
}
return conn.getSocket();
}

View File

@ -41,7 +41,6 @@ import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecRuntime;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.impl.TunnelRefusedException;
import org.apache.hc.client5.http.impl.auth.BasicScheme;
import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
@ -218,9 +217,8 @@ public class TestConnectExec {
Mockito.any())).thenReturn(response);
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
Assertions.assertThrows(TunnelRefusedException.class, () -> exec.execute(request, scope, execChain));
exec.execute(request, scope, execChain);
Mockito.verify(execRuntime, Mockito.atLeastOnce()).disconnectEndpoint();
Mockito.verify(execRuntime).discardEndpoint();
}
@Test