Fixes #10160 - Verify PROXY_AUTHENTICATION is sent to forward proxies (#10162)

Now TunnelRequest.getURI() does not return null, so normalizeRequest() can properly apply the authentication headers.

Moved copy of a request to HttpRequest, so also the sub-type can be copied.
Fixed restore of destination in HttpProxy.HttpProxyClientConnectionFactory.newProxyConnection(): now doing it in the promise rather than in finally block.
Using the proxy destination (not the server's) to send subsequent CONNECT requests in case the first is not replied with 200.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2023-07-31 18:39:13 +02:00 committed by GitHub
parent afef05a413
commit b2477d1c38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 311 additions and 147 deletions

View File

@ -33,6 +33,7 @@ import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.QuotedCSV;
import org.eclipse.jetty.util.NanoTime;
@ -205,14 +206,9 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler
conversation.setAttribute(authenticationAttribute, true);
URI requestURI = request.getURI();
String path = null;
if (requestURI == null)
{
requestURI = resolveURI(request, null);
path = request.getPath();
}
Request newRequest = client.copyRequest(request, requestURI);
Request newRequest = client.copyRequest(request, request.getURI());
if (HttpMethod.CONNECT.is(newRequest.getMethod()))
newRequest.path(request.getPath());
// Adjust the timeout of the new request, taking into account the
// timeout of the previous request and the time already elapsed.
@ -232,9 +228,6 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler
}
}
if (path != null)
newRequest.path(path);
authnResult.apply(newRequest);
// Copy existing, explicitly set, authorization headers.
copyIfAbsent(request, newRequest, HttpHeader.AUTHORIZATION);

View File

@ -37,7 +37,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.client.api.AuthenticationStore;
@ -467,59 +466,12 @@ public class HttpClient extends ContainerLifeCycle
protected Request copyRequest(HttpRequest oldRequest, URI newURI)
{
HttpRequest newRequest = newHttpRequest(oldRequest.getConversation(), newURI);
newRequest.method(oldRequest.getMethod())
.version(oldRequest.getVersion())
.body(oldRequest.getBody())
.idleTimeout(oldRequest.getIdleTimeout(), TimeUnit.MILLISECONDS)
.timeout(oldRequest.getTimeout(), TimeUnit.MILLISECONDS)
.followRedirects(oldRequest.isFollowRedirects())
.tag(oldRequest.getTag());
for (HttpField field : oldRequest.getHeaders())
{
HttpHeader header = field.getHeader();
// We have a new URI, so skip the host header if present.
if (HttpHeader.HOST == header)
continue;
// Remove expectation headers.
if (HttpHeader.EXPECT == header)
continue;
// Remove cookies.
if (HttpHeader.COOKIE == header)
continue;
// Remove authorization headers.
if (HttpHeader.AUTHORIZATION == header ||
HttpHeader.PROXY_AUTHORIZATION == header)
continue;
if (!newRequest.getHeaders().contains(field))
newRequest.addHeader(field);
}
return newRequest;
return oldRequest.copy(newURI);
}
protected HttpRequest newHttpRequest(HttpConversation conversation, URI uri)
{
return new HttpRequest(this, conversation, checkHost(uri));
}
/**
* <p>Checks {@code uri} for the host to be non-null host.</p>
* <p>URIs built from strings that have an internationalized domain name (IDN)
* are parsed without errors, but {@code uri.getHost()} returns null.</p>
*
* @param uri the URI to check for non-null host
* @return the same {@code uri} if the host is non-null
* @throws IllegalArgumentException if the host is null
*/
private URI checkHost(URI uri)
{
if (uri.getHost() == null)
throw new IllegalArgumentException(String.format("Invalid URI host: null (authority: %s)", uri.getRawAuthority()));
return uri;
return new HttpRequest(this, conversation, uri);
}
public Destination resolveDestination(Request request)

View File

