Merge branch 'jetty-9' of ssh://git.eclipse.org/gitroot/jetty/org.eclipse.jetty.project into jetty-9
This commit is contained in:
commit
e9d7cc3cab
|
@ -1,3 +1,21 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 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.embedded;
|
||||
|
||||
import org.eclipse.jetty.xml.XmlConfiguration;
|
||||
|
@ -14,6 +32,5 @@ public class TestXml
|
|||
"../jetty-spdy/spdy-jetty-http-webapp/src/main/config/etc/jetty-spdy.xml"
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
package org.eclipse.jetty.client;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@ -25,21 +26,32 @@ import java.util.regex.Matcher;
|
|||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jetty.client.api.Authentication;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.api.Result;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
public class AuthenticationProtocolHandler extends Response.Listener.Adapter implements ProtocolHandler
|
||||
public class AuthenticationProtocolHandler implements ProtocolHandler
|
||||
{
|
||||
private static final Pattern WWW_AUTHENTICATE_PATTERN = Pattern.compile("([^\\s]+)\\s+realm=\"([^\"]+)\"(\\s*,\\s*)?(.*)", Pattern.CASE_INSENSITIVE);
|
||||
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 final ResponseNotifier notifier = new ResponseNotifier();
|
||||
private final HttpClient client;
|
||||
private final int maxContentLength;
|
||||
|
||||
public AuthenticationProtocolHandler(HttpClient client)
|
||||
{
|
||||
this(client, 4096);
|
||||
}
|
||||
|
||||
public AuthenticationProtocolHandler(HttpClient client, int maxContentLength)
|
||||
{
|
||||
this.client = client;
|
||||
this.maxContentLength = maxContentLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -51,102 +63,146 @@ public class AuthenticationProtocolHandler extends Response.Listener.Adapter imp
|
|||
@Override
|
||||
public Response.Listener getResponseListener()
|
||||
{
|
||||
return this;
|
||||
return new AuthenticationListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
private class AuthenticationListener extends Response.Listener.Adapter
|
||||
{
|
||||
if (!result.isFailed())
|
||||
private byte[] buffer = new byte[0];
|
||||
|
||||
@Override
|
||||
public void onContent(Response response, ByteBuffer content)
|
||||
{
|
||||
List<WWWAuthenticate> wwwAuthenticates = parseWWWAuthenticate(result.getResponse());
|
||||
if (buffer.length == maxContentLength)
|
||||
return;
|
||||
|
||||
long newLength = buffer.length + content.remaining();
|
||||
if (newLength > maxContentLength)
|
||||
newLength = maxContentLength;
|
||||
|
||||
byte[] newBuffer = new byte[(int)newLength];
|
||||
System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
|
||||
content.get(newBuffer, buffer.length, content.remaining());
|
||||
buffer = newBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
{
|
||||
Request request = result.getRequest();
|
||||
ContentResponse response = new HttpContentResponse(result.getResponse(), buffer);
|
||||
if (result.isFailed())
|
||||
{
|
||||
Throwable failure = result.getFailure();
|
||||
LOG.debug("Authentication challenge failed {}", failure);
|
||||
forwardFailure(request, response, failure);
|
||||
return;
|
||||
}
|
||||
|
||||
List<WWWAuthenticate> wwwAuthenticates = parseWWWAuthenticate(response);
|
||||
if (wwwAuthenticates.isEmpty())
|
||||
{
|
||||
// TODO
|
||||
LOG.debug("Authentication challenge without WWW-Authenticate header");
|
||||
forwardFailure(request, response, new HttpResponseException("HTTP protocol violation: 401 without WWW-Authenticate header", response));
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
final String uri = request.uri();
|
||||
Authentication authentication = null;
|
||||
WWWAuthenticate wwwAuthenticate = null;
|
||||
for (WWWAuthenticate wwwAuthn : wwwAuthenticates)
|
||||
{
|
||||
Request request = result.getRequest();
|
||||
final String uri = request.uri();
|
||||
Authentication authentication = null;
|
||||
for (WWWAuthenticate wwwAuthenticate : wwwAuthenticates)
|
||||
{
|
||||
authentication = client.getAuthenticationStore().findAuthentication(wwwAuthenticate.type, uri, wwwAuthenticate.realm);
|
||||
if (authentication != null)
|
||||
break;
|
||||
}
|
||||
authentication = client.getAuthenticationStore().findAuthentication(wwwAuthn.type, uri, wwwAuthn.realm);
|
||||
if (authentication != null)
|
||||
{
|
||||
final Authentication authn = authentication;
|
||||
authn.authenticate(request);
|
||||
request.send(new Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(Result result)
|
||||
{
|
||||
if (!result.isFailed())
|
||||
{
|
||||
Authentication.Result authnResult = new Authentication.Result(uri, authn);
|
||||
client.getAuthenticationStore().addAuthenticationResult(authnResult);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
noAuthentication(request, result.getResponse());
|
||||
wwwAuthenticate = wwwAuthn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<WWWAuthenticate> parseWWWAuthenticate(Response response)
|
||||
{
|
||||
List<WWWAuthenticate> result = new ArrayList<>();
|
||||
List<String> values = Collections.list(response.headers().getValues(HttpHeader.WWW_AUTHENTICATE.asString()));
|
||||
for (String value : values)
|
||||
{
|
||||
Matcher matcher = WWW_AUTHENTICATE_PATTERN.matcher(value);
|
||||
if (matcher.matches())
|
||||
if (authentication == null)
|
||||
{
|
||||
String type = matcher.group(1);
|
||||
String realm = matcher.group(2);
|
||||
String params = matcher.group(4);
|
||||
WWWAuthenticate wwwAuthenticate = new WWWAuthenticate(type, realm, params);
|
||||
result.add(wwwAuthenticate);
|
||||
LOG.debug("No authentication available for {}", request);
|
||||
forwardSuccess(request, response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void noAuthentication(Request request, Response response)
|
||||
{
|
||||
HttpConversation conversation = client.getConversation(request);
|
||||
Response.Listener listener = conversation.exchanges().peekFirst().listener();
|
||||
notifier.notifyBegin(listener, response);
|
||||
notifier.notifyHeaders(listener, response);
|
||||
notifier.notifySuccess(listener, response);
|
||||
// TODO: this call here is horrid, but needed... but here it is too late for the exchange
|
||||
// TODO: to figure out that the conversation is finished, so we need to manually do it here, no matter what.
|
||||
// TODO: However, we also need to make sure that requests are not resent with the same ID
|
||||
// TODO: because here the connection has already been returned to the pool, so the "new" request may see
|
||||
// TODO: the same conversation but it's not really the case.
|
||||
// TODO: perhaps the factory for requests should be the conversation ?
|
||||
conversation.complete();
|
||||
notifier.notifyComplete(listener, new Result(request, response));
|
||||
HttpConversation conversation = client.getConversation(request);
|
||||
final Authentication.Result authnResult = authentication.authenticate(request, response, wwwAuthenticate.value, conversation);
|
||||
LOG.debug("Authentication result {}", authnResult);
|
||||
if (authnResult == null)
|
||||
{
|
||||
forwardSuccess(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
authnResult.apply(request);
|
||||
request.send(new Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onSuccess(Response response)
|
||||
{
|
||||
client.getAuthenticationStore().addAuthenticationResult(authnResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void forwardFailure(Request request, Response response, Throwable failure)
|
||||
{
|
||||
HttpConversation conversation = client.getConversation(request);
|
||||
Response.Listener listener = conversation.exchanges().peekFirst().listener();
|
||||
notifier.notifyBegin(listener, response);
|
||||
notifier.notifyHeaders(listener, response);
|
||||
if (response instanceof ContentResponse)
|
||||
notifier.notifyContent(listener, response, ByteBuffer.wrap(((ContentResponse)response).content()));
|
||||
notifier.notifyFailure(listener, response, failure);
|
||||
conversation.complete();
|
||||
notifier.notifyComplete(listener, new Result(request, response, failure));
|
||||
}
|
||||
|
||||
private void forwardSuccess(Request request, Response response)
|
||||
{
|
||||
HttpConversation conversation = client.getConversation(request);
|
||||
Response.Listener listener = conversation.exchanges().peekFirst().listener();
|
||||
notifier.notifyBegin(listener, response);
|
||||
notifier.notifyHeaders(listener, response);
|
||||
if (response instanceof ContentResponse)
|
||||
notifier.notifyContent(listener, response, ByteBuffer.wrap(((ContentResponse)response).content()));
|
||||
notifier.notifySuccess(listener, response);
|
||||
conversation.complete();
|
||||
notifier.notifyComplete(listener, new Result(request, response));
|
||||
}
|
||||
|
||||
private List<WWWAuthenticate> parseWWWAuthenticate(Response response)
|
||||
{
|
||||
// TODO: these should be ordered by strength
|
||||
List<WWWAuthenticate> result = new ArrayList<>();
|
||||
List<String> values = Collections.list(response.headers().getValues(HttpHeader.WWW_AUTHENTICATE.asString()));
|
||||
for (String value : values)
|
||||
{
|
||||
Matcher matcher = WWW_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);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private class WWWAuthenticate
|
||||
{
|
||||
private final String value;
|
||||
private final String type;
|
||||
private final String realm;
|
||||
private final String params;
|
||||
|
||||
public WWWAuthenticate(String type, String realm, String params)
|
||||
public WWWAuthenticate(String value, String type, String realm)
|
||||
{
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
this.realm = realm;
|
||||
this.params = params;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.eclipse.jetty.client.api.AuthenticationStore;
|
|||
public class HttpAuthenticationStore implements AuthenticationStore
|
||||
{
|
||||
private final List<Authentication> authentications = new CopyOnWriteArrayList<>();
|
||||
private final Map<String, Authentication> results = new ConcurrentHashMap<>();
|
||||
private final Map<String, Authentication.Result> results = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void addAuthentication(Authentication authentication)
|
||||
|
@ -57,7 +57,7 @@ public class HttpAuthenticationStore implements AuthenticationStore
|
|||
@Override
|
||||
public void addAuthenticationResult(Authentication.Result result)
|
||||
{
|
||||
results.put(result.getURI(), result.getAuthentication());
|
||||
results.put(result.getURI(), result);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -67,10 +67,10 @@ public class HttpAuthenticationStore implements AuthenticationStore
|
|||
}
|
||||
|
||||
@Override
|
||||
public Authentication findAuthenticationResult(String uri)
|
||||
public Authentication.Result findAuthenticationResult(String uri)
|
||||
{
|
||||
// TODO: I should match the longest URI
|
||||
for (Map.Entry<String, Authentication> entry : results.entrySet())
|
||||
for (Map.Entry<String, Authentication.Result> entry : results.entrySet())
|
||||
{
|
||||
if (uri.startsWith(entry.getKey()))
|
||||
return entry.getValue();
|
||||
|
|
|
@ -161,9 +161,9 @@ public class HttpConnection extends AbstractConnection implements Connection
|
|||
request.header(HttpHeader.COOKIE.asString(), cookieString.toString());
|
||||
|
||||
// Authorization
|
||||
Authentication authentication = client.getAuthenticationStore().findAuthenticationResult(request.uri());
|
||||
if (authentication != null)
|
||||
authentication.authenticate(request);
|
||||
Authentication.Result authnResult = client.getAuthenticationStore().findAuthenticationResult(request.uri());
|
||||
if (authnResult != null)
|
||||
authnResult.apply(request);
|
||||
|
||||
// TODO: decoder headers
|
||||
|
||||
|
|
|
@ -142,12 +142,17 @@ public class HttpSender
|
|||
header = BufferUtil.EMPTY_BUFFER;
|
||||
if (chunk == null)
|
||||
chunk = BufferUtil.EMPTY_BUFFER;
|
||||
LOG.debug("Writing {} {} {}", header, chunk, content);
|
||||
endPoint.write(null, callback, header, chunk, content);
|
||||
if (callback.pending())
|
||||
{
|
||||
LOG.debug("Write incomplete {} {} {}", header, chunk, content);
|
||||
return;
|
||||
}
|
||||
|
||||
if (callback.completed())
|
||||
{
|
||||
LOG.debug("Write complete {} {} {}", header, chunk, content);
|
||||
if (!committed)
|
||||
committed(request);
|
||||
|
||||
|
@ -234,7 +239,7 @@ public class HttpSender
|
|||
// Notify after
|
||||
HttpExchange exchange = connection.getExchange();
|
||||
Request request = exchange.request();
|
||||
LOG.debug("Failed {}", request);
|
||||
LOG.debug("Failed {} {}", request, failure);
|
||||
|
||||
boolean exchangeCompleted = exchange.requestComplete(false);
|
||||
if (!exchangeCompleted && !committed)
|
||||
|
|
|
@ -18,31 +18,18 @@
|
|||
|
||||
package org.eclipse.jetty.client.api;
|
||||
|
||||
import org.eclipse.jetty.util.Attributes;
|
||||
|
||||
public interface Authentication
|
||||
{
|
||||
boolean matches(String type, String uri, String realm);
|
||||
|
||||
void authenticate(Request request);
|
||||
Result authenticate(Request request, ContentResponse response, String wwwAuthenticate, Attributes context);
|
||||
|
||||
public static class Result
|
||||
public static interface Result
|
||||
{
|
||||
private final String uri;
|
||||
private final Authentication authentication;
|
||||
String getURI();
|
||||
|
||||
public Result(String uri, Authentication authentication)
|
||||
{
|
||||
this.uri = uri;
|
||||
this.authentication = authentication;
|
||||
}
|
||||
|
||||
public String getURI()
|
||||
{
|
||||
return uri;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication()
|
||||
{
|
||||
return authentication;
|
||||
}
|
||||
void apply(Request request);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,5 +30,5 @@ public interface AuthenticationStore
|
|||
|
||||
public void removeAuthenticationResults();
|
||||
|
||||
public Authentication findAuthenticationResult(String uri);
|
||||
public Authentication.Result findAuthenticationResult(String uri);
|
||||
}
|
||||
|
|
|
@ -22,8 +22,10 @@ import java.io.UnsupportedEncodingException;
|
|||
import java.nio.charset.UnsupportedCharsetException;
|
||||
|
||||
import org.eclipse.jetty.client.api.Authentication;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.util.Attributes;
|
||||
import org.eclipse.jetty.util.B64Code;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
|
||||
|
@ -55,17 +57,47 @@ public class BasicAuthentication implements Authentication
|
|||
}
|
||||
|
||||
@Override
|
||||
public void authenticate(Request request)
|
||||
public Result authenticate(Request request, ContentResponse response, String wwwAuthenticate, Attributes context)
|
||||
{
|
||||
String encoding = StringUtil.__ISO_8859_1;
|
||||
try
|
||||
{
|
||||
String value = "Basic " + B64Code.encode(user + ":" + password, encoding);
|
||||
request.header(HttpHeader.AUTHORIZATION.asString(), value);
|
||||
return new BasicResult(request.uri(), value);
|
||||
}
|
||||
catch (UnsupportedEncodingException x)
|
||||
{
|
||||
throw new UnsupportedCharsetException(encoding);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BasicResult implements Result
|
||||
{
|
||||
private final String uri;
|
||||
private final String value;
|
||||
|
||||
public BasicResult(String uri, String value)
|
||||
{
|
||||
this.uri = uri;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getURI()
|
||||
{
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(Request request)
|
||||
{
|
||||
request.header(HttpHeader.AUTHORIZATION.asString(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("Basic authentication result for %s", uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,268 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2012 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.util;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jetty.client.api.Authentication;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.util.Attributes;
|
||||
import org.eclipse.jetty.util.TypeUtil;
|
||||
|
||||
public class DigestAuthentication implements Authentication
|
||||
{
|
||||
private static final Pattern PARAM_PATTERN = Pattern.compile("([^=]+)=(.*)");
|
||||
|
||||
private final String uri;
|
||||
private final String realm;
|
||||
private final String user;
|
||||
private final String password;
|
||||
|
||||
public DigestAuthentication(String uri, String realm, String user, String password)
|
||||
{
|
||||
this.uri = uri;
|
||||
this.realm = realm;
|
||||
this.user = user;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String type, String uri, String realm)
|
||||
{
|
||||
if (!"digest".equalsIgnoreCase(type))
|
||||
return false;
|
||||
|
||||
if (!uri.startsWith(this.uri))
|
||||
return false;
|
||||
|
||||
return this.realm.equals(realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result authenticate(Request request, ContentResponse response, String wwwAuthenticate, Attributes context)
|
||||
{
|
||||
// Avoid case sensitivity problems on the 'D' character
|
||||
String type = "igest";
|
||||
wwwAuthenticate = wwwAuthenticate.substring(wwwAuthenticate.indexOf(type) + type.length());
|
||||
|
||||
Map<String, String> params = parseParams(wwwAuthenticate);
|
||||
String nonce = params.get("nonce");
|
||||
if (nonce == null || nonce.length() == 0)
|
||||
return null;
|
||||
String opaque = params.get("opaque");
|
||||
String algorithm = params.get("algorithm");
|
||||
if (algorithm == null)
|
||||
algorithm = "MD5";
|
||||
MessageDigest digester = getMessageDigest(algorithm);
|
||||
if (digester == null)
|
||||
return null;
|
||||
String serverQOP = params.get("qop");
|
||||
String clientQOP = null;
|
||||
if (serverQOP != null)
|
||||
{
|
||||
List<String> serverQOPValues = Arrays.asList(serverQOP.split(","));
|
||||
if (serverQOPValues.contains("auth"))
|
||||
clientQOP = "auth";
|
||||
else if (serverQOPValues.contains("auth-int"))
|
||||
clientQOP = "auth-int";
|
||||
}
|
||||
|
||||
return new DigestResult(request.uri(), response.content(), realm, user, password, algorithm, nonce, clientQOP, opaque);
|
||||
}
|
||||
|
||||
private Map<String, String> parseParams(String wwwAuthenticate)
|
||||
{
|
||||
Map<String, String> result = new HashMap<>();
|
||||
List<String> parts = splitParams(wwwAuthenticate);
|
||||
for (String part : parts)
|
||||
{
|
||||
Matcher matcher = PARAM_PATTERN.matcher(part);
|
||||
if (matcher.matches())
|
||||
{
|
||||
String name = matcher.group(1).trim().toLowerCase();
|
||||
String value = matcher.group(2).trim();
|
||||
if (value.startsWith("\"") && value.endsWith("\""))
|
||||
value = value.substring(1, value.length() - 1);
|
||||
result.put(name, value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<String> splitParams(String paramString)
|
||||
{
|
||||
List<String> result = new ArrayList<>();
|
||||
int start = 0;
|
||||
for (int i = 0; i < paramString.length(); ++i)
|
||||
{
|
||||
int quotes = 0;
|
||||
char ch = paramString.charAt(i);
|
||||
switch (ch)
|
||||
{
|
||||
case '\\':
|
||||
++i;
|
||||
break;
|
||||
case '"':
|
||||
++quotes;
|
||||
break;
|
||||
case ',':
|
||||
if (quotes % 2 == 0)
|
||||
{
|
||||
result.add(paramString.substring(start, i).trim());
|
||||
start = i + 1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
result.add(paramString.substring(start, paramString.length()).trim());
|
||||
return result;
|
||||
}
|
||||
|
||||
private MessageDigest getMessageDigest(String algorithm)
|
||||
{
|
||||
try
|
||||
{
|
||||
return MessageDigest.getInstance(algorithm);
|
||||
}
|
||||
catch (NoSuchAlgorithmException x)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class DigestResult implements Result
|
||||
{
|
||||
private final AtomicInteger nonceCount = new AtomicInteger();
|
||||
private final String uri;
|
||||
private final byte[] content;
|
||||
private final String realm;
|
||||
private final String user;
|
||||
private final String password;
|
||||
private final String algorithm;
|
||||
private final String nonce;
|
||||
private final String qop;
|
||||
private final String opaque;
|
||||
|
||||
public DigestResult(String uri, byte[] content, String realm, String user, String password, String algorithm, String nonce, String qop, String opaque)
|
||||
{
|
||||
this.uri = uri;
|
||||
this.content = content;
|
||||
this.realm = realm;
|
||||
this.user = user;
|
||||
this.password = password;
|
||||
this.algorithm = algorithm;
|
||||
this.nonce = nonce;
|
||||
this.qop = qop;
|
||||
this.opaque = opaque;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getURI()
|
||||
{
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(Request request)
|
||||
{
|
||||
MessageDigest digester = getMessageDigest(algorithm);
|
||||
if (digester == null)
|
||||
return;
|
||||
|
||||
Charset charset = Charset.forName("ISO-8859-1");
|
||||
String A1 = user + ":" + realm + ":" + password;
|
||||
String hashA1 = toHexString(digester.digest(A1.getBytes(charset)));
|
||||
|
||||
String A2 = request.method().asString() + ":" + request.uri();
|
||||
if ("auth-int".equals(qop))
|
||||
A2 += ":" + toHexString(digester.digest(content));
|
||||
String hashA2 = toHexString(digester.digest(A2.getBytes(charset)));
|
||||
|
||||
String nonceCount;
|
||||
String clientNonce;
|
||||
String A3;
|
||||
if (qop != null)
|
||||
{
|
||||
nonceCount = nextNonceCount();
|
||||
clientNonce = newClientNonce();
|
||||
A3 = hashA1 + ":" + nonce + ":" + nonceCount + ":" + clientNonce + ":" + qop + ":" + hashA2;
|
||||
}
|
||||
else
|
||||
{
|
||||
nonceCount = null;
|
||||
clientNonce = null;
|
||||
A3 = hashA1 + ":" + nonce + ":" + hashA2;
|
||||
}
|
||||
String hashA3 = toHexString(digester.digest(A3.getBytes(charset)));
|
||||
|
||||
StringBuilder value = new StringBuilder("Digest");
|
||||
value.append(" username=\"").append(user).append("\"");
|
||||
value.append(", realm=\"").append(realm).append("\"");
|
||||
value.append(", nonce=\"").append(nonce).append("\"");
|
||||
if (opaque != null)
|
||||
value.append(", opaque=\"").append(opaque).append("\"");
|
||||
value.append(", algorithm=\"").append(algorithm).append("\"");
|
||||
value.append(", uri=\"").append(request.uri()).append("\"");
|
||||
if (qop != null)
|
||||
{
|
||||
value.append(", qop=\"").append(qop).append("\"");
|
||||
value.append(", nc=\"").append(nonceCount).append("\"");
|
||||
value.append(", cnonce=\"").append(clientNonce).append("\"");
|
||||
}
|
||||
value.append(", response=\"").append(hashA3).append("\"");
|
||||
|
||||
request.header(HttpHeader.AUTHORIZATION.asString(), value.toString());
|
||||
}
|
||||
|
||||
private String nextNonceCount()
|
||||
{
|
||||
String padding = "00000000";
|
||||
String next = Integer.toHexString(nonceCount.incrementAndGet()).toLowerCase();
|
||||
return padding.substring(0, padding.length() - next.length()) + next;
|
||||
}
|
||||
|
||||
private String newClientNonce()
|
||||
{
|
||||
Random random = new Random();
|
||||
byte[] bytes = new byte[8];
|
||||
random.nextBytes(bytes);
|
||||
return toHexString(bytes);
|
||||
}
|
||||
|
||||
private String toHexString(byte[] bytes)
|
||||
{
|
||||
return TypeUtil.toHexString(bytes).toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,7 +49,8 @@ public class AbstractHttpClientServerTest
|
|||
|
||||
public void start(Handler handler) throws Exception
|
||||
{
|
||||
server = new Server();
|
||||
if (server == null)
|
||||
server = new Server();
|
||||
connector = new SelectChannelConnector(server);
|
||||
server.addConnector(connector);
|
||||
server.setHandler(handler);
|
||||
|
@ -62,11 +63,12 @@ public class AbstractHttpClientServerTest
|
|||
}
|
||||
|
||||
@After
|
||||
public void destroy() throws Exception
|
||||
public void dispose() throws Exception
|
||||
{
|
||||
if (client != null)
|
||||
client.stop();
|
||||
if (server != null)
|
||||
server.stop();
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,33 +18,87 @@
|
|||
|
||||
package org.eclipse.jetty.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.File;
|
||||
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.Authentication;
|
||||
import org.eclipse.jetty.client.api.AuthenticationStore;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.BasicAuthentication;
|
||||
import org.eclipse.jetty.client.util.DigestAuthentication;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.eclipse.jetty.util.B64Code;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.security.Authenticator;
|
||||
import org.eclipse.jetty.security.ConstraintMapping;
|
||||
import org.eclipse.jetty.security.ConstraintSecurityHandler;
|
||||
import org.eclipse.jetty.security.HashLoginService;
|
||||
import org.eclipse.jetty.security.LoginService;
|
||||
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
|
||||
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.util.security.Constraint;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
|
||||
{
|
||||
@Test
|
||||
public void test_BasicAuthentication_WithChallenge() throws Exception
|
||||
{
|
||||
start(new BasicAuthenticationHandler());
|
||||
private String realm = "TestRealm";
|
||||
|
||||
public void startBasic(Handler handler) throws Exception
|
||||
{
|
||||
start(new BasicAuthenticator(), handler);
|
||||
}
|
||||
|
||||
public void startDigest(Handler handler) throws Exception
|
||||
{
|
||||
start(new DigestAuthenticator(), handler);
|
||||
}
|
||||
|
||||
private void start(Authenticator authenticator, Handler handler) throws Exception
|
||||
{
|
||||
server = new Server();
|
||||
File realmFile = MavenTestingUtils.getTestResourceFile("realm.properties");
|
||||
LoginService loginService = new HashLoginService(realm, realmFile.getAbsolutePath());
|
||||
server.addBean(loginService);
|
||||
|
||||
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
|
||||
|
||||
Constraint constraint = new Constraint();
|
||||
constraint.setAuthenticate(true);
|
||||
constraint.setRoles(new String[]{"*"});
|
||||
ConstraintMapping mapping = new ConstraintMapping();
|
||||
mapping.setPathSpec("/*");
|
||||
mapping.setConstraint(constraint);
|
||||
|
||||
securityHandler.addConstraintMapping(mapping);
|
||||
securityHandler.setAuthenticator(authenticator);
|
||||
securityHandler.setLoginService(loginService);
|
||||
securityHandler.setStrict(false);
|
||||
|
||||
securityHandler.setHandler(handler);
|
||||
start(securityHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_BasicAuthentication() throws Exception
|
||||
{
|
||||
startBasic(new EmptyHandler());
|
||||
test_Authentication(new BasicAuthentication("http://localhost:" + connector.getLocalPort(), realm, "basic", "basic"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_DigestAuthentication() throws Exception
|
||||
{
|
||||
startDigest(new EmptyHandler());
|
||||
test_Authentication(new DigestAuthentication("http://localhost:" + connector.getLocalPort(), realm, "digest", "digest"));
|
||||
}
|
||||
|
||||
private void test_Authentication(Authentication authentication) throws Exception
|
||||
{
|
||||
AuthenticationStore authenticationStore = client.getAuthenticationStore();
|
||||
String realm = "test";
|
||||
|
||||
final AtomicInteger requests = new AtomicInteger();
|
||||
Request.Listener.Adapter requestListener = new Request.Listener.Adapter()
|
||||
|
@ -58,20 +112,15 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
|
|||
client.getRequestListeners().add(requestListener);
|
||||
|
||||
// Request without Authentication causes a 401
|
||||
Request request = client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/test")
|
||||
.param("type", "Basic")
|
||||
.param("realm", realm);
|
||||
ContentResponse response = request.send().get(5, TimeUnit.SECONDS);
|
||||
Request request = client.newRequest("localhost", connector.getLocalPort()).path("/test");
|
||||
ContentResponse response = request.send().get(555, TimeUnit.SECONDS);
|
||||
Assert.assertNotNull(response);
|
||||
Assert.assertEquals(401, response.status());
|
||||
Assert.assertEquals(1, requests.get());
|
||||
client.getRequestListeners().remove(requestListener);
|
||||
requests.set(0);
|
||||
|
||||
String user = "jetty";
|
||||
String password = "rocks";
|
||||
authenticationStore.addAuthentication(new BasicAuthentication("http://localhost:" + connector.getLocalPort(), realm, user, password));
|
||||
authenticationStore.addAuthentication(authentication);
|
||||
|
||||
requestListener = new Request.Listener.Adapter()
|
||||
{
|
||||
|
@ -84,8 +133,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
|
|||
client.getRequestListeners().add(requestListener);
|
||||
|
||||
// Request with authentication causes a 401 (no previous successful authentication) + 200
|
||||
request.param("user", user).param("password", password);
|
||||
response = request.send().get(5, TimeUnit.SECONDS);
|
||||
response = request.send().get(555, TimeUnit.SECONDS);
|
||||
Assert.assertNotNull(response);
|
||||
Assert.assertEquals(200, response.status());
|
||||
Assert.assertEquals(2, requests.get());
|
||||
|
@ -112,58 +160,4 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
|
|||
client.getRequestListeners().remove(requestListener);
|
||||
requests.set(0);
|
||||
}
|
||||
|
||||
private class BasicAuthenticationHandler extends AbstractHandler
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
try
|
||||
{
|
||||
String type = request.getParameter("type");
|
||||
String authorization = request.getHeader(HttpHeader.AUTHORIZATION.asString());
|
||||
if (authorization == null)
|
||||
{
|
||||
String realm = request.getParameter("realm");
|
||||
response.setStatus(401);
|
||||
switch (type)
|
||||
{
|
||||
case "Basic":
|
||||
{
|
||||
response.setHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case "Basic":
|
||||
{
|
||||
String user = request.getParameter("user");
|
||||
String password = request.getParameter("password");
|
||||
String expected = "Basic " + B64Code.encode(user + ":" + password);
|
||||
if (!expected.equals(authorization))
|
||||
throw new IOException(expected + " != " + authorization);
|
||||
IO.copy(request.getInputStream(), response.getOutputStream());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# Format is <user>:<password>,<roles>
|
||||
basic:basic
|
||||
digest:digest
|
Loading…
Reference in New Issue