Resolved a long standing problem with HttpClient not taking into account the user context when pooling / re-using connections. HttpClient now correctly handles stateful / user specific connections such as persistent NTLM connections and SSL connections with client side authentication.

git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@658759 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2008-05-21 17:06:17 +00:00
parent db8947ac50
commit 6d0c809900
9 changed files with 441 additions and 14 deletions

View File

@ -1,6 +1,12 @@
Changes since 4.0 Alpha 4
-------------------
* Resolved a long standing problem with HttpClient not taking into account
the user context when pooling / re-using connections. HttpClient now
correctly handles stateful / user specific connections such as persistent
NTLM connections and SSL connections with client side authentication.
Contributed by Oleg Kalnichevski <olegk at apache.org>
* [HTTPCLIENT-773] Improved handling of the 'expires' attribute by the
'Best Match' cookie spec.
Contributed by Oleg Kalnichevski <olegk at apache.org>

View File

@ -0,0 +1,64 @@
/*
* $HeadURL:$
* $Revision:$
* $Date:$
*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.http.client;
import org.apache.http.protocol.HttpContext;
/**
* A handler for determining if the given execution context is user specific
* or not. The token object returned by this handler is expected to uniquely
* identify the current user if the context is user specific or to be
* <code>null</code> if the context does not contain any resources or details
* specific to the current user.
* <p/>
* The user token will be used to ensure that user specific resouces will not
* shared with or reused by other users.
*
* @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a>
*
* @since 4.0
*/
public interface UserTokenHandler {
/**
* The token object returned by this method is expected to uniquely
* identify the current user if the context is user specific or to be
* <code>null</code> if it is not.
*
* @param context the execution context
*
* @return user token that uniquely identifies the user or
* <code>null</null> if the context is not user specific.
*/
Object getUserToken(HttpContext context);
}

View File

@ -47,5 +47,6 @@ public interface ClientContext {
public static final String TARGET_AUTH_STATE = "http.auth.target-scope";
public static final String PROXY_AUTH_STATE = "http.auth.proxy-scope";
public static final String AUTH_SCHEME_PREF = "http.auth.scheme-pref";
public static final String USER_TOKEN = "http.user-token";
}

View File

@ -49,6 +49,7 @@ import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.RedirectHandler;
import org.apache.http.client.UserTokenHandler;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
@ -110,9 +111,12 @@ public abstract class AbstractHttpClient implements HttpClient {
/** The credentials provider. */
private CredentialsProvider credsProvider;
/** The HttpRoutePlanner object. */
/** The route planner. */
private HttpRoutePlanner routePlanner;
/** The user token handler. */
private UserTokenHandler userTokenHandler;
/**
* Creates a new HTTP client.
@ -169,18 +173,8 @@ public abstract class AbstractHttpClient implements HttpClient {
protected abstract HttpRoutePlanner createHttpRoutePlanner();
public synchronized final HttpRoutePlanner getRoutePlanner() {
if (this.routePlanner == null) {
this.routePlanner = createHttpRoutePlanner();
}
return this.routePlanner;
}
protected abstract UserTokenHandler createUserTokenHandler();
public synchronized void setRoutePlanner(final HttpRoutePlanner routePlanner) {
this.routePlanner = routePlanner;
}
// non-javadoc, see interface HttpClient
public synchronized final HttpParams getParams() {
@ -334,6 +328,32 @@ public abstract class AbstractHttpClient implements HttpClient {
}
public synchronized final HttpRoutePlanner getRoutePlanner() {
if (this.routePlanner == null) {
this.routePlanner = createHttpRoutePlanner();
}
return this.routePlanner;
}
public synchronized void setRoutePlanner(final HttpRoutePlanner routePlanner) {
this.routePlanner = routePlanner;
}
public synchronized final UserTokenHandler getUserTokenHandler() {
if (this.userTokenHandler == null) {
this.userTokenHandler = createUserTokenHandler();
}
return this.userTokenHandler;
}
public synchronized void setUserTokenHandler(final UserTokenHandler userTokenHandler) {
this.userTokenHandler = userTokenHandler;
}
protected synchronized final BasicHttpProcessor getHttpProcessor() {
if (httpProcessor == null) {
httpProcessor = createHttpProcessor();
@ -500,6 +520,7 @@ public abstract class AbstractHttpClient implements HttpClient {
getRedirectHandler(),
getTargetAuthenticationHandler(),
getProxyAuthenticationHandler(),
getUserTokenHandler(),
determineParams(request));
}
@ -524,6 +545,7 @@ public abstract class AbstractHttpClient implements HttpClient {
final RedirectHandler redirectHandler,
final AuthenticationHandler targetAuthHandler,
final AuthenticationHandler proxyAuthHandler,
final UserTokenHandler stateHandler,
final HttpParams params) {
return new DefaultClientRequestDirector(
conman,
@ -534,6 +556,7 @@ public abstract class AbstractHttpClient implements HttpClient {
redirectHandler,
targetAuthHandler,
proxyAuthHandler,
stateHandler,
params);
}

View File

@ -63,6 +63,7 @@ import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.RedirectException;
import org.apache.http.client.RedirectHandler;
import org.apache.http.client.UserTokenHandler;
import org.apache.http.client.methods.AbortableHttpRequest;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.ClientPNames;
@ -134,6 +135,9 @@ public class DefaultClientRequestDirector
/** The proxy authentication handler. */
private final AuthenticationHandler proxyAuthHandler;
/** The user token handler. */
private final UserTokenHandler userTokenHandler;
/** The HTTP parameters. */
protected final HttpParams params;
@ -157,6 +161,7 @@ public class DefaultClientRequestDirector
final RedirectHandler redirectHandler,
final AuthenticationHandler targetAuthHandler,
final AuthenticationHandler proxyAuthHandler,
final UserTokenHandler userTokenHandler,
final HttpParams params) {
if (conman == null) {
@ -191,6 +196,10 @@ public class DefaultClientRequestDirector
throw new IllegalArgumentException
("Proxy authentication handler may not be null.");
}
if (userTokenHandler == null) {
throw new IllegalArgumentException
("User token handler may not be null.");
}
if (params == null) {
throw new IllegalArgumentException
("HTTP parameters may not be null");
@ -203,6 +212,7 @@ public class DefaultClientRequestDirector
this.redirectHandler = redirectHandler;
this.targetAuthHandler = targetAuthHandler;
this.proxyAuthHandler = proxyAuthHandler;
this.userTokenHandler = userTokenHandler;
this.params = params;
this.requestExec = new HttpRequestExecutor();
@ -283,10 +293,13 @@ public class DefaultClientRequestDirector
RequestWrapper wrapper = roureq.getRequest();
HttpRoute route = roureq.getRoute();
// See if we have a user token bound to the execution context
Object userToken = context.getAttribute(ClientContext.USER_TOKEN);
// Allocate connection if needed
if (managedConn == null) {
ClientConnectionRequest connRequest = connManager.requestConnection(
route, null);
route, userToken);
if (orig instanceof AbortableHttpRequest) {
((AbortableHttpRequest) orig).setConnectionRequest(connRequest);
}
@ -415,6 +428,12 @@ public class DefaultClientRequestDirector
followup.getRequest().clearHeaders();
roureq = followup;
}
userToken = this.userTokenHandler.getUserToken(context);
context.setAttribute(ClientContext.USER_TOKEN, userToken);
if (managedConn != null) {
managedConn.setState(userToken);
}
} // while not done
// The connection is in or can be brought to a re-usable state.

View File

@ -39,6 +39,7 @@ import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.RedirectHandler;
import org.apache.http.client.UserTokenHandler;
import org.apache.http.client.params.AuthPolicy;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
@ -289,11 +290,16 @@ public class DefaultHttpClient extends AbstractHttpClient {
}
// non-javadoc, see base class AbstractHttpClient
@Override
protected HttpRoutePlanner createHttpRoutePlanner() {
return new DefaultHttpRoutePlanner
(getConnectionManager().getSchemeRegistry());
}
@Override
protected UserTokenHandler createUserTokenHandler() {
return new DefaultUserTokenHandler();
}
} // class DefaultHttpClient

View File

