400689 - Add support for Proxy authentication.
This commit is contained in:
parent
4951b1ccc4
commit
b921ed13c0
|
@ -35,33 +35,34 @@ import org.eclipse.jetty.http.HttpHeader;
|
||||||
import org.eclipse.jetty.util.log.Log;
|
import org.eclipse.jetty.util.log.Log;
|
||||||
import org.eclipse.jetty.util.log.Logger;
|
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);
|
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 HttpClient client;
|
||||||
private final int maxContentLength;
|
private final int maxContentLength;
|
||||||
private final ResponseNotifier notifier;
|
private final ResponseNotifier notifier;
|
||||||
|
|
||||||
public AuthenticationProtocolHandler(HttpClient client)
|
protected AuthenticationProtocolHandler(HttpClient client, int maxContentLength)
|
||||||
{
|
|
||||||
this(client, 4096);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthenticationProtocolHandler(HttpClient client, int maxContentLength)
|
|
||||||
{
|
{
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.maxContentLength = maxContentLength;
|
this.maxContentLength = maxContentLength;
|
||||||
this.notifier = new ResponseNotifier(client);
|
this.notifier = new ResponseNotifier(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected HttpClient getHttpClient()
|
||||||
public boolean accept(Request request, Response response)
|
|
||||||
{
|
{
|
||||||
return response.getStatus() == 401;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract HttpHeader getAuthenticateHeader();
|
||||||
|
|
||||||
|
protected abstract HttpHeader getAuthorizationHeader();
|
||||||
|
|
||||||
|
protected abstract URI getAuthenticationURI(Request request);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response.Listener getResponseListener()
|
public Response.Listener getResponseListener()
|
||||||
{
|
{
|
||||||
|
@ -89,23 +90,24 @@ public class AuthenticationProtocolHandler implements ProtocolHandler
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<WWWAuthenticate> wwwAuthenticates = parseWWWAuthenticate(response);
|
HttpHeader header = getAuthenticateHeader();
|
||||||
if (wwwAuthenticates.isEmpty())
|
List<Authentication.HeaderInfo> headerInfos = parseAuthenticateHeader(response, header);
|
||||||
|
if (headerInfos.isEmpty())
|
||||||
{
|
{
|
||||||
LOG.debug("Authentication challenge without WWW-Authenticate header");
|
LOG.debug("Authentication challenge without {} header", header);
|
||||||
forwardFailureComplete(request, null, response, new HttpResponseException("HTTP protocol violation: 401 without WWW-Authenticate header", response));
|
forwardFailureComplete(request, null, response, new HttpResponseException("HTTP protocol violation: Authentication challenge without " + header + " header", response));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final URI uri = request.getURI();
|
URI uri = getAuthenticationURI(request);
|
||||||
Authentication authentication = null;
|
Authentication authentication = null;
|
||||||
WWWAuthenticate wwwAuthenticate = null;
|
Authentication.HeaderInfo headerInfo = null;
|
||||||
for (WWWAuthenticate wwwAuthn : wwwAuthenticates)
|
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)
|
if (authentication != null)
|
||||||
{
|
{
|
||||||
wwwAuthenticate = wwwAuthn;
|
headerInfo = element;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,7 +119,7 @@ public class AuthenticationProtocolHandler implements ProtocolHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpConversation conversation = client.getConversation(request.getConversationID(), false);
|
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);
|
LOG.debug("Authentication result {}", authnResult);
|
||||||
if (authnResult == null)
|
if (authnResult == null)
|
||||||
{
|
{
|
||||||
|
@ -125,7 +127,7 @@ public class AuthenticationProtocolHandler implements ProtocolHandler
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Request newRequest = client.copyRequest(request, uri);
|
Request newRequest = client.copyRequest(request, request.getURI());
|
||||||
authnResult.apply(newRequest);
|
authnResult.apply(newRequest);
|
||||||
newRequest.onResponseSuccess(new Response.SuccessListener()
|
newRequest.onResponseSuccess(new Response.SuccessListener()
|
||||||
{
|
{
|
||||||
|
@ -151,37 +153,24 @@ public class AuthenticationProtocolHandler implements ProtocolHandler
|
||||||
notifier.forwardFailureComplete(conversation.getResponseListeners(), request, requestFailure, response, responseFailure);
|
notifier.forwardFailureComplete(conversation.getResponseListeners(), request, requestFailure, response, responseFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<WWWAuthenticate> parseWWWAuthenticate(Response response)
|
private List<Authentication.HeaderInfo> parseAuthenticateHeader(Response response, HttpHeader header)
|
||||||
{
|
{
|
||||||
// TODO: these should be ordered by strength
|
// TODO: these should be ordered by strength
|
||||||
List<WWWAuthenticate> result = new ArrayList<>();
|
List<Authentication.HeaderInfo> result = new ArrayList<>();
|
||||||
List<String> values = Collections.list(response.getHeaders().getValues(HttpHeader.WWW_AUTHENTICATE.asString()));
|
List<String> values = Collections.list(response.getHeaders().getValues(header.asString()));
|
||||||
for (String value : values)
|
for (String value : values)
|
||||||
{
|
{
|
||||||
Matcher matcher = WWW_AUTHENTICATE_PATTERN.matcher(value);
|
Matcher matcher = AUTHENTICATE_PATTERN.matcher(value);
|
||||||
if (matcher.matches())
|
if (matcher.matches())
|
||||||
{
|
{
|
||||||
String type = matcher.group(1);
|
String type = matcher.group(1);
|
||||||
String realm = matcher.group(2);
|
String realm = matcher.group(2);
|
||||||
WWWAuthenticate wwwAuthenticate = new WWWAuthenticate(value, type, realm);
|
String params = matcher.group(3);
|
||||||
result.add(wwwAuthenticate);
|
Authentication.HeaderInfo headerInfo = new Authentication.HeaderInfo(type, realm, params, getAuthorizationHeader());
|
||||||
|
result.add(headerInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -207,7 +207,8 @@ public class HttpClient extends ContainerLifeCycle
|
||||||
|
|
||||||
handlers.add(new ContinueProtocolHandler(this));
|
handlers.add(new ContinueProtocolHandler(this));
|
||||||
handlers.add(new RedirectProtocolHandler(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());
|
decoderFactories.add(new GZIPContentDecoder.Factory());
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.eclipse.jetty.client;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.HttpCookie;
|
import java.net.HttpCookie;
|
||||||
|
import java.net.URI;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.UnsupportedCharsetException;
|
import java.nio.charset.UnsupportedCharsetException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -251,7 +252,8 @@ public class HttpConnection extends AbstractConnection implements Connection
|
||||||
request.header(HttpHeader.COOKIE.asString(), cookieString.toString());
|
request.header(HttpHeader.COOKIE.asString(), cookieString.toString());
|
||||||
|
|
||||||
// Authorization
|
// 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)
|
if (authnResult != null)
|
||||||
authnResult.apply(request);
|
authnResult.apply(request);
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.eclipse.jetty.client;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
import java.nio.channels.AsynchronousCloseException;
|
import java.nio.channels.AsynchronousCloseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -141,6 +142,15 @@ public class HttpDestination implements Destination, Closeable, Dumpable
|
||||||
return proxyAddress != null;
|
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()
|
public HttpField getHostField()
|
||||||
{
|
{
|
||||||
return hostField;
|
return hostField;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,18 +20,19 @@ package org.eclipse.jetty.client.api;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.http.HttpHeader;
|
||||||
import org.eclipse.jetty.util.Attributes;
|
import org.eclipse.jetty.util.Attributes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link Authentication} represents a mechanism to authenticate requests for protected resources.
|
* {@link Authentication} represents a mechanism to authenticate requests for protected resources.
|
||||||
* <p />
|
* <p />
|
||||||
* {@link Authentication}s are added to an {@link AuthenticationStore}, which is then
|
* {@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
|
* {@link Authentication} mechanism to use based on its type, URI and realm, as returned by
|
||||||
* {@code WWW-Authenticate} response headers.
|
* {@code WWW-Authenticate} response headers.
|
||||||
* <p />
|
* <p />
|
||||||
* If an {@link Authentication} mechanism is found, it is then
|
* 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}
|
* returning an {@link Authentication.Result}, which is then stored in the {@link AuthenticationStore}
|
||||||
* so that subsequent requests can be preemptively authenticated.
|
* 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 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 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
|
* @param headerInfo the {@code WWW-Authenticate} (or {@code Proxy-Authenticate}) header chosen for this
|
||||||
* (among the many that the response may contain)
|
* authentication (among the many that the response may contain)
|
||||||
* @param context the conversation context in case the authentication needs multiple exchanges
|
* @param context the conversation context in case the authentication needs multiple exchanges
|
||||||
* to be completed and information needs to be stored across 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
|
* @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)}.
|
* {@link Result} holds the information needed to authenticate a {@link Request} via {@link #apply(Request)}.
|
||||||
|
|
|
@ -71,20 +71,22 @@ public class BasicAuthentication implements Authentication
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 encoding = StringUtil.__ISO_8859_1;
|
||||||
String value = "Basic " + B64Code.encode(user + ":" + password, encoding);
|
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 static class BasicResult implements Result
|
||||||
{
|
{
|
||||||
|
private final HttpHeader header;
|
||||||
private final URI uri;
|
private final URI uri;
|
||||||
private final String value;
|
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.uri = uri;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
@ -98,8 +100,7 @@ public class BasicAuthentication implements Authentication
|
||||||
@Override
|
@Override
|
||||||
public void apply(Request request)
|
public void apply(Request request)
|
||||||
{
|
{
|
||||||
if (request.getURI().toString().startsWith(uri.toString()))
|
request.header(header, value);
|
||||||
request.header(HttpHeader.AUTHORIZATION, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -85,13 +85,9 @@ public class DigestAuthentication implements Authentication
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
Map<String, String> params = parseParameters(headerInfo.getParameters());
|
||||||
String type = "igest";
|
|
||||||
wwwAuthenticate = wwwAuthenticate.substring(wwwAuthenticate.indexOf(type) + type.length());
|
|
||||||
|
|
||||||
Map<String, String> params = parseParams(wwwAuthenticate);
|
|
||||||
String nonce = params.get("nonce");
|
String nonce = params.get("nonce");
|
||||||
if (nonce == null || nonce.length() == 0)
|
if (nonce == null || nonce.length() == 0)
|
||||||
return null;
|
return null;
|
||||||
|
@ -113,10 +109,10 @@ public class DigestAuthentication implements Authentication
|
||||||
clientQOP = "auth-int";
|
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<String, String> parseParams(String wwwAuthenticate)
|
private Map<String, String> parseParameters(String wwwAuthenticate)
|
||||||
{
|
{
|
||||||
Map<String, String> result = new HashMap<>();
|
Map<String, String> result = new HashMap<>();
|
||||||
List<String> parts = splitParams(wwwAuthenticate);
|
List<String> parts = splitParams(wwwAuthenticate);
|
||||||
|
@ -154,7 +150,9 @@ public class DigestAuthentication implements Authentication
|
||||||
case ',':
|
case ',':
|
||||||
if (quotes % 2 == 0)
|
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;
|
start = i + 1;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -181,6 +179,7 @@ public class DigestAuthentication implements Authentication
|
||||||
private class DigestResult implements Result
|
private class DigestResult implements Result
|
||||||
{
|
{
|
||||||
private final AtomicInteger nonceCount = new AtomicInteger();
|
private final AtomicInteger nonceCount = new AtomicInteger();
|
||||||
|
private final HttpHeader header;
|
||||||
private final URI uri;
|
private final URI uri;
|
||||||
private final byte[] content;
|
private final byte[] content;
|
||||||
private final String realm;
|
private final String realm;
|
||||||
|
@ -191,8 +190,9 @@ public class DigestAuthentication implements Authentication
|
||||||
private final String qop;
|
private final String qop;
|
||||||
private final String opaque;
|
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.uri = uri;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
|
@ -213,9 +213,6 @@ public class DigestAuthentication implements Authentication
|
||||||
@Override
|
@Override
|
||||||
public void apply(Request request)
|
public void apply(Request request)
|
||||||
{
|
{
|
||||||
if (!request.getURI().toString().startsWith(uri.toString()))
|
|
||||||
return;
|
|
||||||
|
|
||||||
MessageDigest digester = getMessageDigest(algorithm);
|
MessageDigest digester = getMessageDigest(algorithm);
|
||||||
if (digester == null)
|
if (digester == null)
|
||||||
return;
|
return;
|
||||||
|
@ -262,7 +259,7 @@ public class DigestAuthentication implements Authentication
|
||||||
}
|
}
|
||||||
value.append(", response=\"").append(hashA3).append("\"");
|
value.append(", response=\"").append(hashA3).append("\"");
|
||||||
|
|
||||||
request.header(HttpHeader.AUTHORIZATION, value.toString());
|
request.header(header, value.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String nextNonceCount()
|
private String nextNonceCount()
|
||||||
|
|
|
@ -21,6 +21,7 @@ package org.eclipse.jetty.client;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.http.HttpScheme;
|
||||||
import org.eclipse.jetty.server.Handler;
|
import org.eclipse.jetty.server.Handler;
|
||||||
import org.eclipse.jetty.server.NetworkConnector;
|
import org.eclipse.jetty.server.NetworkConnector;
|
||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
|
@ -54,7 +55,7 @@ public abstract class AbstractHttpClientServerTest
|
||||||
public AbstractHttpClientServerTest(SslContextFactory sslContextFactory)
|
public AbstractHttpClientServerTest(SslContextFactory sslContextFactory)
|
||||||
{
|
{
|
||||||
this.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
|
public void start(Handler handler) throws Exception
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue