From b921ed13c01cdf3badb992dead2913b22c88d254 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Wed, 27 Mar 2013 21:05:03 +0100 Subject: [PATCH] 400689 - Add support for Proxy authentication. --- .../client/AuthenticationProtocolHandler.java | 73 ++++----- .../org/eclipse/jetty/client/HttpClient.java | 3 +- .../eclipse/jetty/client/HttpConnection.java | 4 +- .../eclipse/jetty/client/HttpDestination.java | 10 ++ .../ProxyAuthenticationProtocolHandler.java | 64 ++++++++ .../WWWAuthenticationProtocolHandler.java | 63 +++++++ .../jetty/client/api/Authentication.java | 62 ++++++- .../client/util/BasicAuthentication.java | 11 +- .../client/util/DigestAuthentication.java | 25 ++- .../client/AbstractHttpClientServerTest.java | 3 +- .../jetty/client/HttpClientProxyTest.java | 155 ++++++++++++++++++ 11 files changed, 404 insertions(+), 69 deletions(-) create mode 100644 jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java create mode 100644 jetty-client/src/main/java/org/eclipse/jetty/client/WWWAuthenticationProtocolHandler.java create mode 100644 jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java index ccb15cf6424..7b66d6f2b45 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java @@ -35,33 +35,34 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -public class AuthenticationProtocolHandler implements ProtocolHandler +public abstract class AuthenticationProtocolHandler implements ProtocolHandler { + public static final int DEFAULT_MAX_CONTENT_LENGTH = 4096; public static final Logger LOG = Log.getLogger(AuthenticationProtocolHandler.class); - private static final Pattern WWW_AUTHENTICATE_PATTERN = Pattern.compile("([^\\s]+)\\s+realm=\"([^\"]+)\".*", Pattern.CASE_INSENSITIVE); + private static final Pattern AUTHENTICATE_PATTERN = Pattern.compile("([^\\s]+)\\s+realm=\"([^\"]+)\"(.*)", Pattern.CASE_INSENSITIVE); private final HttpClient client; private final int maxContentLength; private final ResponseNotifier notifier; - public AuthenticationProtocolHandler(HttpClient client) - { - this(client, 4096); - } - - public AuthenticationProtocolHandler(HttpClient client, int maxContentLength) + protected AuthenticationProtocolHandler(HttpClient client, int maxContentLength) { this.client = client; this.maxContentLength = maxContentLength; this.notifier = new ResponseNotifier(client); } - @Override - public boolean accept(Request request, Response response) + protected HttpClient getHttpClient() { - return response.getStatus() == 401; + return client; } + protected abstract HttpHeader getAuthenticateHeader(); + + protected abstract HttpHeader getAuthorizationHeader(); + + protected abstract URI getAuthenticationURI(Request request); + @Override public Response.Listener getResponseListener() { @@ -89,23 +90,24 @@ public class AuthenticationProtocolHandler implements ProtocolHandler return; } - List wwwAuthenticates = parseWWWAuthenticate(response); - if (wwwAuthenticates.isEmpty()) + HttpHeader header = getAuthenticateHeader(); + List headerInfos = parseAuthenticateHeader(response, header); + if (headerInfos.isEmpty()) { - LOG.debug("Authentication challenge without WWW-Authenticate header"); - forwardFailureComplete(request, null, response, new HttpResponseException("HTTP protocol violation: 401 without WWW-Authenticate header", response)); + LOG.debug("Authentication challenge without {} header", header); + forwardFailureComplete(request, null, response, new HttpResponseException("HTTP protocol violation: Authentication challenge without " + header + " header", response)); return; } - final URI uri = request.getURI(); + URI uri = getAuthenticationURI(request); Authentication authentication = null; - WWWAuthenticate wwwAuthenticate = null; - for (WWWAuthenticate wwwAuthn : wwwAuthenticates) + Authentication.HeaderInfo headerInfo = null; + for (Authentication.HeaderInfo element : headerInfos) { - authentication = client.getAuthenticationStore().findAuthentication(wwwAuthn.type, uri, wwwAuthn.realm); + authentication = client.getAuthenticationStore().findAuthentication(element.getType(), uri, element.getRealm()); if (authentication != null) { - wwwAuthenticate = wwwAuthn; + headerInfo = element; break; } } @@ -117,7 +119,7 @@ public class AuthenticationProtocolHandler implements ProtocolHandler } HttpConversation conversation = client.getConversation(request.getConversationID(), false); - final Authentication.Result authnResult = authentication.authenticate(request, response, wwwAuthenticate.value, conversation); + final Authentication.Result authnResult = authentication.authenticate(request, response, headerInfo, conversation); LOG.debug("Authentication result {}", authnResult); if (authnResult == null) { @@ -125,7 +127,7 @@ public class AuthenticationProtocolHandler implements ProtocolHandler return; } - Request newRequest = client.copyRequest(request, uri); + Request newRequest = client.copyRequest(request, request.getURI()); authnResult.apply(newRequest); newRequest.onResponseSuccess(new Response.SuccessListener() { @@ -151,37 +153,24 @@ public class AuthenticationProtocolHandler implements ProtocolHandler notifier.forwardFailureComplete(conversation.getResponseListeners(), request, requestFailure, response, responseFailure); } - private List parseWWWAuthenticate(Response response) + private List parseAuthenticateHeader(Response response, HttpHeader header) { // TODO: these should be ordered by strength - List result = new ArrayList<>(); - List values = Collections.list(response.getHeaders().getValues(HttpHeader.WWW_AUTHENTICATE.asString())); + List result = new ArrayList<>(); + List values = Collections.list(response.getHeaders().getValues(header.asString())); for (String value : values) { - Matcher matcher = WWW_AUTHENTICATE_PATTERN.matcher(value); + Matcher matcher = AUTHENTICATE_PATTERN.matcher(value); if (matcher.matches()) { String type = matcher.group(1); String realm = matcher.group(2); - WWWAuthenticate wwwAuthenticate = new WWWAuthenticate(value, type, realm); - result.add(wwwAuthenticate); + String params = matcher.group(3); + Authentication.HeaderInfo headerInfo = new Authentication.HeaderInfo(type, realm, params, getAuthorizationHeader()); + result.add(headerInfo); } } return result; } } - - private class WWWAuthenticate - { - private final String value; - private final String type; - private final String realm; - - public WWWAuthenticate(String value, String type, String realm) - { - this.value = value; - this.type = type; - this.realm = realm; - } - } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index b183d598a67..953855afc9a 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -207,7 +207,8 @@ public class HttpClient extends ContainerLifeCycle handlers.add(new ContinueProtocolHandler(this)); handlers.add(new RedirectProtocolHandler(this)); - handlers.add(new AuthenticationProtocolHandler(this)); + handlers.add(new WWWAuthenticationProtocolHandler(this)); + handlers.add(new ProxyAuthenticationProtocolHandler(this)); decoderFactories.add(new GZIPContentDecoder.Factory()); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java index 28a3198d3b3..ad96efbad78 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.client; import java.io.UnsupportedEncodingException; import java.net.HttpCookie; +import java.net.URI; import java.net.URLEncoder; import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; @@ -251,7 +252,8 @@ public class HttpConnection extends AbstractConnection implements Connection request.header(HttpHeader.COOKIE.asString(), cookieString.toString()); // Authorization - Authentication.Result authnResult = client.getAuthenticationStore().findAuthenticationResult(request.getURI()); + URI authenticationURI = destination.isProxied() ? destination.getProxyURI() : request.getURI(); + Authentication.Result authnResult = client.getAuthenticationStore().findAuthenticationResult(authenticationURI); if (authnResult != null) authnResult.apply(request); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java index f1ccb649c6e..6967fd7e371 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.client; import java.io.Closeable; import java.io.IOException; +import java.net.URI; import java.nio.channels.AsynchronousCloseException; import java.util.ArrayList; import java.util.List; @@ -141,6 +142,15 @@ public class HttpDestination implements Destination, Closeable, Dumpable return proxyAddress != null; } + public URI getProxyURI() + { + ProxyConfiguration proxyConfiguration = client.getProxyConfiguration(); + String uri = getScheme() + "://" + proxyConfiguration.getHost(); + if (!client.isDefaultPort(getScheme(), proxyConfiguration.getPort())) + uri += ":" + proxyConfiguration.getPort(); + return URI.create(uri); + } + public HttpField getHostField() { return hostField; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java new file mode 100644 index 00000000000..841c661414d --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java @@ -0,0 +1,64 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import java.net.URI; + +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; + +public class ProxyAuthenticationProtocolHandler extends AuthenticationProtocolHandler +{ + public ProxyAuthenticationProtocolHandler(HttpClient client) + { + this(client, DEFAULT_MAX_CONTENT_LENGTH); + } + + public ProxyAuthenticationProtocolHandler(HttpClient client, int maxContentLength) + { + super(client, maxContentLength); + } + + @Override + public boolean accept(Request request, Response response) + { + return response.getStatus() == HttpStatus.PROXY_AUTHENTICATION_REQUIRED_407; + } + + @Override + protected HttpHeader getAuthenticateHeader() + { + return HttpHeader.PROXY_AUTHENTICATE; + } + + @Override + protected HttpHeader getAuthorizationHeader() + { + return HttpHeader.PROXY_AUTHORIZATION; + } + + @Override + protected URI getAuthenticationURI(Request request) + { + HttpDestination destination = getHttpClient().destinationFor(request.getScheme(), request.getHost(), request.getPort()); + return destination.isProxied() ? destination.getProxyURI() : request.getURI(); + } +} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/WWWAuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/WWWAuthenticationProtocolHandler.java new file mode 100644 index 00000000000..21c7c0be98f --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/WWWAuthenticationProtocolHandler.java @@ -0,0 +1,63 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import java.net.URI; + +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; + +public class WWWAuthenticationProtocolHandler extends AuthenticationProtocolHandler +{ + public WWWAuthenticationProtocolHandler(HttpClient client) + { + this(client, DEFAULT_MAX_CONTENT_LENGTH); + } + + public WWWAuthenticationProtocolHandler(HttpClient client, int maxContentLength) + { + super(client, maxContentLength); + } + + @Override + public boolean accept(Request request, Response response) + { + return response.getStatus() == HttpStatus.UNAUTHORIZED_401; + } + + @Override + protected HttpHeader getAuthenticateHeader() + { + return HttpHeader.WWW_AUTHENTICATE; + } + + @Override + protected HttpHeader getAuthorizationHeader() + { + return HttpHeader.AUTHORIZATION; + } + + @Override + protected URI getAuthenticationURI(Request request) + { + return request.getURI(); + } +} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Authentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Authentication.java index aeb7dd4ac89..9341d7b1eb3 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Authentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Authentication.java @@ -20,18 +20,19 @@ package org.eclipse.jetty.client.api; import java.net.URI; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.util.Attributes; /** * {@link Authentication} represents a mechanism to authenticate requests for protected resources. *

* {@link Authentication}s are added to an {@link AuthenticationStore}, which is then - * {@link #matches(String, String, String) queried} to find the right + * {@link #matches(String, URI, String) queried} to find the right * {@link Authentication} mechanism to use based on its type, URI and realm, as returned by * {@code WWW-Authenticate} response headers. *

* If an {@link Authentication} mechanism is found, it is then - * {@link #authenticate(Request, ContentResponse, String, Attributes) executed} for the given request, + * {@link #authenticate(Request, ContentResponse, HeaderInfo, Attributes) executed} for the given request, * returning an {@link Authentication.Result}, which is then stored in the {@link AuthenticationStore} * so that subsequent requests can be preemptively authenticated. */ @@ -56,13 +57,64 @@ public interface Authentication * * @param request the request to execute the authentication mechanism for * @param response the 401 response obtained in the previous attempt to request the protected resource - * @param wwwAuthenticate the {@code WWW-Authenticate} header chosen for this authentication - * (among the many that the response may contain) + * @param headerInfo the {@code WWW-Authenticate} (or {@code Proxy-Authenticate}) header chosen for this + * authentication (among the many that the response may contain) * @param context the conversation context in case the authentication needs multiple exchanges * to be completed and information needs to be stored across exchanges * @return the authentication result, or null if the authentication could not be performed */ - Result authenticate(Request request, ContentResponse response, String wwwAuthenticate, Attributes context); + Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context); + + /** + * Structure holding information about the {@code WWW-Authenticate} (or {@code Proxy-Authenticate}) header. + */ + public static class HeaderInfo + { + private final String type; + private final String realm; + private final String params; + private final HttpHeader header; + + public HeaderInfo(String type, String realm, String params, HttpHeader header) + { + this.type = type; + this.realm = realm; + this.params = params; + this.header = header; + } + + /** + * @return the authentication type (for example "Basic" or "Digest") + */ + public String getType() + { + return type; + } + + /** + * @return the realm name + */ + public String getRealm() + { + return realm; + } + + /** + * @return additional authentication parameters + */ + public String getParameters() + { + return params; + } + + /** + * @return the {@code Authorization} (or {@code Proxy-Authorization}) header + */ + public HttpHeader getHeader() + { + return header; + } + } /** * {@link Result} holds the information needed to authenticate a {@link Request} via {@link #apply(Request)}. diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java index 6769e85b23d..7bf9fc29abd 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java @@ -71,20 +71,22 @@ public class BasicAuthentication implements Authentication } @Override - public Result authenticate(Request request, ContentResponse response, String wwwAuthenticate, Attributes context) + public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context) { String encoding = StringUtil.__ISO_8859_1; String value = "Basic " + B64Code.encode(user + ":" + password, encoding); - return new BasicResult(request.getURI(), value); + return new BasicResult(headerInfo.getHeader(), uri, value); } private static class BasicResult implements Result { + private final HttpHeader header; private final URI uri; private final String value; - public BasicResult(URI uri, String value) + public BasicResult(HttpHeader header, URI uri, String value) { + this.header = header; this.uri = uri; this.value = value; } @@ -98,8 +100,7 @@ public class BasicAuthentication implements Authentication @Override public void apply(Request request) { - if (request.getURI().toString().startsWith(uri.toString())) - request.header(HttpHeader.AUTHORIZATION, value); + request.header(header, value); } @Override diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java index 61604f62425..b6a54cb870d 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java @@ -85,13 +85,9 @@ public class DigestAuthentication implements Authentication } @Override - public Result authenticate(Request request, ContentResponse response, String wwwAuthenticate, Attributes context) + public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context) { - // Avoid case sensitivity problems on the 'D' character - String type = "igest"; - wwwAuthenticate = wwwAuthenticate.substring(wwwAuthenticate.indexOf(type) + type.length()); - - Map params = parseParams(wwwAuthenticate); + Map params = parseParameters(headerInfo.getParameters()); String nonce = params.get("nonce"); if (nonce == null || nonce.length() == 0) return null; @@ -113,10 +109,10 @@ public class DigestAuthentication implements Authentication clientQOP = "auth-int"; } - return new DigestResult(request.getURI(), response.getContent(), realm, user, password, algorithm, nonce, clientQOP, opaque); + return new DigestResult(headerInfo.getHeader(), uri, response.getContent(), realm, user, password, algorithm, nonce, clientQOP, opaque); } - private Map parseParams(String wwwAuthenticate) + private Map parseParameters(String wwwAuthenticate) { Map result = new HashMap<>(); List parts = splitParams(wwwAuthenticate); @@ -154,7 +150,9 @@ public class DigestAuthentication implements Authentication case ',': if (quotes % 2 == 0) { - result.add(paramString.substring(start, i).trim()); + String element = paramString.substring(start, i).trim(); + if (element.length() > 0) + result.add(element); start = i + 1; } break; @@ -181,6 +179,7 @@ public class DigestAuthentication implements Authentication private class DigestResult implements Result { private final AtomicInteger nonceCount = new AtomicInteger(); + private final HttpHeader header; private final URI uri; private final byte[] content; private final String realm; @@ -191,8 +190,9 @@ public class DigestAuthentication implements Authentication private final String qop; private final String opaque; - public DigestResult(URI uri, byte[] content, String realm, String user, String password, String algorithm, String nonce, String qop, String opaque) + public DigestResult(HttpHeader header, URI uri, byte[] content, String realm, String user, String password, String algorithm, String nonce, String qop, String opaque) { + this.header = header; this.uri = uri; this.content = content; this.realm = realm; @@ -213,9 +213,6 @@ public class DigestAuthentication implements Authentication @Override public void apply(Request request) { - if (!request.getURI().toString().startsWith(uri.toString())) - return; - MessageDigest digester = getMessageDigest(algorithm); if (digester == null) return; @@ -262,7 +259,7 @@ public class DigestAuthentication implements Authentication } value.append(", response=\"").append(hashA3).append("\""); - request.header(HttpHeader.AUTHORIZATION, value.toString()); + request.header(header, value.toString()); } private String nextNonceCount() diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java index 5fc20d1971c..42e3b50516f 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.client; import java.util.Arrays; import java.util.Collection; +import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Server; @@ -54,7 +55,7 @@ public abstract class AbstractHttpClientServerTest public AbstractHttpClientServerTest(SslContextFactory sslContextFactory) { this.sslContextFactory = sslContextFactory; - this.scheme = sslContextFactory == null ? "http" : "https"; + this.scheme = (sslContextFactory == null ? HttpScheme.HTTP : HttpScheme.HTTPS).asString(); } public void start(Handler handler) throws Exception diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java new file mode 100644 index 00000000000..c645f1cdb3e --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java @@ -0,0 +1,155 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.ProxyConfiguration; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.BasicAuthentication; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.B64Code; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.Assert; +import org.junit.Test; + +public class HttpClientProxyTest extends AbstractHttpClientServerTest +{ + public HttpClientProxyTest(SslContextFactory sslContextFactory) + { + // Avoid TLS otherwise CONNECT requests are sent instead of proxied requests + super(null); + } + + @Test + public void testProxiedRequest() throws Exception + { + final String serverHost = "server"; + final int status = HttpStatus.NO_CONTENT_204; + start(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + if (serverHost.equals(request.getServerName())) + response.setStatus(status); + } + }); + + int proxyPort = connector.getLocalPort(); + int serverPort = proxyPort + 1; // Any port will do for these tests - just not the same as the proxy + client.setProxyConfiguration(new ProxyConfiguration("localhost", proxyPort)); + + ContentResponse response = client.newRequest(serverHost, serverPort) + .scheme(scheme) + .timeout(5, TimeUnit.SECONDS) + .send(); + + Assert.assertEquals(status, response.getStatus()); + } + + @Test + public void testAuthenticatedProxiedRequest() throws Exception + { + final String user = "foo"; + final String password = "bar"; + final String credentials = B64Code.encode(user + ":" + password, "ISO-8859-1"); + final String serverHost = "server"; + final String realm = "test_realm"; + final int status = HttpStatus.NO_CONTENT_204; + start(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + String authorization = request.getHeader(HttpHeader.PROXY_AUTHORIZATION.asString()); + if (authorization == null) + { + response.setStatus(HttpStatus.PROXY_AUTHENTICATION_REQUIRED_407); + response.setHeader(HttpHeader.PROXY_AUTHENTICATE.asString(), "Basic realm=\"" + realm + "\""); + } + else + { + String prefix = "Basic "; + if (authorization.startsWith(prefix)) + { + String attempt = authorization.substring(prefix.length()); + if (credentials.equals(attempt)) + response.setStatus(status); + } + } + } + }); + + String proxyHost = "localhost"; + int proxyPort = connector.getLocalPort(); + int serverPort = proxyPort + 1; // Any port will do for these tests - just not the same as the proxy + client.setProxyConfiguration(new ProxyConfiguration(proxyHost, proxyPort)); + + ContentResponse response1 = client.newRequest(serverHost, serverPort) + .scheme(scheme) + .timeout(555, TimeUnit.SECONDS) + .send(); + + // No Authentication available => 407 + Assert.assertEquals(HttpStatus.PROXY_AUTHENTICATION_REQUIRED_407, response1.getStatus()); + + // Add authentication... + URI uri = URI.create(scheme + "://" + proxyHost + ":" + proxyPort); + client.getAuthenticationStore().addAuthentication(new BasicAuthentication(uri, realm, user, password)); + final AtomicInteger requests = new AtomicInteger(); + client.getRequestListeners().add(new Request.Listener.Empty() + { + @Override + public void onSuccess(Request request) + { + requests.incrementAndGet(); + } + }); + // ...and perform the request again => 407 + 204 + ContentResponse response2 = client.newRequest(serverHost, serverPort) + .scheme(scheme) + .timeout(555, TimeUnit.SECONDS) + .send(); + + Assert.assertEquals(status, response2.getStatus()); + Assert.assertEquals(2, requests.get()); + + // Now the authentication result is cached => 204 + requests.set(0); + ContentResponse response3 = client.newRequest(serverHost, serverPort) + .scheme(scheme) + .timeout(555, TimeUnit.SECONDS) + .send(); + + Assert.assertEquals(status, response3.getStatus()); + Assert.assertEquals(1, requests.get()); + } +}