@ -0,0 +1,88 @@
/*
* $HeadURL:$
* $Revision:$
* $Date:$
*
* ====================================================================
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.http.impl.client;
import java.security.Principal;
import javax.net.ssl.SSLSession;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.Credentials;
import org.apache.http.client.AuthState;
import org.apache.http.client.UserTokenHandler;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ManagedClientConnection;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
public class DefaultUserTokenHandler implements UserTokenHandler {
public Object getUserToken(final HttpContext context) {
Principal userPrincipal = null;
AuthState targetAuthState = (AuthState) context.getAttribute(
ClientContext.TARGET_AUTH_STATE);
if (targetAuthState != null) {
userPrincipal = getAuthPrincipal(targetAuthState);
if (userPrincipal == null) {
AuthState proxyAuthState = (AuthState) context.getAttribute(
ClientContext.PROXY_AUTH_STATE);
userPrincipal = getAuthPrincipal(proxyAuthState);
}
}
if (userPrincipal == null) {
ManagedClientConnection conn = (ManagedClientConnection) context.getAttribute(
ExecutionContext.HTTP_CONNECTION);
if (conn.isOpen()) {
SSLSession sslsession = conn.getSSLSession();
if (sslsession != null) {
userPrincipal = sslsession.getLocalPrincipal();
}
}
}
return userPrincipal;
}
private static Principal getAuthPrincipal(final AuthState authState) {
AuthScheme scheme = authState.getAuthScheme();
if (scheme != null && scheme.isComplete() && scheme.isConnectionBased()) {
Credentials creds = authState.getCredentials();
if (creds != null) {
return creds.getUserPrincipal();
}
}
return null;
}
}

View File

@ -45,6 +45,7 @@ public class TestAllHttpClientImpl extends TestCase {
suite.addTest(TestBasicCredentialsProvider.suite());
suite.addTest(TestRequestWrapper.suite());
suite.addTest(TestDefaultClientRequestDirector.suite());
suite.addTest(TestStatefulConnManagement.suite());
return suite;
}

View File

@ -0,0 +1,219 @@
/*
* $HeadURL$
* $Revision$
* $Date$
* ====================================================================
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*/
package org.apache.http.impl.client;
import java.io.IOException;
import junit.framework.Test;
import junit.framework.TestSuite;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.UserTokenHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.ManagedClientConnection;
import org.apache.http.conn.params.ConnPerRouteBean;
import org.apache.http.conn.params.HttpConnectionManagerParams;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.localserver.ServerTestBase;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
/**
* Unit tests for {@link DefaultClientRequestDirector}
*/
public class TestStatefulConnManagement extends ServerTestBase {
public TestStatefulConnManagement(final String testName) throws IOException {
super(testName);
}
public static void main(String args[]) {
String[] testCaseName = { TestStatefulConnManagement.class.getName() };
junit.textui.TestRunner.main(testCaseName);
}
public static Test suite() {
return new TestSuite(TestStatefulConnManagement.class);
}
private class SimpleService implements HttpRequestHandler {
public SimpleService() {
super();
}
public void handle(
final HttpRequest request,
final HttpResponse response,
final HttpContext context) throws HttpException, IOException {
response.setStatusCode(HttpStatus.SC_OK);
StringEntity entity = new StringEntity("Whatever");
response.setEntity(entity);
}
}
public void testStatefulConnections() throws Exception {
int workerCount = 5;
int requestCount = 5;
int port = this.localServer.getServicePort();
this.localServer.register("*", new SimpleService());
HttpHost target = new HttpHost("localhost", port);
HttpParams params = defaultParams.copy();
HttpConnectionManagerParams.setMaxTotalConnections(params, workerCount);
HttpConnectionManagerParams.setMaxConnectionsPerRoute(params,
new ConnPerRouteBean(workerCount));
HttpClientParams.setConnectionManagerTimeout(params, 10L);
ThreadSafeClientConnManager mgr = new ThreadSafeClientConnManager(
params, supportedSchemes);
DefaultHttpClient client = new DefaultHttpClient(mgr, params);
HttpContext[] contexts = new HttpContext[workerCount];
HttpWorker[] workers = new HttpWorker[workerCount];
for (int i = 0; i < contexts.length; i++) {
HttpContext context = new BasicHttpContext();
context.setAttribute("user", Integer.valueOf(i));
contexts[i] = context;
workers[i] = new HttpWorker(context, requestCount, target, client);
}
client.setUserTokenHandler(new UserTokenHandler() {
public Object getUserToken(final HttpContext context) {
Integer id = (Integer) context.getAttribute("user");
return id;
}
});
for (int i = 0; i < workers.length; i++) {
workers[i].start();
}
for (int i = 0; i < workers.length; i++) {
workers[i].join(10000);
}
for (int i = 0; i < workers.length; i++) {
Exception ex = workers[i].getException();
if (ex != null) {
throw ex;
}
assertEquals(requestCount, workers[i].getCount());
}
for (int i = 0; i < contexts.length; i++) {
HttpContext context = contexts[i];
Integer id = (Integer) context.getAttribute("user");
for (int r = 0; r < requestCount; r++) {
Integer state = (Integer) context.getAttribute("r" + r);
assertNotNull(state);
assertEquals(id, state);
}
}
}
static class HttpWorker extends Thread {
private final HttpContext context;
private final int requestCount;
private final HttpHost target;
private final HttpClient httpclient;
private volatile Exception exception;
private volatile int count;
public HttpWorker(
final HttpContext context,
int requestCount,
final HttpHost target,
final HttpClient httpclient) {
super();
this.context = context;
this.requestCount = requestCount;
this.target = target;
this.httpclient = httpclient;
this.count = 0;
}
public int getCount() {
return this.count;
}
public Exception getException() {
return this.exception;
}
@Override
public void run() {
try {
for (int r = 0; r < this.requestCount; r++) {
HttpGet httpget = new HttpGet("/");
HttpResponse response = this.httpclient.execute(
this.target,
httpget,
this.context);
this.count++;
ManagedClientConnection conn = (ManagedClientConnection) this.context.getAttribute(
ExecutionContext.HTTP_CONNECTION);
this.context.setAttribute("r" + r, conn.getState());
HttpEntity entity = response.getEntity();
if (entity != null) {
entity.consumeContent();
}
}
} catch (Exception ex) {
this.exception = ex;
}
}
}
}