@ -29,7 +29,6 @@ import org.eclipse.jetty.client.util.BytesRequestContent;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.CyclicTimeouts;
import org.eclipse.jetty.util.Attachable;
import org.eclipse.jetty.util.HttpCookieStore;
@ -151,21 +150,32 @@ public abstract class HttpConnection implements IConnection, Attachable
request.path(path);
}
boolean http1 = request.getVersion().getVersion() <= 11;
boolean applyProxyAuthentication = false;
ProxyConfiguration.Proxy proxy = destination.getProxy();
if (proxy instanceof HttpProxy && !HttpClient.isSchemeSecure(request.getScheme()))
if (proxy instanceof HttpProxy)
{
URI uri = request.getURI();
if (uri != null)
boolean tunnelled = ((HttpProxy)proxy).requiresTunnel(destination.getOrigin());
// RFC 9112, section 3.2.2: when making a request to a proxy other than CONNECT,
// the client must send the target URI in absolute-form as the request target.
// In practice, this is only valid for HTTP/1.1 requests that are not tunnelled.
if (http1 && !tunnelled)
{
path = uri.toString();
request.path(path);
URI uri = request.getURI();
if (uri != null)
request.path(uri.toString());
}
// Do not send proxy authentication headers when tunnelled,
// otherwise proxy credentials arrive to the server.
applyProxyAuthentication = !tunnelled;
}
// If we are HTTP 1.1, add the Host header
HttpVersion version = request.getVersion();
// If we are HTTP 1.1, add the Host header.
HttpFields headers = request.getHeaders();
if (version.getVersion() <= 11)
if (http1)
{
if (!headers.contains(HttpHeader.HOST.asString()))
{
@ -177,7 +187,7 @@ public abstract class HttpConnection implements IConnection, Attachable
}
}
// Add content headers
// Add content headers.
Request.Content content = request.getBody();
if (content == null)
{
@ -204,7 +214,7 @@ public abstract class HttpConnection implements IConnection, Attachable
}
}
// Cookies
// Cookies.
StringBuilder cookies = convertCookies(request.getCookies(), null);
CookieStore cookieStore = getHttpClient().getCookieStore();
if (cookieStore != null && cookieStore.getClass() != HttpCookieStore.Empty.class)
@ -219,8 +229,9 @@ public abstract class HttpConnection implements IConnection, Attachable
request.addHeader(cookieField);
}
// Authentication
applyProxyAuthentication(request, proxy);
// Authentication.
if (applyProxyAuthentication)
applyProxyAuthentication(request, proxy);
applyRequestAuthentication(request);
}

View File

