Merge branch 'master' into javawebsocket-jsr
This commit is contained in:
commit
4dea484853
|
@ -0,0 +1,4 @@
|
|||
language: java
|
||||
jdk:
|
||||
- openjdk7
|
||||
- oraclejdk7
|
|
@ -1,5 +1,6 @@
|
|||
This is a source checkout of the Jetty webserver.
|
||||
|
||||
|
||||
To build, use:
|
||||
|
||||
mvn clean install
|
||||
|
|
|
@ -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<WWWAuthenticate> wwwAuthenticates = parseWWWAuthenticate(response);
|
||||
if (wwwAuthenticates.isEmpty())
|
||||
HttpHeader header = getAuthenticateHeader();
|
||||
List<Authentication.HeaderInfo> 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<WWWAuthenticate> parseWWWAuthenticate(Response response)
|
||||
private List<Authentication.HeaderInfo> parseAuthenticateHeader(Response response, HttpHeader header)
|
||||
{
|
||||
// TODO: these should be ordered by strength
|
||||
List<WWWAuthenticate> result = new ArrayList<>();
|
||||
List<String> values = Collections.list(response.getHeaders().getValues(HttpHeader.WWW_AUTHENTICATE.asString()));
|
||||
List<Authentication.HeaderInfo> result = new ArrayList<>();
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
@ -965,6 +966,7 @@ public class HttpClient extends ContainerLifeCycle
|
|||
engine.setUseClientMode(true);
|
||||
|
||||
SslConnection sslConnection = newSslConnection(HttpClient.this, endPoint, engine);
|
||||
sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed());
|
||||
EndPoint appEndPoint = sslConnection.getDecryptedEndPoint();
|
||||
HttpConnection connection = newHttpConnection(HttpClient.this, appEndPoint, destination);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.util.Attributes;
|
||||
|
||||
/**
|
||||
* {@link Authentication} represents a mechanism to authenticate requests for protected resources.
|
||||
* <p />
|
||||
* {@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.
|
||||
* <p />
|
||||
* 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)}.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<String, String> params = parseParams(wwwAuthenticate);
|
||||
Map<String, String> 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<String, String> parseParams(String wwwAuthenticate)
|
||||
private Map<String, String> parseParameters(String wwwAuthenticate)
|
||||
{
|
||||
Map<String, String> result = new HashMap<>();
|
||||
List<String> 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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,369 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.ssl;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLServerSocket;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.FutureResponseListener;
|
||||
import org.eclipse.jetty.http.HttpScheme;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SslBytesClientTest extends SslBytesTest
|
||||
{
|
||||
private ExecutorService threadPool;
|
||||
private HttpClient client;
|
||||
private SslContextFactory sslContextFactory;
|
||||
private SSLServerSocket acceptor;
|
||||
private SimpleProxy proxy;
|
||||
|
||||
@Before
|
||||
public void init() throws Exception
|
||||
{
|
||||
threadPool = Executors.newCachedThreadPool();
|
||||
|
||||
client = new HttpClient(new SslContextFactory(true));
|
||||
client.setMaxConnectionsPerDestination(1);
|
||||
File keyStore = MavenTestingUtils.getTestResourceFile("keystore.jks");
|
||||
sslContextFactory = client.getSslContextFactory();
|
||||
sslContextFactory.setKeyStorePath(keyStore.getAbsolutePath());
|
||||
sslContextFactory.setKeyStorePassword("storepwd");
|
||||
client.start();
|
||||
|
||||
SSLContext sslContext = sslContextFactory.getSslContext();
|
||||
acceptor = (SSLServerSocket)sslContext.getServerSocketFactory().createServerSocket(43191);
|
||||
|
||||
int serverPort = acceptor.getLocalPort();
|
||||
|
||||
proxy = new SimpleProxy(threadPool, "localhost", serverPort);
|
||||
proxy.start();
|
||||
logger.info(":{} <==> :{}", proxy.getPort(), serverPort);
|
||||
}
|
||||
|
||||
@After
|
||||
public void destroy() throws Exception
|
||||
{
|
||||
if (acceptor != null)
|
||||
acceptor.close();
|
||||
if (proxy != null)
|
||||
proxy.stop();
|
||||
if (client != null)
|
||||
client.stop();
|
||||
if (threadPool != null)
|
||||
threadPool.shutdownNow();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandshake() throws Exception
|
||||
{
|
||||
Request request = client.newRequest("localhost", proxy.getPort());
|
||||
FutureResponseListener listener = new FutureResponseListener(request);
|
||||
request.scheme(HttpScheme.HTTPS.asString()).send(listener);
|
||||
|
||||
Assert.assertTrue(proxy.awaitClient(5, TimeUnit.SECONDS));
|
||||
|
||||
final SSLSocket server = (SSLSocket)acceptor.accept();
|
||||
server.setUseClientMode(false);
|
||||
|
||||
Future<Object> handshake = threadPool.submit(new Callable<Object>()
|
||||
{
|
||||
public Object call() throws Exception
|
||||
{
|
||||
server.startHandshake();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Client Hello
|
||||
TLSRecord record = proxy.readFromClient();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToServer(record);
|
||||
|
||||
// Server Hello + Certificate + Server Done
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToClient(record);
|
||||
|
||||
// Client Key Exchange
|
||||
record = proxy.readFromClient();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToServer(record);
|
||||
|
||||
// Change Cipher Spec
|
||||
record = proxy.readFromClient();
|
||||
Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType());
|
||||
proxy.flushToServer(record);
|
||||
|
||||
// Client Done
|
||||
record = proxy.readFromClient();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToServer(record);
|
||||
|
||||
// Change Cipher Spec
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType());
|
||||
proxy.flushToClient(record);
|
||||
|
||||
// Server Done
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToClient(record);
|
||||
|
||||
Assert.assertNull(handshake.get(5, TimeUnit.SECONDS));
|
||||
|
||||
SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow();
|
||||
// Read request
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(server.getInputStream(), "UTF-8"));
|
||||
String line = reader.readLine();
|
||||
Assert.assertTrue(line.startsWith("GET"));
|
||||
while (line.length() > 0)
|
||||
line = reader.readLine();
|
||||
|
||||
// Write response
|
||||
OutputStream output = server.getOutputStream();
|
||||
output.write(("HTTP/1.1 200 OK\r\n" +
|
||||
"Content-Length: 0\r\n" +
|
||||
"\r\n").getBytes("UTF-8"));
|
||||
output.flush();
|
||||
Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS));
|
||||
|
||||
ContentResponse response = listener.get(5, TimeUnit.SECONDS);
|
||||
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
|
||||
server.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServerRenegotiation() throws Exception
|
||||
{
|
||||
Request request = client.newRequest("localhost", proxy.getPort());
|
||||
FutureResponseListener listener = new FutureResponseListener(request);
|
||||
request.scheme(HttpScheme.HTTPS.asString()).send(listener);
|
||||
|
||||
Assert.assertTrue(proxy.awaitClient(5, TimeUnit.SECONDS));
|
||||
|
||||
final SSLSocket server = (SSLSocket)acceptor.accept();
|
||||
server.setUseClientMode(false);
|
||||
|
||||
Future<Object> handshake = threadPool.submit(new Callable<Object>()
|
||||
{
|
||||
public Object call() throws Exception
|
||||
{
|
||||
server.startHandshake();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow();
|
||||
Assert.assertNull(handshake.get(5, TimeUnit.SECONDS));
|
||||
|
||||
// Read request
|
||||
InputStream serverInput = server.getInputStream();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput, "UTF-8"));
|
||||
String line = reader.readLine();
|
||||
Assert.assertTrue(line.startsWith("GET"));
|
||||
while (line.length() > 0)
|
||||
line = reader.readLine();
|
||||
|
||||
OutputStream serverOutput = server.getOutputStream();
|
||||
byte[] data1 = new byte[1024];
|
||||
Arrays.fill(data1, (byte)'X');
|
||||
String content1 = new String(data1, "UTF-8");
|
||||
byte[] data2 = new byte[1024];
|
||||
Arrays.fill(data2, (byte)'Y');
|
||||
final String content2 = new String(data2, "UTF-8");
|
||||
// Write first part of the response
|
||||
serverOutput.write(("HTTP/1.1 200 OK\r\n" +
|
||||
"Content-Type: text/plain\r\n" +
|
||||
"Content-Length: " + (content1.length() + content2.length()) + "\r\n" +
|
||||
"\r\n" +
|
||||
content1).getBytes("UTF-8"));
|
||||
serverOutput.flush();
|
||||
Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS));
|
||||
|
||||
// Renegotiate
|
||||
Future<Object> renegotiation = threadPool.submit(new Callable<Object>()
|
||||
{
|
||||
public Object call() throws Exception
|
||||
{
|
||||
server.startHandshake();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Renegotiation Handshake
|
||||
TLSRecord record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToClient(record);
|
||||
|
||||
// Renegotiation Handshake
|
||||
record = proxy.readFromClient();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToServer(record);
|
||||
|
||||
// Trigger a read to have the server write the final renegotiation steps
|
||||
server.setSoTimeout(100);
|
||||
try
|
||||
{
|
||||
serverInput.read();
|
||||
Assert.fail();
|
||||
}
|
||||
catch (SocketTimeoutException x)
|
||||
{
|
||||
// Expected
|
||||
}
|
||||
|
||||
// Renegotiation Handshake
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToClient(record);
|
||||
|
||||
// Renegotiation Change Cipher
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType());
|
||||
proxy.flushToClient(record);
|
||||
|
||||
// Renegotiation Handshake
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToClient(record);
|
||||
|
||||
// Renegotiation Change Cipher
|
||||
record = proxy.readFromClient();
|
||||
Assert.assertEquals(TLSRecord.Type.CHANGE_CIPHER_SPEC, record.getType());
|
||||
proxy.flushToServer(record);
|
||||
|
||||
// Renegotiation Handshake
|
||||
record = proxy.readFromClient();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToServer(record);
|
||||
|
||||
Assert.assertNull(renegotiation.get(5, TimeUnit.SECONDS));
|
||||
|
||||
// Complete the response
|
||||
automaticProxyFlow = proxy.startAutomaticFlow();
|
||||
serverOutput.write(data2);
|
||||
serverOutput.flush();
|
||||
Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS));
|
||||
|
||||
ContentResponse response = listener.get(5, TimeUnit.SECONDS);
|
||||
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
Assert.assertEquals(data1.length + data2.length, response.getContent().length);
|
||||
|
||||
server.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServerRenegotiationWhenRenegotiationIsForbidden() throws Exception
|
||||
{
|
||||
sslContextFactory.setRenegotiationAllowed(false);
|
||||
|
||||
Request request = client.newRequest("localhost", proxy.getPort());
|
||||
FutureResponseListener listener = new FutureResponseListener(request);
|
||||
request.scheme(HttpScheme.HTTPS.asString()).send(listener);
|
||||
|
||||
Assert.assertTrue(proxy.awaitClient(5, TimeUnit.SECONDS));
|
||||
|
||||
final SSLSocket server = (SSLSocket)acceptor.accept();
|
||||
server.setUseClientMode(false);
|
||||
|
||||
Future<Object> handshake = threadPool.submit(new Callable<Object>()
|
||||
{
|
||||
public Object call() throws Exception
|
||||
{
|
||||
server.startHandshake();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow();
|
||||
Assert.assertNull(handshake.get(5, TimeUnit.SECONDS));
|
||||
|
||||
// Read request
|
||||
InputStream serverInput = server.getInputStream();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput, "UTF-8"));
|
||||
String line = reader.readLine();
|
||||
Assert.assertTrue(line.startsWith("GET"));
|
||||
while (line.length() > 0)
|
||||
line = reader.readLine();
|
||||
|
||||
OutputStream serverOutput = server.getOutputStream();
|
||||
byte[] data1 = new byte[1024];
|
||||
Arrays.fill(data1, (byte)'X');
|
||||
String content1 = new String(data1, "UTF-8");
|
||||
byte[] data2 = new byte[1024];
|
||||
Arrays.fill(data2, (byte)'Y');
|
||||
final String content2 = new String(data2, "UTF-8");
|
||||
// Write first part of the response
|
||||
serverOutput.write(("HTTP/1.1 200 OK\r\n" +
|
||||
"Content-Type: text/plain\r\n" +
|
||||
"Content-Length: " + (content1.length() + content2.length()) + "\r\n" +
|
||||
"\r\n" +
|
||||
content1).getBytes("UTF-8"));
|
||||
serverOutput.flush();
|
||||
Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS));
|
||||
|
||||
// Renegotiate
|
||||
threadPool.submit(new Callable<Object>()
|
||||
{
|
||||
public Object call() throws Exception
|
||||
{
|
||||
server.startHandshake();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Renegotiation Handshake
|
||||
TLSRecord record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToClient(record);
|
||||
|
||||
record = proxy.readFromClient();
|
||||
Assert.assertEquals(TLSRecord.Type.ALERT, record.getType());
|
||||
proxy.flushToServer(record);
|
||||
|
||||
record = proxy.readFromClient();
|
||||
Assert.assertNull(record);
|
||||
proxy.flushToServer(record);
|
||||
|
||||
server.close();
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server.ssl;
|
||||
package org.eclipse.jetty.client.ssl;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.EOFException;
|
||||
|
@ -39,7 +39,6 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
@ -85,7 +84,7 @@ public class SslBytesServerTest extends SslBytesTest
|
|||
private final int idleTimeout = 2000;
|
||||
private ExecutorService threadPool;
|
||||
private Server server;
|
||||
private int serverPort;
|
||||
private SslContextFactory sslContextFactory;
|
||||
private SSLContext sslContext;
|
||||
private SimpleProxy proxy;
|
||||
|
||||
|
@ -95,11 +94,10 @@ public class SslBytesServerTest extends SslBytesTest
|
|||
threadPool = Executors.newCachedThreadPool();
|
||||
server = new Server();
|
||||
|
||||
File keyStore = MavenTestingUtils.getTestResourceFile("keystore");
|
||||
SslContextFactory sslContextFactory = new SslContextFactory();
|
||||
File keyStore = MavenTestingUtils.getTestResourceFile("keystore.jks");
|
||||
sslContextFactory = new SslContextFactory();
|
||||
sslContextFactory.setKeyStorePath(keyStore.getAbsolutePath());
|
||||
sslContextFactory.setKeyStorePassword("storepwd");
|
||||
sslContextFactory.setKeyManagerPassword("keypwd");
|
||||
|
||||
HttpConnectionFactory httpFactory = new HttpConnectionFactory()
|
||||
{
|
||||
|
@ -205,7 +203,7 @@ public class SslBytesServerTest extends SslBytesTest
|
|||
}
|
||||
});
|
||||
server.start();
|
||||
serverPort = connector.getLocalPort();
|
||||
int serverPort = connector.getLocalPort();
|
||||
|
||||
sslContext = sslContextFactory.getSslContext();
|
||||
|
||||
|
@ -866,7 +864,62 @@ public class SslBytesServerTest extends SslBytesTest
|
|||
// Close the raw socket, this generates a truncation attack
|
||||
proxy.flushToServer(null);
|
||||
|
||||
// Expect raw close from server
|
||||
// Expect alert + raw close from server
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.ALERT, record.getType());
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertNull(String.valueOf(record), record);
|
||||
proxy.flushToClient(record);
|
||||
|
||||
// Check that we did not spin
|
||||
TimeUnit.MILLISECONDS.sleep(500);
|
||||
Assert.assertThat(sslFills.get(), Matchers.lessThan(20));
|
||||
Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20));
|
||||
Assert.assertThat(httpParses.get(), Matchers.lessThan(20));
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestWithImmediateRawClose() throws Exception
|
||||
{
|
||||
final SSLSocket client = newClient();
|
||||
|
||||
SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow();
|
||||
client.startHandshake();
|
||||
Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS));
|
||||
|
||||
Future<Object> request = threadPool.submit(new Callable<Object>()
|
||||
{
|
||||
@Override
|
||||
public Object call() throws Exception
|
||||
{
|
||||
OutputStream clientOutput = client.getOutputStream();
|
||||
clientOutput.write(("" +
|
||||
"GET / HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"\r\n").getBytes("UTF-8"));
|
||||
clientOutput.flush();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Application data
|
||||
TLSRecord record = proxy.readFromClient();
|
||||
Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType());
|
||||
proxy.flushToServer(record, 0);
|
||||
// Close the raw socket, this generates a truncation attack
|
||||
proxy.flushToServer(null);
|
||||
Assert.assertNull(request.get(5, TimeUnit.SECONDS));
|
||||
|
||||
// Application data
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.APPLICATION, record.getType());
|
||||
proxy.flushToClient(record);
|
||||
|
||||
// Expect alert + raw close from server
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.ALERT, record.getType());
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertNull(String.valueOf(record), record);
|
||||
proxy.flushToClient(record);
|
||||
|
@ -1223,6 +1276,98 @@ public class SslBytesServerTest extends SslBytesTest
|
|||
closeClient(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestWithContentWithRenegotiationInMiddleOfContentWhenRenegotiationIsForbidden() throws Exception
|
||||
{
|
||||
assumeJavaVersionSupportsTLSRenegotiations();
|
||||
|
||||
sslContextFactory.setRenegotiationAllowed(false);
|
||||
|
||||
final SSLSocket client = newClient();
|
||||
final OutputStream clientOutput = client.getOutputStream();
|
||||
|
||||
SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow();
|
||||
client.startHandshake();
|
||||
Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS));
|
||||
|
||||
byte[] data1 = new byte[1024];
|
||||
Arrays.fill(data1, (byte)'X');
|
||||
String content1 = new String(data1, "UTF-8");
|
||||
byte[] data2 = new byte[1024];
|
||||
Arrays.fill(data2, (byte)'Y');
|
||||
final String content2 = new String(data2, "UTF-8");
|
||||
|
||||
// Write only part of the body
|
||||
automaticProxyFlow = proxy.startAutomaticFlow();
|
||||
clientOutput.write(("" +
|
||||
"POST / HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"Content-Type: text/plain\r\n" +
|
||||
"Content-Length: " + (content1.length() + content2.length()) + "\r\n" +
|
||||
"\r\n" +
|
||||
content1).getBytes("UTF-8"));
|
||||
clientOutput.flush();
|
||||
Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS));
|
||||
|
||||
// Renegotiate
|
||||
threadPool.submit(new Callable<Object>()
|
||||
{
|
||||
@Override
|
||||
public Object call() throws Exception
|
||||
{
|
||||
client.startHandshake();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Renegotiation Handshake
|
||||
TLSRecord record = proxy.readFromClient();
|
||||
Assert.assertEquals(TLSRecord.Type.HANDSHAKE, record.getType());
|
||||
proxy.flushToServer(record);
|
||||
|
||||
// Renegotiation now allowed, server has closed
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertEquals(TLSRecord.Type.ALERT, record.getType());
|
||||
proxy.flushToClient(record);
|
||||
|
||||
record = proxy.readFromServer();
|
||||
Assert.assertNull(record);
|
||||
|
||||
// Write the rest of the request
|
||||
threadPool.submit(new Callable<Object>()
|
||||
{
|
||||
@Override
|
||||
public Object call() throws Exception
|
||||
{
|
||||
clientOutput.write(content2.getBytes("UTF-8"));
|
||||
clientOutput.flush();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Trying to write more application data results in an exception since the server closed
|
||||
record = proxy.readFromClient();
|
||||
proxy.flushToServer(record);
|
||||
try
|
||||
{
|
||||
record = proxy.readFromClient();
|
||||
Assert.assertNotNull(record);
|
||||
proxy.flushToServer(record);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (IOException expected)
|
||||
{
|
||||
}
|
||||
|
||||
// Check that we did not spin
|
||||
TimeUnit.MILLISECONDS.sleep(500);
|
||||
Assert.assertThat(sslFills.get(), Matchers.lessThan(50));
|
||||
Assert.assertThat(sslFlushes.get(), Matchers.lessThan(20));
|
||||
Assert.assertThat(httpParses.get(), Matchers.lessThan(50));
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestWithBigContentWithRenegotiationInMiddleOfContent() throws Exception
|
||||
{
|
|
@ -16,7 +16,7 @@
|
|||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server.ssl;
|
||||
package org.eclipse.jetty.client.ssl;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
@ -115,8 +115,8 @@ public abstract class SslBytesTest
|
|||
|
||||
public void start() throws Exception
|
||||
{
|
||||
// serverSocket = new ServerSocket(5871);
|
||||
serverSocket = new ServerSocket(0);
|
||||
serverSocket = new ServerSocket(47009);
|
||||
// serverSocket = new ServerSocket(0);
|
||||
Thread acceptor = new Thread(this);
|
||||
acceptor.start();
|
||||
server = new Socket(serverHost, serverPort);
|
Binary file not shown.
|
@ -99,6 +99,7 @@ public class SslConnection extends AbstractConnection
|
|||
_decryptedEndPoint.getWriteFlusher().completeWrite();
|
||||
}
|
||||
};
|
||||
private boolean _renegotiationAllowed;
|
||||
|
||||
public SslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine sslEngine)
|
||||
{
|
||||
|
@ -137,6 +138,16 @@ public class SslConnection extends AbstractConnection
|
|||
return _decryptedEndPoint;
|
||||
}
|
||||
|
||||
public boolean isRenegotiationAllowed()
|
||||
{
|
||||
return _renegotiationAllowed;
|
||||
}
|
||||
|
||||
public void setRenegotiationAllowed(boolean renegotiationAllowed)
|
||||
{
|
||||
this._renegotiationAllowed = renegotiationAllowed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen()
|
||||
{
|
||||
|
@ -242,6 +253,7 @@ public class SslConnection extends AbstractConnection
|
|||
private boolean _fillRequiresFlushToProgress;
|
||||
private boolean _flushRequiresFillToProgress;
|
||||
private boolean _cannotAcceptMoreAppDataToFlush;
|
||||
private boolean _handshaken;
|
||||
private boolean _underFlown;
|
||||
|
||||
private final Callback _writeCallback = new Callback()
|
||||
|
@ -493,15 +505,19 @@ public class SslConnection extends AbstractConnection
|
|||
if (DEBUG)
|
||||
LOG.debug("{} unwrap {}", SslConnection.this, unwrapResult);
|
||||
|
||||
Status unwrapResultStatus = unwrapResult.getStatus();
|
||||
HandshakeStatus unwrapHandshakeStatus = unwrapResult.getHandshakeStatus();
|
||||
HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus();
|
||||
|
||||
// and deal with the results
|
||||
switch (unwrapResult.getStatus())
|
||||
switch (unwrapResultStatus)
|
||||
{
|
||||
case BUFFER_OVERFLOW:
|
||||
throw new IllegalStateException();
|
||||
|
||||
case CLOSED:
|
||||
// Dang! we have to care about the handshake state specially for close
|
||||
switch (_sslEngine.getHandshakeStatus())
|
||||
switch (handshakeStatus)
|
||||
{
|
||||
case NOT_HANDSHAKING:
|
||||
// We were not handshaking, so just tell the app we are closed
|
||||
|
@ -521,10 +537,28 @@ public class SslConnection extends AbstractConnection
|
|||
throw new IllegalStateException();
|
||||
|
||||
default:
|
||||
if (unwrapResult.getStatus()==Status.BUFFER_UNDERFLOW)
|
||||
_underFlown=true;
|
||||
if (unwrapHandshakeStatus == HandshakeStatus.FINISHED && !_handshaken)
|
||||
{
|
||||
_handshaken = true;
|
||||
if (DEBUG)
|
||||
LOG.debug("{} handshake completed client-side", SslConnection.this);
|
||||
}
|
||||
|
||||
// if we produced bytes, we don't care about the handshake state for now and it can be dealt with on another call to fill or flush
|
||||
// Check whether renegotiation is allowed
|
||||
if (_handshaken && handshakeStatus != HandshakeStatus.NOT_HANDSHAKING && !isRenegotiationAllowed())
|
||||
{
|
||||
if (DEBUG)
|
||||
LOG.debug("{} renegotiation denied", SslConnection.this);
|
||||
closeInbound();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (unwrapResultStatus == Status.BUFFER_UNDERFLOW)
|
||||
_underFlown = true;
|
||||
|
||||
// If bytes were produced, don't bother with the handshake status;
|
||||
// pass the decrypted data to the application, which will perform
|
||||
// another call to fill() or flush().
|
||||
if (unwrapResult.bytesProduced() > 0)
|
||||
{
|
||||
if (app_in == buffer)
|
||||
|
@ -533,13 +567,15 @@ public class SslConnection extends AbstractConnection
|
|||
}
|
||||
|
||||
// Dang! we have to care about the handshake state
|
||||
switch (_sslEngine.getHandshakeStatus())
|
||||
switch (handshakeStatus)
|
||||
{
|
||||
case NOT_HANDSHAKING:
|
||||
// we just didn't read anything.
|
||||
if (net_filled < 0)
|
||||
_sslEngine.closeInbound();
|
||||
|
||||
{
|
||||
closeInbound();
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
|
||||
case NEED_TASK:
|
||||
|
@ -551,7 +587,7 @@ public class SslConnection extends AbstractConnection
|
|||
// we need to send some handshake data
|
||||
|
||||
// if we are called from flush
|
||||
if (buffer==__FLUSH_CALLED_FILL)
|
||||
if (buffer == __FLUSH_CALLED_FILL)
|
||||
return 0; // let it do the wrapping
|
||||
|
||||
_fillRequiresFlushToProgress = true;
|
||||
|
@ -568,14 +604,8 @@ public class SslConnection extends AbstractConnection
|
|||
// if we just filled some net data
|
||||
if (net_filled < 0)
|
||||
{
|
||||
// If we call closeInbound() before having read the SSL close
|
||||
// message an exception will be thrown (truncation attack).
|
||||
// The TLS specification says that the sender of the SSL close
|
||||
// message may just close and avoid to read the response.
|
||||
// If that is the case, we avoid calling closeInbound() because
|
||||
// will throw the truncation attack exception for nothing.
|
||||
if (isOpen())
|
||||
_sslEngine.closeInbound();
|
||||
closeInbound();
|
||||
return -1;
|
||||
}
|
||||
else if (net_filled > 0)
|
||||
{
|
||||
|
@ -625,6 +655,18 @@ public class SslConnection extends AbstractConnection
|
|||
}
|
||||
}
|
||||
|
||||
private void closeInbound()
|
||||
{
|
||||
try
|
||||
{
|
||||
_sslEngine.closeInbound();
|
||||
}
|
||||
catch (SSLException x)
|
||||
{
|
||||
LOG.ignore(x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean flush(ByteBuffer... appOuts) throws IOException
|
||||
{
|
||||
|
@ -653,7 +695,6 @@ public class SslConnection extends AbstractConnection
|
|||
|
||||
while (true)
|
||||
{
|
||||
// do the funky SSL thang!
|
||||
// We call sslEngine.wrap to try to take bytes from appOut buffers and encrypt them into the _netOut buffer
|
||||
BufferUtil.compact(_encryptedOutput);
|
||||
int pos = BufferUtil.flipToFill(_encryptedOutput);
|
||||
|
@ -664,18 +705,20 @@ public class SslConnection extends AbstractConnection
|
|||
if (wrapResult.bytesConsumed()>0)
|
||||
consumed+=wrapResult.bytesConsumed();
|
||||
|
||||
boolean all_consumed=true;
|
||||
boolean allConsumed=true;
|
||||
// clear empty buffers to prevent position creeping up the buffer
|
||||
for (ByteBuffer b : appOuts)
|
||||
{
|
||||
if (BufferUtil.isEmpty(b))
|
||||
BufferUtil.clear(b);
|
||||
else
|
||||
all_consumed=false;
|
||||
allConsumed=false;
|
||||
}
|
||||
|
||||
Status wrapResultStatus = wrapResult.getStatus();
|
||||
|
||||
// and deal with the results returned from the sslEngineWrap
|
||||
switch (wrapResult.getStatus())
|
||||
switch (wrapResultStatus)
|
||||
{
|
||||
case CLOSED:
|
||||
// The SSL engine has close, but there may be close handshake that needs to be written
|
||||
|
@ -684,32 +727,52 @@ public class SslConnection extends AbstractConnection
|
|||
_cannotAcceptMoreAppDataToFlush = true;
|
||||
getEndPoint().flush(_encryptedOutput);
|
||||
// If we failed to flush the close handshake then we will just pretend that
|
||||
// the write has progressed normally and let a subsequent call to flush (or WriteFlusher#onIncompleteFlushed)
|
||||
// to finish writing the close handshake. The caller will find out about the close on a subsequent flush or fill.
|
||||
// the write has progressed normally and let a subsequent call to flush
|
||||
// (or WriteFlusher#onIncompleteFlushed) to finish writing the close handshake.
|
||||
// The caller will find out about the close on a subsequent flush or fill.
|
||||
if (BufferUtil.hasContent(_encryptedOutput))
|
||||
return false;
|
||||
}
|
||||
|
||||
// otherwise we have written, and the caller will close the underlying connection
|
||||
return all_consumed;
|
||||
return allConsumed;
|
||||
|
||||
case BUFFER_UNDERFLOW:
|
||||
throw new IllegalStateException();
|
||||
|
||||
default:
|
||||
if (DEBUG)
|
||||
LOG.debug("{} {} {}", this, wrapResult.getStatus(), BufferUtil.toDetailString(_encryptedOutput));
|
||||
LOG.debug("{} {} {}", this, wrapResultStatus, BufferUtil.toDetailString(_encryptedOutput));
|
||||
|
||||
if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED && !_handshaken)
|
||||
{
|
||||
_handshaken = true;
|
||||
if (DEBUG)
|
||||
LOG.debug("{} handshake completed server-side", SslConnection.this);
|
||||
}
|
||||
|
||||
HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus();
|
||||
|
||||
// Check whether renegotiation is allowed
|
||||
if (_handshaken && handshakeStatus != HandshakeStatus.NOT_HANDSHAKING && !isRenegotiationAllowed())
|
||||
{
|
||||
if (DEBUG)
|
||||
LOG.debug("{} renegotiation denied", SslConnection.this);
|
||||
shutdownOutput();
|
||||
BufferUtil.clear(_encryptedOutput);
|
||||
return allConsumed;
|
||||
}
|
||||
|
||||
// if we have net bytes, let's try to flush them
|
||||
if (BufferUtil.hasContent(_encryptedOutput))
|
||||
getEndPoint().flush(_encryptedOutput);
|
||||
|
||||
// But we also might have more to do for the handshaking state.
|
||||
switch (_sslEngine.getHandshakeStatus())
|
||||
switch (handshakeStatus)
|
||||
{
|
||||
case NOT_HANDSHAKING:
|
||||
// Return with the number of bytes consumed (which may be 0)
|
||||
return all_consumed&&BufferUtil.isEmpty(_encryptedOutput);
|
||||
return allConsumed && BufferUtil.isEmpty(_encryptedOutput);
|
||||
|
||||
case NEED_TASK:
|
||||
// run the task and continue
|
||||
|
@ -729,14 +792,13 @@ public class SslConnection extends AbstractConnection
|
|||
_flushRequiresFillToProgress = true;
|
||||
fill(__FLUSH_CALLED_FILL);
|
||||
// Check if after the fill() we need to wrap again
|
||||
if (_sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP)
|
||||
if (handshakeStatus == HandshakeStatus.NEED_WRAP)
|
||||
continue;
|
||||
}
|
||||
return all_consumed&&BufferUtil.isEmpty(_encryptedOutput);
|
||||
return allConsumed&&BufferUtil.isEmpty(_encryptedOutput);
|
||||
|
||||
case FINISHED:
|
||||
throw new IllegalStateException();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import java.nio.channels.SocketChannel;
|
|||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLEngineResult;
|
||||
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
||||
import org.eclipse.jetty.io.ssl.SslConnection;
|
||||
|
@ -73,10 +74,9 @@ public class SelectChannelEndPointSslTest extends SelectChannelEndPointTest
|
|||
SSLEngine engine = __sslCtxFactory.newSSLEngine();
|
||||
engine.setUseClientMode(false);
|
||||
SslConnection sslConnection = new SslConnection(__byteBufferPool, _threadPool, endpoint, engine);
|
||||
|
||||
sslConnection.setRenegotiationAllowed(__sslCtxFactory.isRenegotiationAllowed());
|
||||
Connection appConnection = super.newConnection(channel,sslConnection.getDecryptedEndPoint());
|
||||
sslConnection.getDecryptedEndPoint().setConnection(appConnection);
|
||||
|
||||
return sslConnection;
|
||||
}
|
||||
|
||||
|
@ -197,10 +197,28 @@ public class SelectChannelEndPointSslTest extends SelectChannelEndPointTest
|
|||
|
||||
Assert.assertEquals("HelloWorld",reply);
|
||||
|
||||
if (debug) System.err.println("Shutting down output");
|
||||
client.socket().shutdownOutput();
|
||||
|
||||
filled=client.read(sslIn);
|
||||
if (debug) System.err.println("in="+filled);
|
||||
sslIn.flip();
|
||||
try
|
||||
{
|
||||
// Since the client closed abruptly, the server is sending a close alert with a failure
|
||||
engine.unwrap(sslIn, appIn);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (SSLException x)
|
||||
{
|
||||
// Expected
|
||||
}
|
||||
|
||||
sslIn.clear();
|
||||
filled=client.read(sslIn);
|
||||
Assert.assertEquals(-1,filled);
|
||||
|
||||
Assert.assertFalse(server.isOpen());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -30,7 +30,6 @@ import java.nio.channels.SocketChannel;
|
|||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
||||
|
@ -80,10 +79,9 @@ public class SslConnectionTest
|
|||
SSLEngine engine = __sslCtxFactory.newSSLEngine();
|
||||
engine.setUseClientMode(false);
|
||||
SslConnection sslConnection = new SslConnection(__byteBufferPool, getExecutor(), endpoint, engine);
|
||||
|
||||
sslConnection.setRenegotiationAllowed(__sslCtxFactory.isRenegotiationAllowed());
|
||||
Connection appConnection = new TestConnection(sslConnection.getDecryptedEndPoint());
|
||||
sslConnection.getDecryptedEndPoint().setConnection(appConnection);
|
||||
|
||||
return sslConnection;
|
||||
}
|
||||
|
||||
|
|
|
@ -78,6 +78,7 @@ public class SslConnectionFactory extends AbstractConnectionFactory
|
|||
engine.setUseClientMode(false);
|
||||
|
||||
SslConnection sslConnection = newSslConnection(connector, endPoint, engine);
|
||||
sslConnection.setRenegotiationAllowed(_sslContextFactory.isRenegotiationAllowed());
|
||||
configure(sslConnection, connector, endPoint);
|
||||
|
||||
ConnectionFactory next = connector.getConnectionFactory(_nextProtocol);
|
||||
|
|
|
@ -130,8 +130,11 @@ public class DoSFilter implements Filter
|
|||
{
|
||||
private static final Logger LOG = Log.getLogger(DoSFilter.class);
|
||||
|
||||
private static final Pattern IP_PATTERN = Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})");
|
||||
private static final Pattern CIDR_PATTERN = Pattern.compile(IP_PATTERN + "/(\\d{1,2})");
|
||||
private static final String IPv4_GROUP = "(\\d{1,3})";
|
||||
private static final Pattern IPv4_PATTERN = Pattern.compile(IPv4_GROUP+"\\."+IPv4_GROUP+"\\."+IPv4_GROUP+"\\."+IPv4_GROUP);
|
||||
private static final String IPv6_GROUP = "(\\p{XDigit}{1,4})";
|
||||
private static final Pattern IPv6_PATTERN = Pattern.compile(IPv6_GROUP+":"+IPv6_GROUP+":"+IPv6_GROUP+":"+IPv6_GROUP+":"+IPv6_GROUP+":"+IPv6_GROUP+":"+IPv6_GROUP+":"+IPv6_GROUP);
|
||||
private static final Pattern CIDR_PATTERN = Pattern.compile("([^/]+)/(\\d+)");
|
||||
|
||||
private static final String __TRACKER = "DoSFilter.Tracker";
|
||||
private static final String __THROTTLED = "DoSFilter.Throttled";
|
||||
|
@ -625,31 +628,94 @@ public class DoSFilter implements Filter
|
|||
return false;
|
||||
}
|
||||
|
||||
protected boolean subnetMatch(String subnetAddress, String candidate)
|
||||
protected boolean subnetMatch(String subnetAddress, String address)
|
||||
{
|
||||
Matcher matcher = CIDR_PATTERN.matcher(subnetAddress);
|
||||
int subnet = intFromAddress(matcher);
|
||||
int prefix = Integer.parseInt(matcher.group(5));
|
||||
// Sets the most significant prefix bits to 1
|
||||
// If prefix == 8 => 11111111_00000000_00000000_00000000
|
||||
int mask = ~((1 << (32 - prefix)) - 1);
|
||||
int ip = intFromAddress(IP_PATTERN.matcher(candidate));
|
||||
return (ip & mask) == (subnet & mask);
|
||||
Matcher cidrMatcher = CIDR_PATTERN.matcher(subnetAddress);
|
||||
if (!cidrMatcher.matches())
|
||||
return false;
|
||||
|
||||
String subnet = cidrMatcher.group(1);
|
||||
int prefix;
|
||||
try
|
||||
{
|
||||
prefix = Integer.parseInt(cidrMatcher.group(2));
|
||||
}
|
||||
catch (NumberFormatException x)
|
||||
{
|
||||
LOG.info("Ignoring malformed CIDR address {}", subnetAddress);
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] subnetBytes = addressToBytes(subnet);
|
||||
if (subnetBytes == null)
|
||||
{
|
||||
LOG.info("Ignoring malformed CIDR address {}", subnetAddress);
|
||||
return false;
|
||||
}
|
||||
byte[] addressBytes = addressToBytes(address);
|
||||
if (addressBytes == null)
|
||||
{
|
||||
LOG.info("Ignoring malformed remote address {}", address);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Comparing IPv4 with IPv6 ?
|
||||
int length = subnetBytes.length;
|
||||
if (length != addressBytes.length)
|
||||
return false;
|
||||
|
||||
byte[] mask = prefixToBytes(prefix, length);
|
||||
|
||||
for (int i = 0; i < length; ++i)
|
||||
{
|
||||
if ((subnetBytes[i] & mask[i]) != (addressBytes[i] & mask[i]))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int intFromAddress(Matcher matcher)
|
||||
private byte[] addressToBytes(String address)
|
||||
{
|
||||
int result = 0;
|
||||
if (matcher.matches())
|
||||
Matcher ipv4Matcher = IPv4_PATTERN.matcher(address);
|
||||
if (ipv4Matcher.matches())
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
int b = Integer.parseInt(matcher.group(i + 1));
|
||||
result |= b << 8 * (3 - i);
|
||||
}
|
||||
byte[] result = new byte[4];
|
||||
for (int i = 0; i < result.length; ++i)
|
||||
result[i] = Integer.valueOf(ipv4Matcher.group(i + 1)).byteValue();
|
||||
return result;
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
else
|
||||
{
|
||||
Matcher ipv6Matcher = IPv6_PATTERN.matcher(address);
|
||||
if (ipv6Matcher.matches())
|
||||
{
|
||||
byte[] result = new byte[16];
|
||||
for (int i = 0; i < result.length; i += 2)
|
||||
{
|
||||
int word = Integer.valueOf(ipv6Matcher.group(i / 2 + 1), 16);
|
||||
result[i] = (byte)((word & 0xFF00) >>> 8);
|
||||
result[i + 1] = (byte)(word & 0xFF);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private byte[] prefixToBytes(int prefix, int length)
|
||||
{
|
||||
byte[] result = new byte[length];
|
||||
int index = 0;
|
||||
while (prefix / 8 > 0)
|
||||
{
|
||||
result[index] = -1;
|
||||
prefix -= 8;
|
||||
++index;
|
||||
}
|
||||
// Sets the _prefix_ most significant bits to 1
|
||||
result[index] = (byte)~((1 << (8 - prefix)) - 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void destroy()
|
||||
|
@ -980,14 +1046,7 @@ public class DoSFilter implements Filter
|
|||
private boolean addWhitelistAddress(List<String> list, String address)
|
||||
{
|
||||
address = address.trim();
|
||||
if (address.length() > 0)
|
||||
{
|
||||
if (CIDR_PATTERN.matcher(address).matches() || IP_PATTERN.matcher(address).matches())
|
||||
return list.add(address);
|
||||
else
|
||||
LOG.warn("Ignoring malformed whitelist IP address {}", address);
|
||||
}
|
||||
return false;
|
||||
return address.length() > 0 && list.add(address);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -80,12 +80,17 @@ public class DoSFilterTest extends AbstractDoSFilterTest
|
|||
List<String> whitelist = new ArrayList<String>();
|
||||
whitelist.add("192.168.0.1");
|
||||
whitelist.add("10.0.0.0/8");
|
||||
whitelist.add("4d8:0:a:1234:ABc:1F:b18:17");
|
||||
whitelist.add("4d8:0:a:1234:ABc:1F:0:0/96");
|
||||
Assert.assertTrue(filter.checkWhitelist(whitelist, "192.168.0.1"));
|
||||
Assert.assertFalse(filter.checkWhitelist(whitelist, "192.168.0.2"));
|
||||
Assert.assertFalse(filter.checkWhitelist(whitelist, "11.12.13.14"));
|
||||
Assert.assertTrue(filter.checkWhitelist(whitelist, "10.11.12.13"));
|
||||
Assert.assertTrue(filter.checkWhitelist(whitelist, "10.0.0.0"));
|
||||
Assert.assertFalse(filter.checkWhitelist(whitelist, "0.0.0.0"));
|
||||
Assert.assertTrue(filter.checkWhitelist(whitelist, "4d8:0:a:1234:ABc:1F:b18:17"));
|
||||
Assert.assertTrue(filter.checkWhitelist(whitelist, "4d8:0:a:1234:ABc:1F:b18:0"));
|
||||
Assert.assertFalse(filter.checkWhitelist(whitelist, "4d8:0:a:1234:ABc:1D:0:0"));
|
||||
}
|
||||
|
||||
private boolean hitRateTracker(DoSFilter doSFilter, int sleep) throws InterruptedException
|
||||
|
|
|
@ -13,10 +13,84 @@
|
|||
<name>Jetty :: SPDY :: Parent</name>
|
||||
|
||||
<properties>
|
||||
<npn.version>1.1.5.v20130313</npn.version>
|
||||
<npn.api.version>1.1.0.v20120525</npn.api.version>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>7u9</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>java.version</name>
|
||||
<value>1.7.0_9</value>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<npn.version>1.1.3.v20130313</npn.version>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>7u10</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>java.version</name>
|
||||
<value>1.7.0_10</value>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<npn.version>1.1.3.v20130313</npn.version>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>7u11</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>java.version</name>
|
||||
<value>1.7.0_11</value>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<npn.version>1.1.3.v20130313</npn.version>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>7u13</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>java.version</name>
|
||||
<value>1.7.0_13</value>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<npn.version>1.1.4.v20130313</npn.version>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>7u15</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>java.version</name>
|
||||
<value>1.7.0_15</value>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<npn.version>1.1.5.v20130313</npn.version>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>7u17</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>java.version</name>
|
||||
<value>1.7.0_17</value>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<npn.version>1.1.5.v20130313</npn.version>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<modules>
|
||||
<module>spdy-core</module>
|
||||
<module>spdy-client</module>
|
||||
|
|
|
@ -307,6 +307,7 @@ public class SPDYClient
|
|||
{
|
||||
final SSLEngine engine = client.newSSLEngine(sslContextFactory, channel);
|
||||
SslConnection sslConnection = new SslConnection(bufferPool, getExecutor(), endPoint, engine);
|
||||
sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed());
|
||||
DecryptedEndPoint sslEndPoint = sslConnection.getDecryptedEndPoint();
|
||||
NextProtoNegoClientConnection connection = new NextProtoNegoClientConnection(channel, sslEndPoint, attachment, getExecutor(), client);
|
||||
sslEndPoint.setConnection(connection);
|
||||
|
|
|
@ -41,6 +41,9 @@ import java.util.Collections;
|
|||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.net.ssl.CertPathTrustManagerParameters;
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
|
@ -196,8 +199,12 @@ public class SslContextFactory extends AbstractLifeCycle
|
|||
/** EndpointIdentificationAlgorithm - when set to "HTTPS" hostname verification will be enabled */
|
||||
private String _endpointIdentificationAlgorithm = null;
|
||||
|
||||
/** Whether to blindly trust certificates */
|
||||
private boolean _trustAll;
|
||||
|
||||
/** Whether TLS renegotiation is allowed */
|
||||
private boolean _renegotiationAllowed = true;
|
||||
|
||||
/**
|
||||
* Construct an instance of SslContextFactory
|
||||
* Default constructor for use in XmlConfiguration files
|
||||
|
@ -369,6 +376,7 @@ public class SslContextFactory extends AbstractLifeCycle
|
|||
}
|
||||
|
||||
/**
|
||||
* You can either use the exact cipher suite name or a a regular expression.
|
||||
* @param cipherSuites
|
||||
* The array of cipher suite names to exclude from
|
||||
* {@link SSLEngine#setEnabledCipherSuites(String[])}
|
||||
|
@ -399,6 +407,7 @@ public class SslContextFactory extends AbstractLifeCycle
|
|||
}
|
||||
|
||||
/**
|
||||
* You can either use the exact cipher suite name or a a regular expression.
|
||||
* @param cipherSuites
|
||||
* The array of cipher suite names to include in
|
||||
* {@link SSLEngine#setEnabledCipherSuites(String[])}
|
||||
|
@ -759,6 +768,22 @@ public class SslContextFactory extends AbstractLifeCycle
|
|||
_trustManagerFactoryAlgorithm = algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether TLS renegotiation is allowed (true by default)
|
||||
*/
|
||||
public boolean isRenegotiationAllowed()
|
||||
{
|
||||
return _renegotiationAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param renegotiationAllowed whether TLS renegotiation is allowed
|
||||
*/
|
||||
public void setRenegotiationAllowed(boolean renegotiationAllowed)
|
||||
{
|
||||
_renegotiationAllowed = renegotiationAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Path to file that contains Certificate Revocation List
|
||||
*/
|
||||
|
@ -1035,25 +1060,47 @@ public class SslContextFactory extends AbstractLifeCycle
|
|||
*/
|
||||
public String[] selectCipherSuites(String[] enabledCipherSuites, String[] supportedCipherSuites)
|
||||
{
|
||||
Set<String> selected_ciphers = new LinkedHashSet<>();
|
||||
Set<String> selected_ciphers = new CopyOnWriteArraySet<>();
|
||||
|
||||
// Set the starting ciphers - either from the included or enabled list
|
||||
if (_includeCipherSuites!=null)
|
||||
{
|
||||
// Use only the supported included ciphers
|
||||
for (String cipherSuite : _includeCipherSuites)
|
||||
if(Arrays.asList(supportedCipherSuites).contains(cipherSuite))
|
||||
selected_ciphers.add(cipherSuite);
|
||||
}
|
||||
processIncludeCipherSuites(supportedCipherSuites, selected_ciphers);
|
||||
else
|
||||
selected_ciphers.addAll(Arrays.asList(enabledCipherSuites));
|
||||
|
||||
removeExcludedCipherSuites(selected_ciphers);
|
||||
|
||||
// Remove any excluded ciphers
|
||||
selected_ciphers.removeAll(_excludeCipherSuites);
|
||||
return selected_ciphers.toArray(new String[selected_ciphers.size()]);
|
||||
}
|
||||
|
||||
private void processIncludeCipherSuites(String[] supportedCipherSuites, Set<String> selected_ciphers)
|
||||
{
|
||||
for (String cipherSuite : _includeCipherSuites)
|
||||
{
|
||||
Pattern p = Pattern.compile(cipherSuite);
|
||||
for (String supportedCipherSuite : supportedCipherSuites)
|
||||
{
|
||||
Matcher m = p.matcher(supportedCipherSuite);
|
||||
if (m.matches())
|
||||
selected_ciphers.add(supportedCipherSuite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeExcludedCipherSuites(Set<String> selected_ciphers)
|
||||
{
|
||||
for (String excludeCipherSuite : _excludeCipherSuites)
|
||||
{
|
||||
Pattern excludeCipherPattern = Pattern.compile(excludeCipherSuite);
|
||||
for (String selectedCipherSuite : selected_ciphers)
|
||||
{
|
||||
Matcher m = excludeCipherPattern.matcher(selectedCipherSuite);
|
||||
if (m.matches())
|
||||
selected_ciphers.remove(selectedCipherSuite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the lifecycle has been started and throw runtime exception
|
||||
*/
|
||||
|
|
|
@ -18,15 +18,12 @@
|
|||
|
||||
package org.eclipse.jetty.util.ssl;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
|
||||
import javax.net.ssl.SSLEngine;
|
||||
|
||||
import org.eclipse.jetty.util.component.AbstractLifeCycle;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.StdErrLog;
|
||||
|
@ -35,6 +32,12 @@ import org.junit.Assert;
|
|||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
|
||||
public class SslContextFactoryTest
|
||||
{
|
||||
|
@ -189,6 +192,30 @@ public class SslContextFactoryTest
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetExcludeCipherSuitesRegex() throws Exception
|
||||
{
|
||||
cf.setExcludeCipherSuites(".*RC4.*");
|
||||
cf.start();
|
||||
SSLEngine sslEngine = cf.newSSLEngine();
|
||||
String[] enabledCipherSuites = sslEngine.getEnabledCipherSuites();
|
||||
assertThat("At least 1 cipherSuite is enabled", enabledCipherSuites.length, greaterThan(0));
|
||||
for (String enabledCipherSuite : enabledCipherSuites)
|
||||
assertThat("CipherSuite does not contain RC4", enabledCipherSuite.contains("RC4"), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetIncludeCipherSuitesRegex() throws Exception
|
||||
{
|
||||
cf.setIncludeCipherSuites(".*RC4.*");
|
||||
cf.start();
|
||||
SSLEngine sslEngine = cf.newSSLEngine();
|
||||
String[] enabledCipherSuites = sslEngine.getEnabledCipherSuites();
|
||||
assertThat("At least 1 cipherSuite is enabled", enabledCipherSuites.length, greaterThan(0));
|
||||
for (String enabledCipherSuite : enabledCipherSuites)
|
||||
assertThat("CipherSuite contains RC4", enabledCipherSuite.contains("RC4"), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetIncludeCipherSuitesPreservesOrder()
|
||||
{
|
||||
|
|
|
@ -22,7 +22,6 @@ import java.io.IOException;
|
|||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.net.ssl.SSLEngine;
|
||||
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
|
@ -82,6 +81,7 @@ public class WebSocketClientSelectorManager extends SelectorManager
|
|||
{
|
||||
SSLEngine engine = newSSLEngine(sslContextFactory,channel);
|
||||
SslConnection sslConnection = new SslConnection(bufferPool,getExecutor(),endPoint,engine);
|
||||
sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed());
|
||||
EndPoint sslEndPoint = sslConnection.getDecryptedEndPoint();
|
||||
|
||||
Connection connection = newUpgradeConnection(channel,sslEndPoint,connectPromise);
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
|
||||
package org.eclipse.jetty.websocket.server;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -45,11 +43,15 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
|||
import org.junit.AfterClass;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
/**
|
||||
* Tests various close scenarios
|
||||
*/
|
||||
@Ignore
|
||||
public class WebSocketCloseTest
|
||||
{
|
||||
@SuppressWarnings("serial")
|
||||
|
|
Loading…
Reference in New Issue