@ -28,7 +28,6 @@ import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
@ -85,6 +84,27 @@ public class HttpProxy extends ProxyConfiguration.Proxy
return URI.create(getOrigin().asString());
}
boolean requiresTunnel(Origin serverOrigin)
{
if (HttpClient.isSchemeSecure(serverOrigin.getScheme()))
return true;
Origin.Protocol serverProtocol = serverOrigin.getProtocol();
if (serverProtocol == null)
return true;
List<String> serverProtocols = serverProtocol.getProtocols();
return getProtocol().getProtocols().stream().noneMatch(p -> protocolMatches(p, serverProtocols));
}
private boolean protocolMatches(String protocol, List<String> protocols)
{
return protocols.stream().anyMatch(p -> protocol.equalsIgnoreCase(p) || (isHTTP2(p) && isHTTP2(protocol)));
}
private boolean isHTTP2(String protocol)
{
return "h2".equalsIgnoreCase(protocol) || "h2c".equalsIgnoreCase(protocol);
}
private class HttpProxyClientConnectionFactory implements ClientConnectionFactory
{
private final ClientConnectionFactory connectionFactory;
@ -98,60 +118,27 @@ public class HttpProxy extends ProxyConfiguration.Proxy
public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map<String, Object> context) throws IOException
{
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
Origin.Protocol serverProtocol = destination.getOrigin().getProtocol();
boolean sameProtocol = proxySpeaksServerProtocol(serverProtocol);
if (destination.isSecure() || !sameProtocol)
{
@SuppressWarnings("unchecked")
Promise<Connection> promise = (Promise<Connection>)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
Promise<Connection> wrapped = promise;
if (promise instanceof Promise.Wrapper)
wrapped = ((Promise.Wrapper<Connection>)promise).unwrap();
if (wrapped instanceof TunnelPromise)
{
// In case the server closes the tunnel (e.g. proxy authentication
// required: 407 + Connection: close), we will open another tunnel
// so we need to tell the promise about the new EndPoint.
((TunnelPromise)wrapped).setEndPoint(endPoint);
return connectionFactory.newConnection(endPoint, context);
}
else
{
return newProxyConnection(endPoint, context);
}
}
if (requiresTunnel(destination.getOrigin()))
return newProxyConnection(endPoint, context);
else
{
return connectionFactory.newConnection(endPoint, context);
}
}
private boolean proxySpeaksServerProtocol(Origin.Protocol serverProtocol)
{
return serverProtocol != null && getProtocol().getProtocols().stream().anyMatch(p -> serverProtocol.getProtocols().stream().anyMatch(p::equalsIgnoreCase));
}
private org.eclipse.jetty.io.Connection newProxyConnection(EndPoint endPoint, Map<String, Object> context) throws IOException
{
// Replace the promise with the proxy promise that creates the tunnel to the server.
@SuppressWarnings("unchecked")
Promise<Connection> promise = (Promise<Connection>)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
CreateTunnelPromise tunnelPromise = new CreateTunnelPromise(connectionFactory, endPoint, promise, context);
context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, tunnelPromise);
// Replace the destination with the proxy destination.
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
HttpClient client = destination.getHttpClient();
HttpDestination proxyDestination = client.resolveDestination(getOrigin());
context.put(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY, proxyDestination);
try
{
return connectionFactory.newConnection(endPoint, context);
}
finally
{
context.put(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY, destination);
}
// Replace the promise with the proxy promise that creates the tunnel to the server.
@SuppressWarnings("unchecked")
Promise<Connection> promise = (Promise<Connection>)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
CreateTunnelPromise tunnelPromise = new CreateTunnelPromise(connectionFactory, endPoint, destination, promise, context);
context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, tunnelPromise);
return connectionFactory.newConnection(endPoint, context);
}
}
@ -165,13 +152,15 @@ public class HttpProxy extends ProxyConfiguration.Proxy
{
private final ClientConnectionFactory connectionFactory;
private final EndPoint endPoint;
private final HttpDestination destination;
private final Promise<Connection> promise;
private final Map<String, Object> context;
private CreateTunnelPromise(ClientConnectionFactory connectionFactory, EndPoint endPoint, Promise<Connection> promise, Map<String, Object> context)
private CreateTunnelPromise(ClientConnectionFactory connectionFactory, EndPoint endPoint, HttpDestination destination, Promise<Connection> promise, Map<String, Object> context)
{
this.connectionFactory = connectionFactory;
this.endPoint = endPoint;
this.destination = destination;
this.promise = promise;
this.context = context;
}
@ -179,10 +168,11 @@ public class HttpProxy extends ProxyConfiguration.Proxy
@Override
public void succeeded(Connection connection)
{
// Replace the destination back with the original.
context.put(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY, destination);
// Replace the promise back with the original.
context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, promise);
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
tunnel(destination, connection);
tunnel(connection);
}
@Override
@ -191,24 +181,20 @@ public class HttpProxy extends ProxyConfiguration.Proxy
tunnelFailed(endPoint, x);
}
private void tunnel(HttpDestination destination, Connection connection)
private void tunnel(Connection connection)
{
String target = destination.getOrigin().getAddress().asString();
Origin.Address proxyAddress = destination.getConnectAddress();
HttpClient httpClient = destination.getHttpClient();
long connectTimeout = httpClient.getConnectTimeout();
Request connect = new TunnelRequest(httpClient, proxyAddress)
.method(HttpMethod.CONNECT)
Request connect = new TunnelRequest(httpClient, destination.getProxy().getURI())
.path(target)
.headers(headers -> headers.put(HttpHeader.HOST, target))
// Use the connect timeout as a total timeout,
// since this request is to "connect" to the server.
.timeout(connectTimeout, TimeUnit.MILLISECONDS);
ProxyConfiguration.Proxy proxy = destination.getProxy();
if (proxy.isSecure())
connect.scheme(HttpScheme.HTTPS.asString());
connect.attribute(Connection.class.getName(), new ProxyConnection(destination, connection, promise));
HttpDestination proxyDestination = httpClient.resolveDestination(destination.getProxy().getOrigin());
connect.attribute(Connection.class.getName(), new ProxyConnection(proxyDestination, connection, promise));
connection.send(connect, new TunnelListener(connect));
}
@ -217,19 +203,19 @@ public class HttpProxy extends ProxyConfiguration.Proxy
try
{
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
ClientConnectionFactory connectionFactory = this.connectionFactory;
ClientConnectionFactory factory = connectionFactory;
if (destination.isSecure())
{
// Don't want to do DNS resolution here.
InetSocketAddress address = InetSocketAddress.createUnresolved(destination.getHost(), destination.getPort());
context.put(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address);
connectionFactory = destination.newSslClientConnectionFactory(null, connectionFactory);
factory = destination.newSslClientConnectionFactory(null, factory);
}
var oldConnection = endPoint.getConnection();
var newConnection = connectionFactory.newConnection(endPoint, context);
endPoint.upgrade(newConnection);
var newConnection = factory.newConnection(endPoint, context);
if (LOG.isDebugEnabled())
LOG.debug("HTTP tunnel established: {} over {}", oldConnection, newConnection);
endPoint.upgrade(newConnection);
}
catch (Throwable x)
{
@ -363,9 +349,30 @@ public class HttpProxy extends ProxyConfiguration.Proxy
public static class TunnelRequest extends HttpRequest
{
private TunnelRequest(HttpClient client, Origin.Address address)
private final URI proxyURI;
private TunnelRequest(HttpClient client, URI proxyURI)
{
super(client, new HttpConversation(), URI.create("http://" + address.asString()));
this(client, new HttpConversation(), proxyURI);
}
private TunnelRequest(HttpClient client, HttpConversation conversation, URI proxyURI)
{
super(client, conversation, proxyURI);
this.proxyURI = proxyURI;
method(HttpMethod.CONNECT);
}
@Override
HttpRequest copyInstance(URI newURI)
{
return new TunnelRequest(getHttpClient(), getConversation(), newURI);
}
@Override
public URI getURI()
{
return proxyURI;
}
}
}

View File

@ -324,6 +324,10 @@ public class HttpRedirector
headers.remove(HttpHeader.CONTENT_TYPE);
});
}
else if (HttpMethod.CONNECT.is(method))
{
redirect.path(httpRequest.getPath());
}
Request.Content body = redirect.getBody();
if (body != null && !body.isReproducible())

View File

@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@ -56,6 +57,7 @@ import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.URIUtil;
public class HttpRequest implements Request
{
@ -92,6 +94,10 @@ public class HttpRequest implements Request
protected HttpRequest(HttpClient client, HttpConversation conversation, URI uri)
{
// URIs built from strings that have an internationalized domain name (IDN)
// are parsed without errors, but uri.getHost() returns null.
if (uri.getHost() == null)
throw new IllegalArgumentException(String.format("Invalid URI host: null (authority: %s)", uri.getRawAuthority()));
this.client = client;
this.conversation = conversation;
scheme = uri.getScheme();
@ -110,6 +116,47 @@ public class HttpRequest implements Request
headers.put(userAgentField);
}
HttpRequest copy(URI newURI)
{
if (newURI == null)
{
StringBuilder builder = new StringBuilder(64);
URIUtil.appendSchemeHostPort(builder, getScheme(), getHost(), getPort());
newURI = URI.create(builder.toString());
}
HttpRequest newRequest = copyInstance(newURI);
newRequest.method(getMethod())
.version(getVersion())
.body(getBody())
.idleTimeout(getIdleTimeout(), TimeUnit.MILLISECONDS)
.timeout(getTimeout(), TimeUnit.MILLISECONDS)
.followRedirects(isFollowRedirects())
.tag(getTag())
.headers(h -> h.clear().add(getHeaders())
// Remove the headers that depend on the URI.
.remove(EnumSet.of(
HttpHeader.HOST,
HttpHeader.EXPECT,
HttpHeader.COOKIE,
HttpHeader.AUTHORIZATION,
HttpHeader.PROXY_AUTHORIZATION
))
);
return newRequest;
}
HttpRequest copyInstance(URI newURI)
{
return new HttpRequest(getHttpClient(), getConversation(), newURI);
}
HttpClient getHttpClient()
{
return client;
}
public HttpConversation getConversation()
{
return conversation;

View File

@ -20,7 +20,7 @@ import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.util.Callback;
/**
* <p>A HTTP/2 specific handler of events for normal and tunneled exchanges.</p>
* <p>A HTTP/2 specific handler of events for normal and tunnelled exchanges.</p>
*/
public interface HTTP2Channel
{

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.http.client;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.StandardCharsets;
@ -25,6 +26,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -38,8 +40,10 @@ import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic;
import org.eclipse.jetty.client.http.HttpClientConnectionFactory;
import org.eclipse.jetty.client.util.BasicAuthentication;
import org.eclipse.jetty.client.util.ByteBufferRequestContent;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
@ -91,6 +95,7 @@ import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ProxyWithDynamicTransportTest
@ -145,6 +150,11 @@ public class ProxyWithDynamicTransportTest
}
private void startProxy(ConnectHandler connectHandler) throws Exception
{
startProxy(connectHandler, new ForwardProxyServlet());
}
private void startProxy(ConnectHandler connectHandler, ForwardProxyServlet proxyServlet) throws Exception
{
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
@ -173,17 +183,7 @@ public class ProxyWithDynamicTransportTest
proxy.setHandler(connectHandler);
ServletContextHandler context = new ServletContextHandler(connectHandler, "/");
ServletHolder holder = new ServletHolder(new AsyncProxyServlet()
{
@Override
protected HttpClient newHttpClient(ClientConnector clientConnector)
{
ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11;
HTTP2Client http2Client = new HTTP2Client(clientConnector);
ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client);
return new HttpClient(new HttpClientTransportDynamic(clientConnector, h1, http2));
}
});
ServletHolder holder = new ServletHolder(proxyServlet);
context.addServlet(holder, "/*");
proxy.start();
LOG.info("Started proxy on :{} and :{}", proxyConnector.getLocalPort(), proxyTLSConnector.getLocalPort());
@ -345,6 +345,144 @@ public class ProxyWithDynamicTransportTest
}));
}
@ParameterizedTest(name = "proxyProtocol={0}, proxySecure={1}, serverProtocol={2}, serverSecure={3}")
@MethodSource("testParams")
public void testProxyAuthentication(Origin.Protocol proxyProtocol, boolean proxySecure, HttpVersion serverProtocol, boolean serverSecure) throws Exception
{
int status = HttpStatus.NO_CONTENT_204;
startServer(new EmptyServerHandler()
{
@Override
protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
{
response.setStatus(status);
}
});
startProxy(new ConnectHandler()
{
@Override
protected void handleConnect(Request baseRequest, HttpServletRequest request, HttpServletResponse response, String serverAddress)
{
// Handle proxy authentication for tunnelled requests.
String proxyAuthorization = request.getHeader(HttpHeader.PROXY_AUTHORIZATION.asString());
if (proxyAuthorization == null)
{
baseRequest.setHandled(true);
response.setStatus(HttpStatus.FORBIDDEN_403);
}
else
{
super.handleConnect(baseRequest, request, response, serverAddress);
}
}
}, new ForwardProxyServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
// Handle proxy authentication for non-tunnelled requests.
String proxyAuthorization = request.getHeader(HttpHeader.PROXY_AUTHORIZATION.asString());
if (proxyAuthorization == null)
response.sendError(HttpStatus.FORBIDDEN_403);
else
super.service(request, response);
}
});
startClient();
String proxyScheme = proxySecure ? "https" : "http";
int proxyPort = proxySecure ? proxyTLSConnector.getLocalPort() : proxyConnector.getLocalPort();
Origin.Address proxyAddress = new Origin.Address("localhost", proxyPort);
HttpProxy proxy = new HttpProxy(proxyAddress, proxySecure, proxyProtocol);
client.getProxyConfiguration().addProxy(proxy);
URI uri = URI.create(proxyScheme + "://" + proxyAddress.asString());
client.getAuthenticationStore().addAuthenticationResult(new BasicAuthentication.BasicResult(uri, HttpHeader.PROXY_AUTHORIZATION, "proxy", "proxy"));
String serverScheme = serverSecure ? "https" : "http";
int serverPort = serverSecure ? serverTLSConnector.getLocalPort() : serverConnector.getLocalPort();
ContentResponse response = client.newRequest("localhost", serverPort)
.scheme(serverScheme)
.version(serverProtocol)
.timeout(5, TimeUnit.SECONDS)
.send();
assertEquals(status, response.getStatus());
}
@ParameterizedTest(name = "proxyProtocol={0}, proxySecure={1}, serverProtocol={2}, serverSecure={3}")
@MethodSource("testParams")
public void testProxyAuthenticationAndServerAuthentication(Origin.Protocol proxyProtocol, boolean proxySecure, HttpVersion serverProtocol, boolean serverSecure) throws Exception
{
int status = HttpStatus.NO_CONTENT_204;
startServer(new EmptyServerHandler()
{
@Override
protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
{
String proxyAuthorization = request.getHeader(HttpHeader.PROXY_AUTHORIZATION.asString());
assertNull(proxyAuthorization);
String authorization = request.getHeader(HttpHeader.AUTHORIZATION.asString());
if (authorization == null)
response.setStatus(HttpStatus.FORBIDDEN_403);
else
response.setStatus(status);
}
});
startProxy(new ConnectHandler()
{
@Override
protected void handleConnect(Request baseRequest, HttpServletRequest request, HttpServletResponse response, String serverAddress)
{
// Handle proxy authentication for tunnelled requests.
String proxyAuthorization = request.getHeader(HttpHeader.PROXY_AUTHORIZATION.asString());
if (proxyAuthorization == null)
{
baseRequest.setHandled(true);
response.setStatus(HttpStatus.FORBIDDEN_403);
}
else
{
super.handleConnect(baseRequest, request, response, serverAddress);
}
}
}, new ForwardProxyServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
// Handle proxy authentication for non-tunnelled requests.
String proxyAuthorization = request.getHeader(HttpHeader.PROXY_AUTHORIZATION.asString());
if (proxyAuthorization == null)
response.sendError(HttpStatus.FORBIDDEN_403);
else
super.service(request, response);
}
});
startClient();
String proxyScheme = proxySecure ? "https" : "http";
int proxyPort = proxySecure ? proxyTLSConnector.getLocalPort() : proxyConnector.getLocalPort();
Origin.Address proxyAddress = new Origin.Address("localhost", proxyPort);
HttpProxy proxy = new HttpProxy(proxyAddress, proxySecure, proxyProtocol);
client.getProxyConfiguration().addProxy(proxy);
String serverScheme = serverSecure ? "https" : "http";
int serverPort = serverSecure ? serverTLSConnector.getLocalPort() : serverConnector.getLocalPort();
URI proxyURI = URI.create(proxyScheme + "://" + proxyAddress.asString());
client.getAuthenticationStore().addAuthenticationResult(new BasicAuthentication.BasicResult(proxyURI, HttpHeader.PROXY_AUTHORIZATION, "proxy", "proxy"));
URI serverURI = URI.create(serverScheme + "://localhost:" + serverPort);
client.getAuthenticationStore().addAuthenticationResult(new BasicAuthentication.BasicResult(serverURI, HttpHeader.AUTHORIZATION, "server", "server"));
ContentResponse response = client.newRequest("localhost", serverPort)
.scheme(serverScheme)
.version(serverProtocol)
.timeout(5, TimeUnit.SECONDS)
.send();
assertEquals(status, response.getStatus());
}
@Test
public void testHTTP2TunnelClosedByClient() throws Exception
{
@ -629,4 +767,16 @@ public class ProxyWithDynamicTransportTest
// Tunnel must be closed.
assertTrue(closeLatch.await(5, TimeUnit.SECONDS));
}
private static class ForwardProxyServlet extends AsyncProxyServlet
{
@Override
protected HttpClient newHttpClient(ClientConnector clientConnector)
{
ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11;
HTTP2Client http2Client = new HTTP2Client(clientConnector);
ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client);
return new HttpClient(new HttpClientTransportDynamic(clientConnector, h1, http2));
}
}
}