Index: java/org/apache/commons/httpclient/HttpMethod.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethod.java,v retrieving revision 1.34 diff -u -r1.34 HttpMethod.java --- java/org/apache/commons/httpclient/HttpMethod.java 22 Feb 2004 18:08:45 -0000 1.34 +++ java/org/apache/commons/httpclient/HttpMethod.java 18 Mar 2004 21:11:09 -0000 @@ -34,6 +34,7 @@ import java.io.IOException; import java.io.InputStream; +import org.apache.commons.httpclient.auth.AuthState; import org.apache.commons.httpclient.params.*; @@ -211,6 +212,13 @@ void removeRequestHeader(String headerName); /** + * Removes the given request header. + * + * @param header the header + */ + void removeRequestHeader(Header header); + + /** * Returns true if the HTTP method should automatically follow HTTP redirects * (status code 302, etc.), false otherwise. * @@ -531,5 +539,23 @@ * @param handler the methodRetryHandler to use when this method executed */ public void setMethodRetryHandler(MethodRetryHandler handler); + + /** + * Returns the target host {@link AuthState authentication state} + * + * @return host authentication state + * + * @since 3.0 + */ + public AuthState getHostAuthState(); + + /** + * Returns the proxy {@link AuthState authentication state} + * + * @return host authentication state + * + * @since 3.0 + */ + public AuthState getProxyAuthState(); } Index: java/org/apache/commons/httpclient/HttpMethodBase.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodBase.java,v retrieving revision 1.201 diff -u -r1.201 HttpMethodBase.java --- java/org/apache/commons/httpclient/HttpMethodBase.java 9 Mar 2004 14:01:02 -0000 1.201 +++ java/org/apache/commons/httpclient/HttpMethodBase.java 18 Mar 2004 21:11:15 -0000 @@ -36,6 +36,7 @@ import java.io.IOException; import java.io.InputStream; +import org.apache.commons.httpclient.auth.AuthState; import org.apache.commons.httpclient.cookie.CookiePolicy; import org.apache.commons.httpclient.cookie.CookieSpec; import org.apache.commons.httpclient.cookie.MalformedCookieException; @@ -139,6 +140,12 @@ /** HTTP protocol parameters. */ private HttpMethodParams params = new HttpMethodParams(); + /** Host authentication state */ + private AuthState hostAuthState = new AuthState(); + + /** Proxy authentication state */ + private AuthState proxyAuthState = new AuthState(); + /** True if this method has already been executed. */ private boolean used = false; @@ -998,6 +1005,8 @@ responseBody = null; recoverableExceptionCount = 0; connectionCloseForced = false; + hostAuthState.invalidate(); + proxyAuthState.invalidate(); } /** @@ -1040,6 +1049,18 @@ } } + + /** + * Removes the given request header. + * + * @param header the header + */ + public void removeRequestHeader(final Header header) { + if (header == null) { + return; + } + getRequestHeaderGroup().removeHeader(header); + } // ---------------------------------------------------------------- Queries @@ -2039,27 +2060,27 @@ } /** - * @deprecated no longer used - * * Returns proxy authentication realm, if it has been used during authentication process. * Otherwise returns null. * * @return proxy authentication realm + * + * @deprecated use #getProxyAuthState() */ public String getProxyAuthenticationRealm() { - return null; + return this.proxyAuthState.getRealm(); } /** - * @deprecated no longer used - * * Returns authentication realm, if it has been used during authentication process. * Otherwise returns null. * * @return authentication realm + * + * @deprecated use #getHostAuthState() */ public String getAuthenticationRealm() { - return null; + return this.hostAuthState.getRealm(); } /** @@ -2241,4 +2262,25 @@ this.responseStream = responseStream; } + /** + * Returns the target host {@link AuthState authentication state} + * + * @return host authentication state + * + * @since 3.0 + */ + public AuthState getHostAuthState() { + return this.hostAuthState; + } + + /** + * Returns the proxy {@link AuthState authentication state} + * + * @return host authentication state + * + * @since 3.0 + */ + public AuthState getProxyAuthState() { + return this.proxyAuthState; + } } Index: java/org/apache/commons/httpclient/HttpMethodDirector.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodDirector.java,v retrieving revision 1.18 diff -u -r1.18 HttpMethodDirector.java --- java/org/apache/commons/httpclient/HttpMethodDirector.java 22 Feb 2004 18:08:46 -0000 1.18 +++ java/org/apache/commons/httpclient/HttpMethodDirector.java 18 Mar 2004 21:11:17 -0000 @@ -32,13 +32,13 @@ package org.apache.commons.httpclient; import java.io.IOException; -import java.util.Collection; -import java.util.Iterator; import java.util.Map; +import org.apache.commons.httpclient.auth.AuthChallengeException; import org.apache.commons.httpclient.auth.AuthChallengeParser; -import org.apache.commons.httpclient.auth.AuthPolicy; +import org.apache.commons.httpclient.auth.AuthChallengeProcessor; import org.apache.commons.httpclient.auth.AuthScheme; +import org.apache.commons.httpclient.auth.AuthState; import org.apache.commons.httpclient.auth.AuthenticationException; import org.apache.commons.httpclient.auth.CredentialsProvider; import org.apache.commons.httpclient.auth.CredentialsNotAvailableException; @@ -93,20 +93,11 @@ private static final int AUTH_NOT_REQUIRED = Integer.MAX_VALUE; /** Actual state of authentication process */ - private int authState = AUTH_UNINITIATED; + private int authProcess = AUTH_UNINITIATED; - /** Actual authentication scheme */ - private AuthScheme authScheme = null; + /** Authentication processor */ + private AuthChallengeProcessor authProcessor = null; - /** Whether preemtive authentication is attempted */ - private boolean authPreemptive = false; - - /** Actual proxy authentication scheme */ - private AuthScheme proxyAuthScheme = null; - - /** Whether preemtive proxy authentication is attempted */ - private boolean proxyAuthPreemptive = false; - public HttpMethodDirector( final HttpConnectionManager connectionManager, final HostConfiguration hostConfiguration, @@ -118,6 +109,7 @@ this.hostConfiguration = hostConfiguration; this.params = params; this.state = state; + this.authProcessor = new AuthChallengeProcessor(this.params); } @@ -128,9 +120,7 @@ * @throws HttpException */ public void executeMethod(final HttpMethod method) throws IOException, HttpException { - - if (method == null) - { + if (method == null) { throw new IllegalArgumentException("Method may not be null"); } method.getParams().setDefaults(this.params); @@ -161,12 +151,10 @@ || this.state.isAuthenticationPreemptive()) { LOG.debug("Preemptively sending default basic credentials"); - this.authState = AUTH_PREEMPTIVE; - this.authScheme = AuthPolicy.getAuthScheme("basic"); - this.authPreemptive = true; + this.authProcess = AUTH_PREEMPTIVE; + method.getHostAuthState().setPreemptive(); if (this.conn.isProxied()) { - this.proxyAuthPreemptive = true; - this.proxyAuthScheme = AuthPolicy.getAuthScheme("basic"); + method.getProxyAuthState().setPreemptive(); } } } @@ -188,7 +176,7 @@ retry = true; } } else { - this.authState = AUTH_NOT_REQUIRED; + this.authProcess = AUTH_NOT_REQUIRED; } if (!retry) { break; @@ -237,14 +225,33 @@ } } + + private boolean cleanAuthHeaders(final HttpMethod method, final String name) { + Header[] authheaders = method.getRequestHeaders(name); + boolean clean = true; + for (int i = 0; i < authheaders.length; i++) { + Header authheader = authheaders[i]; + if (authheader.isAutogenerated()) { + method.removeRequestHeader(authheader); + } else { + clean = false; + } + } + return clean; + } + private void authenticateHost(final HttpMethod method) throws AuthenticationException { // Clean up existing authentication headers - method.removeRequestHeader(WWW_AUTH_RESP); - if ((this.authScheme != null) - && ((this.authState == AUTH_WWW_REQUIRED) - || (!this.authScheme.isConnectionBased()))) - { + if (!cleanAuthHeaders(method, WWW_AUTH_RESP)) { + // User defined authentication header(s) present + return; + } + AuthScheme authscheme = method.getHostAuthState().getAuthScheme(); + if (authscheme == null) { + return; + } + if ((this.authProcess == AUTH_WWW_REQUIRED) || (!authscheme.isConnectionBased())) { String host = conn.getVirtualHost(); if (host == null) { host = conn.getHost(); @@ -252,14 +259,14 @@ int port = conn.getPort(); HttpAuthRealm realm = new HttpAuthRealm( host, port, - this.authScheme.getRealm(), - this.authScheme.getSchemeName()); + authscheme.getRealm(), + authscheme.getSchemeName()); if (LOG.isDebugEnabled()) { LOG.debug("Authenticating with " + realm); } Credentials credentials = this.state.getCredentials(realm); if (credentials != null) { - String authstring = this.authScheme.authenticate(credentials, method); + String authstring = authscheme.authenticate(credentials, method); if (authstring != null) { method.addRequestHeader(new Header(WWW_AUTH_RESP, authstring, true)); } @@ -272,21 +279,26 @@ private void authenticateProxy(final HttpMethod method) throws AuthenticationException { // Clean up existing authentication headers - method.removeRequestHeader(PROXY_AUTH_RESP); - if ((this.proxyAuthScheme != null) - && ((this.authState == AUTH_PROXY_REQUIRED) - || (!this.proxyAuthScheme.isConnectionBased()))) - { + // Clean up existing authentication headers + if (!cleanAuthHeaders(method, PROXY_AUTH_RESP)) { + // User defined authentication header(s) present + return; + } + AuthScheme authscheme = method.getProxyAuthState().getAuthScheme(); + if (authscheme == null) { + return; + } + if ((this.authProcess == AUTH_PROXY_REQUIRED) || (!authscheme.isConnectionBased())) { HttpAuthRealm realm = new HttpAuthRealm( conn.getProxyHost(), conn.getProxyPort(), - this.proxyAuthScheme.getRealm(), - this.proxyAuthScheme.getSchemeName()); + authscheme.getRealm(), + authscheme.getSchemeName()); if (LOG.isDebugEnabled()) { LOG.debug("Authenticating with " + realm); } Credentials credentials = this.state.getProxyCredentials(realm); if (credentials != null) { - String authstring = this.proxyAuthScheme.authenticate(credentials, method); + String authstring = authscheme.authenticate(credentials, method); if (authstring != null) { method.addRequestHeader(new Header(PROXY_AUTH_RESP, authstring, true)); } @@ -416,7 +428,7 @@ retry = true; } } else { - this.authState = AUTH_NOT_REQUIRED; + this.authProcess = AUTH_NOT_REQUIRED; } if (!retry) { break; @@ -467,6 +479,8 @@ this.connectMethod.getResponseHeaderGroup(), this.connectMethod.getResponseBodyAsStream() ); + method.getProxyAuthState().setAuthScheme( + this.connectMethod.getProxyAuthState().getAuthScheme()); this.connectMethod = null; } else { releaseConnection = true; @@ -537,7 +551,7 @@ } //And finally invalidate the actual authentication scheme - this.authScheme = null; + method.getHostAuthState().invalidate(); return true; } @@ -574,21 +588,24 @@ private boolean processWWWAuthChallenge(final HttpMethod method) throws MalformedChallengeException, AuthenticationException { - if (this.authPreemptive) { - this.authScheme = null; - this.authPreemptive = false; + AuthState authstate = method.getHostAuthState(); + if (authstate.isPreemptive()) { + authstate.invalidate(); } Map challenges = AuthChallengeParser.parseChallenges( method.getResponseHeaders(WWW_AUTH_CHALLENGE)); if (challenges.isEmpty()) { return false; } - if (this.authScheme != null) { - processChallenge(this.authScheme, challenges); - } else { - this.authScheme = processChallenge(challenges); + AuthScheme authscheme = null; + try { + authscheme = this.authProcessor.processChallenge(authstate, challenges); + } catch (AuthChallengeException e) { + if (LOG.isWarnEnabled()) { + LOG.warn(e.getMessage()); + } } - if (this.authScheme == null) { + if (authscheme == null) { return false; } String host = conn.getVirtualHost(); @@ -598,13 +615,13 @@ int port = conn.getPort(); HttpAuthRealm realm = new HttpAuthRealm( host, port, - this.authScheme.getRealm(), - this.authScheme.getSchemeName()); + authscheme.getRealm(), + authscheme.getSchemeName()); - if ((this.authState == AUTH_WWW_REQUIRED) - && (this.authScheme.isComplete())) { + if ((this.authProcess == AUTH_WWW_REQUIRED) && (authscheme.isComplete())) { // Already tried and failed - Credentials credentials = promptForCredentials(method.getParams(), realm); + Credentials credentials = promptForCredentials( + authscheme, method.getParams(), realm); if (credentials == null) { if (LOG.isInfoEnabled()) { LOG.info("Failure authenticating with " + realm); @@ -614,11 +631,12 @@ return true; } } else { - this.authState = AUTH_WWW_REQUIRED; + this.authProcess = AUTH_WWW_REQUIRED; Credentials credentials = this.state.getCredentials(realm); if (credentials == null) { - credentials = promptForCredentials(method.getParams(), realm); + credentials = promptForCredentials( + authscheme, method.getParams(), realm); } if (credentials == null) { if (LOG.isInfoEnabled()) { @@ -634,32 +652,35 @@ private boolean processProxyAuthChallenge(final HttpMethod method) throws MalformedChallengeException, AuthenticationException { - if (this.proxyAuthPreemptive) { - this.proxyAuthScheme = null; - this.proxyAuthPreemptive = false; + AuthState authstate = method.getProxyAuthState(); + if (authstate.isPreemptive()) { + authstate.invalidate(); } Map proxyChallenges = AuthChallengeParser.parseChallenges( method.getResponseHeaders(PROXY_AUTH_CHALLENGE)); if (proxyChallenges.isEmpty()) { return false; } - if (this.proxyAuthScheme != null) { - processChallenge(this.proxyAuthScheme, proxyChallenges); - } else { - this.proxyAuthScheme = processChallenge(proxyChallenges); + AuthScheme authscheme = null; + try { + authscheme = this.authProcessor.processChallenge(authstate, proxyChallenges); + } catch (AuthChallengeException e) { + if (LOG.isWarnEnabled()) { + LOG.warn(e.getMessage()); + } } - if (this.proxyAuthScheme == null) { + if (authscheme == null) { return false; } HttpAuthRealm realm = new HttpAuthRealm( conn.getProxyHost(), conn.getProxyPort(), - this.proxyAuthScheme.getRealm(), - this.proxyAuthScheme.getSchemeName()); + authscheme.getRealm(), + authscheme.getSchemeName()); - if ((this.authState == AUTH_PROXY_REQUIRED) - && (this.proxyAuthScheme.isComplete())) { + if ((this.authProcess == AUTH_PROXY_REQUIRED) && (authscheme.isComplete())) { // Already tried and failed - Credentials credentials = promptForProxyCredentials(method.getParams(), realm); + Credentials credentials = promptForProxyCredentials( + authscheme, method.getParams(), realm); if (credentials == null) { if (LOG.isInfoEnabled()) { LOG.info("Failure authenticating with " + realm); @@ -669,11 +690,12 @@ return true; } } else { - this.authState = AUTH_PROXY_REQUIRED; + this.authProcess = AUTH_PROXY_REQUIRED; Credentials credentials = this.state.getProxyCredentials(realm); if (credentials == null) { - credentials = promptForProxyCredentials(method.getParams(), realm); + credentials = promptForProxyCredentials( + authscheme, method.getParams(), realm); } if (credentials == null) { if (LOG.isInfoEnabled()) { @@ -686,71 +708,6 @@ } } - private void processChallenge(final AuthScheme authscheme, final Map challenges) - throws MalformedChallengeException, AuthenticationException - { - String id = authscheme.getSchemeName(); - if (LOG.isDebugEnabled()) { - LOG.debug("Using present authentication scheme: " + id); - } - String challenge = (String) challenges.get(id.toLowerCase()); - if (challenge == null) { - throw new AuthenticationException(id + - " authorization challenge expected, but not found"); - } - authscheme.processChallenge(challenge); - } - - private AuthScheme processChallenge(final Map challenges) - throws MalformedChallengeException, AuthenticationException { - - Collection authPrefs = (Collection) this.params.getParameter( - AuthPolicy.AUTH_SCHEME_PRIORITY); - if (authPrefs == null || authPrefs.isEmpty()) { - authPrefs = AuthPolicy.getDefaultAuthPrefs(); - } - - AuthScheme authscheme = null; - String challenge = null; - if (LOG.isDebugEnabled()) { - LOG.debug("Supported authentication schemes in the order of preference: " - + authPrefs); - } - Iterator item = authPrefs.iterator(); - while (item.hasNext()) { - String id = (String) item.next(); - challenge = (String) challenges.get(id.toLowerCase()); - if (challenge != null) { - if (LOG.isInfoEnabled()) { - LOG.info(id + " authentication scheme selected"); - } - authscheme = AuthPolicy.getAuthScheme(id); - if (authscheme == null) { - throw new AuthenticationException("Requested authorization scheme " + - id + " is not supported"); - } - // Looks like we got something - break; - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Challenge for " + id + " authentication scheme not available"); - // Try again - } - } - } - if (authscheme == null) { - // If none selected, something is wrong, warn and leave - if (LOG.isWarnEnabled()) { - LOG.warn("None of the following challenges can be responsed to: " - + challenges); - } - } else { - // Process the challenge - authscheme.processChallenge(challenge); - } - return authscheme; - } - /** * Tests if the {@link HttpMethod method} requires a redirect to another location. * @@ -802,6 +759,7 @@ } private Credentials promptForCredentials( + final AuthScheme authScheme, final HttpParams params, final HttpAuthRealm realm) { @@ -811,7 +769,7 @@ if (credProvider != null) { try { creds = credProvider.getCredentials( - this.authScheme, realm.getHost(), realm.getPort(), false); + authScheme, realm.getHost(), realm.getPort(), false); } catch (CredentialsNotAvailableException e) { LOG.warn(e.getMessage()); } @@ -826,6 +784,7 @@ } private Credentials promptForProxyCredentials( + final AuthScheme authScheme, final HttpParams params, final HttpAuthRealm realm) { @@ -835,7 +794,7 @@ if (credProvider != null) { try { creds = credProvider.getCredentials( - this.proxyAuthScheme, realm.getHost(), realm.getPort(), true); + authScheme, realm.getHost(), realm.getPort(), true); } catch (CredentialsNotAvailableException e) { LOG.warn(e.getMessage()); } @@ -875,33 +834,5 @@ */ public HttpParams getParams() { return this.params; - } - - /** - * Returns the authentication scheme used to authenticate with the - * target host. - * - * @return the authentication scheme used to authenticate with the - * target host. Null is returned if target host authentication - * was not required. - * - * @since 2.1 - */ - public AuthScheme getAuthScheme() { - return authScheme; - } - - /** - * Returns the authentication scheme used to authenticate with the - * proxy host. - * - * @return the authentication scheme used to authenticate with the - * proxy host. Null is returned if proxy authentication was not - * required. - * - * @since 2.1 - */ - public AuthScheme getProxyAuthScheme() { - return proxyAuthScheme; } } Index: java/org/apache/commons/httpclient/auth/AuthChallengeException.java =================================================================== RCS file: java/org/apache/commons/httpclient/auth/AuthChallengeException.java diff -N java/org/apache/commons/httpclient/auth/AuthChallengeException.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ java/org/apache/commons/httpclient/auth/AuthChallengeException.java 18 Mar 2004 21:11:18 -0000 @@ -0,0 +1,70 @@ +/* + * $Header: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/auth/AuthenticationException.java,v 1.4 2004/02/22 18:08:47 olegk Exp $ + * $Revision: 1.4 $ + * $Date: 2004/02/22 18:08:47 $ + * + * ==================================================================== + * + * Copyright 2002-2004 The Apache Software Foundation + * + * Licensed 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 + * . + * + * [Additional notices, if required by prior licensing conditions] + * + */ + +package org.apache.commons.httpclient.auth; + +/** + * Signals a failure processing authentication challenge + * + * @author Oleg Kalnichevski + * + * @since 3.0 + */ +public class AuthChallengeException extends AuthenticationException { + + /** + * Creates a new AuthChallengeException with a null detail message. + */ + public AuthChallengeException() { + super(); + } + + /** + * Creates a new AuthChallengeException with the specified message. + * + * @param message the exception detail message + */ + public AuthChallengeException(String message) { + super(message); + } + + /** + * Creates a new AuthChallengeException with the specified detail message and cause. + * + * @param message the exception detail message + * @param cause the Throwable that caused this exception, or null + * if the cause is unavailable, unknown, or not a Throwable + */ + public AuthChallengeException(String message, Throwable cause) { + super(message, cause); + } + +} Index: java/org/apache/commons/httpclient/auth/AuthChallengeProcessor.java =================================================================== RCS file: java/org/apache/commons/httpclient/auth/AuthChallengeProcessor.java diff -N java/org/apache/commons/httpclient/auth/AuthChallengeProcessor.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ java/org/apache/commons/httpclient/auth/AuthChallengeProcessor.java 18 Mar 2004 21:11:18 -0000 @@ -0,0 +1,165 @@ +/* + * $Header$ + * $Revision$ + * $Date$ + * + * ==================================================================== + * + * Copyright 2002-2004 The Apache Software Foundation + * + * Licensed 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 + * . + * + * [Additional notices, if required by prior licensing conditions] + * + */ + +package org.apache.commons.httpclient.auth; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +import org.apache.commons.httpclient.params.HttpParams; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class provides utility methods for processing HTTP www and proxy authentication + * challenges. + * + * @author Oleg Kalnichevski + * + * @since 3.0 + */ +public final class AuthChallengeProcessor { + + private static final Log LOG = LogFactory.getLog(AuthChallengeProcessor.class); + + private HttpParams params = null; + + /** + * Creates an authentication challenge processor with the given {@link HttpParams HTTP + * parameters} + * + * @param params the {@link HttpParams HTTP parameters} used by this processor + */ + public AuthChallengeProcessor(final HttpParams params) { + super(); + if (params == null) { + throw new IllegalArgumentException("Parameter collection may not be null"); + } + this.params = params; + } + + /** + * Determines the preferred {@link AuthScheme authentication scheme} that can be used + * to respond to the given collection of challenges. + * + * @param challenges the collection of authentication challenges + * + * @return the preferred {@link AuthScheme authentication scheme} + * + * @throws AuthChallengeException if the preferred authentication scheme + * cannot be determined or is not supported + */ + public AuthScheme selectAuthScheme(final Map challenges) throws AuthChallengeException { + if (challenges == null) { + throw new IllegalArgumentException("Challenge map may not be null"); + } + Collection authPrefs = (Collection) this.params.getParameter( + AuthPolicy.AUTH_SCHEME_PRIORITY); + if (authPrefs == null || authPrefs.isEmpty()) { + authPrefs = AuthPolicy.getDefaultAuthPrefs(); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Supported authentication schemes in the order of preference: " + + authPrefs); + } + AuthScheme authscheme = null; + String challenge = null; + Iterator item = authPrefs.iterator(); + while (item.hasNext()) { + String id = (String) item.next(); + challenge = (String) challenges.get(id.toLowerCase()); + if (challenge != null) { + if (LOG.isInfoEnabled()) { + LOG.info(id + " authentication scheme selected"); + } + try { + authscheme = AuthPolicy.getAuthScheme(id); + } catch (IllegalStateException e) { + throw new AuthChallengeException(e.getMessage()); + } + break; + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("Challenge for " + id + " authentication scheme not available"); + // Try again + } + } + } + if (authscheme == null) { + // If none selected, something is wrong + throw new AuthChallengeException( + "Unable to respond to any of these challenges: " + + challenges); + } + return authscheme; + } + + /** + * Processes the given collection of challenges and updates the + * {@link AuthState state} of the authentication process. + * + * @param challenges the collection of authentication challenges + * + * @return the {@link AuthScheme authentication scheme} used to + * process the challenge + * + * @throws AuthChallengeException if authentication challenges cannot be + * successfully processed or the preferred authentication scheme cannot + * be determined + */ + public AuthScheme processChallenge(final AuthState state, final Map challenges) + throws MalformedChallengeException, AuthenticationException + { + if (state == null) { + throw new IllegalArgumentException("Authentication state may not be null"); + } + if (challenges == null) { + throw new IllegalArgumentException("Challenge map may not be null"); + } + if (state.getAuthScheme() == null) { + // Authentication not attempted before + state.setAuthScheme(selectAuthScheme(challenges)); + } + AuthScheme authscheme = state.getAuthScheme(); + String id = authscheme.getSchemeName(); + if (LOG.isDebugEnabled()) { + LOG.debug("Using authentication scheme: " + id); + } + String challenge = (String) challenges.get(id.toLowerCase()); + if (challenge == null) { + throw new AuthenticationException(id + + " authorization challenge expected, but not found"); + } + authscheme.processChallenge(challenge); + return authscheme; + } +} Index: java/org/apache/commons/httpclient/auth/AuthState.java =================================================================== RCS file: java/org/apache/commons/httpclient/auth/AuthState.java diff -N java/org/apache/commons/httpclient/auth/AuthState.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ java/org/apache/commons/httpclient/auth/AuthState.java 18 Mar 2004 21:11:18 -0000 @@ -0,0 +1,117 @@ +/* + * $Header$ + * $Revision$ + * $Date$ + * + * ==================================================================== + * + * Copyright 1999-2004 The Apache Software Foundation + * + * Licensed 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 + * . + * + * [Additional notices, if required by prior licensing conditions] + * + */ + +package org.apache.commons.httpclient.auth; + +/** + * This class provides detailed information about the state of the + * authentication process. + * + * @author Oleg Kalnichevski + * @since 3.0 + */ +public class AuthState { + + /** Actual authentication scheme */ + private AuthScheme authScheme = null; + + /** Whether preemtive authentication is attempted */ + private boolean preemptive = false; + + /** + * Default constructor. + * + */ + public AuthState() { + super(); + } + + /** + * Preemptively assigns Basic authentication scheme. + */ + public void setPreemptive() { + if (this.authScheme != null) { + throw new IllegalStateException("Authentication state already initialized"); + } + this.authScheme = AuthPolicy.getAuthScheme("basic"); + this.preemptive = true; + } + + /** + * Invalidates the authentication state by resetting its parameters. + */ + public void invalidate() { + this.authScheme = null; + this.preemptive = false; + } + + /** + * Tests if preemptive authentication is used. + * + * @return true if using the default Basic {@link AuthScheme + * authentication scheme}, false otherwise. + */ + public boolean isPreemptive() { + return this.preemptive; + } + + /** + * Assigns the given {@link AuthScheme authentication scheme}. + * + * @param authScheme the {@link AuthScheme authentication scheme} + */ + public void setAuthScheme(final AuthScheme authScheme) { + this.authScheme = authScheme; + this.preemptive = false; + } + + /** + * Returns the {@link AuthScheme authentication scheme}. + * + * @return {@link AuthScheme authentication scheme} + */ + public AuthScheme getAuthScheme() { + return authScheme; + } + + /** + * Returns the authentication realm. + * + * @return the name of the authentication realm + */ + public String getRealm() { + if (this.authScheme != null) { + return this.authScheme.getRealm(); + } else { + return null; + } + } +} Index: test/org/apache/commons/httpclient/TestChallengeParser.java =================================================================== RCS file: test/org/apache/commons/httpclient/TestChallengeParser.java diff -N test/org/apache/commons/httpclient/TestChallengeParser.java --- test/org/apache/commons/httpclient/TestChallengeParser.java 22 Feb 2004 18:08:49 -0000 1.3 +++ /dev/null 1 Jan 1970 00:00:00 -0000 @@ -1,80 +0,0 @@ -/* - * ==================================================================== - * - * Copyright 1999-2004 The Apache Software Foundation - * - * Licensed 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 - * . - * - * [Additional notices, if required by prior licensing conditions] - * - */ - -package org.apache.commons.httpclient; - -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; -import java.util.Map; -import org.apache.commons.httpclient.auth.AuthChallengeParser; -import org.apache.commons.httpclient.auth.MalformedChallengeException; - -/** - * Unit tests for {@link AuthChallengeParser}. - * - * @author Oleg Kalnichevski - */ -public class TestChallengeParser extends TestCase { - - // ------------------------------------------------------------ Constructor - public TestChallengeParser(String testName) { - super(testName); - } - - // ------------------------------------------------------------------- Main - public static void main(String args[]) { - String[] testCaseName = { TestChallengeParser.class.getName() }; - junit.textui.TestRunner.main(testCaseName); - } - - // ------------------------------------------------------- TestCase Methods - - public static Test suite() { - return new TestSuite(TestChallengeParser.class); - } - - - public void testParsingChallenge() { - String challenge = - "Basic realm=\"realm1\", test, test1 = stuff, test2 = \"stuff, stuff\", test3=\"crap"; - String scheme = null; - Map elements = null; - try { - scheme = AuthChallengeParser.extractScheme(challenge); - elements = AuthChallengeParser.extractParams(challenge); - } catch (MalformedChallengeException e) { - fail("Unexpected exception: " + e.toString()); - } - assertEquals("basic", scheme); - assertEquals("realm1", elements.get("realm")); - assertEquals(null, elements.get("test")); - assertEquals("stuff", elements.get("test1")); - assertEquals("stuff, stuff", elements.get("test2")); - assertEquals("\"crap", elements.get("test3")); - } -} Index: test/org/apache/commons/httpclient/TestExceptions.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestExceptions.java,v retrieving revision 1.3 diff -u -r1.3 TestExceptions.java --- test/org/apache/commons/httpclient/TestExceptions.java 22 Feb 2004 18:08:49 -0000 1.3 +++ test/org/apache/commons/httpclient/TestExceptions.java 18 Mar 2004 21:11:19 -0000 @@ -34,6 +34,8 @@ import java.io.PrintWriter; import java.io.StringWriter; +import org.apache.commons.httpclient.auth.*; + import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; Index: test/org/apache/commons/httpclient/TestNoHost.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestNoHost.java,v retrieving revision 1.32 diff -u -r1.32 TestNoHost.java --- test/org/apache/commons/httpclient/TestNoHost.java 27 Feb 2004 19:01:33 -0000 1.32 +++ test/org/apache/commons/httpclient/TestNoHost.java 18 Mar 2004 21:11:20 -0000 @@ -35,6 +35,8 @@ import junit.framework.TestSuite; import org.apache.commons.httpclient.auth.TestBasicAuth; +import org.apache.commons.httpclient.auth.TestChallengeParser; +import org.apache.commons.httpclient.auth.TestChallengeProcessor; /** * Tests that don't require any external host. @@ -61,6 +63,7 @@ suite.addTest(TestParameterParser.suite()); suite.addTest(TestHeaderElement.suite()); suite.addTest(TestChallengeParser.suite()); + suite.addTest(TestChallengeProcessor.suite()); suite.addTest(TestAuthenticator.suite()); suite.addTest(TestBasicAuth.suite()); suite.addTest(TestHttpUrlMethod.suite()); Index: test/org/apache/commons/httpclient/auth/TestBasicAuth.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/auth/TestBasicAuth.java,v retrieving revision 1.1 diff -u -r1.1 TestBasicAuth.java --- test/org/apache/commons/httpclient/auth/TestBasicAuth.java 27 Feb 2004 19:04:32 -0000 1.1 +++ test/org/apache/commons/httpclient/auth/TestBasicAuth.java 18 Mar 2004 21:11:21 -0000 @@ -216,6 +216,10 @@ this.client.executeMethod(httpget); assertNotNull(httpget.getStatusLine()); assertEquals(HttpStatus.SC_UNAUTHORIZED, httpget.getStatusLine().getStatusCode()); + AuthState authstate = httpget.getHostAuthState(); + assertNotNull(authstate.getAuthScheme()); + assertTrue(authstate.getAuthScheme() instanceof BasicScheme); + assertEquals("test", authstate.getRealm()); } finally { httpget.releaseConnection(); } @@ -262,6 +266,10 @@ String expected = "Basic " + EncodingUtil.getAsciiString( Base64.encodeBase64(EncodingUtil.getAsciiBytes("test:test"))); assertEquals(expected, auth.getValue()); + AuthState authstate = httpget.getHostAuthState(); + assertNotNull(authstate.getAuthScheme()); + assertTrue(authstate.getAuthScheme() instanceof BasicScheme); + assertEquals("test", authstate.getRealm()); } public void testBasicAuthentication() throws Exception { @@ -286,6 +294,10 @@ String expected = "Basic " + EncodingUtil.getAsciiString( Base64.encodeBase64(EncodingUtil.getAsciiBytes("test:test"))); assertEquals(expected, auth.getValue()); + AuthState authstate = httpget.getHostAuthState(); + assertNotNull(authstate.getAuthScheme()); + assertTrue(authstate.getAuthScheme() instanceof BasicScheme); + assertEquals("test", authstate.getRealm()); } public void testBasicAuthenticationWithInvalidCredentials() throws Exception { @@ -305,6 +317,10 @@ } assertNotNull(httpget.getStatusLine()); assertEquals(HttpStatus.SC_FORBIDDEN, httpget.getStatusLine().getStatusCode()); + AuthState authstate = httpget.getHostAuthState(); + assertNotNull(authstate.getAuthScheme()); + assertTrue(authstate.getAuthScheme() instanceof BasicScheme); + assertEquals("test", authstate.getRealm()); } public void testBasicAuthenticationWithMutlipleRealms() throws Exception { @@ -335,6 +351,10 @@ String expected = "Basic " + EncodingUtil.getAsciiString( Base64.encodeBase64(EncodingUtil.getAsciiBytes("test:test"))); assertEquals(expected, auth.getValue()); + AuthState authstate = httpget.getHostAuthState(); + assertNotNull(authstate.getAuthScheme()); + assertTrue(authstate.getAuthScheme() instanceof BasicScheme); + assertEquals("test", authstate.getRealm()); } { this.server.setHttpService(new BasicAuthService2()); @@ -351,6 +371,10 @@ String expected = "Basic " + EncodingUtil.getAsciiString( Base64.encodeBase64(EncodingUtil.getAsciiBytes("test2:test2"))); assertEquals(expected, auth.getValue()); + AuthState authstate = httpget.getHostAuthState(); + assertNotNull(authstate.getAuthScheme()); + assertTrue(authstate.getAuthScheme() instanceof BasicScheme); + assertEquals("test2", authstate.getRealm()); } } @@ -373,6 +397,11 @@ String expected = "Basic " + EncodingUtil.getAsciiString( Base64.encodeBase64(EncodingUtil.getAsciiBytes("test:test"))); assertEquals(expected, auth.getValue()); + AuthState authstate = httpget.getHostAuthState(); + assertNotNull(authstate.getAuthScheme()); + assertTrue(authstate.getAuthScheme() instanceof BasicScheme); + assertNull(authstate.getRealm()); + assertTrue(authstate.isPreemptive()); } public void testBasicAuthenticationCaseInsensitivity() throws Exception { @@ -397,5 +426,21 @@ String expected = "Basic " + EncodingUtil.getAsciiString( Base64.encodeBase64(EncodingUtil.getAsciiBytes("test:test"))); assertEquals(expected, auth.getValue()); + } + + + public void testCustomAuthorizationHeader() throws Exception { + String authResponse = "Basic " + EncodingUtil.getAsciiString( + Base64.encodeBase64(EncodingUtil.getAsciiBytes("test:test"))); + this.server.setHttpService(new BasicAuthService()); + GetMethod httpget = new GetMethod("/test/"); + httpget.addRequestHeader(new Header("Authorization", authResponse)); + try { + this.client.executeMethod(httpget); + } finally { + httpget.releaseConnection(); + } + assertNotNull(httpget.getStatusLine()); + assertEquals(HttpStatus.SC_OK, httpget.getStatusLine().getStatusCode()); } } Index: test/org/apache/commons/httpclient/auth/TestChallengeParser.java =================================================================== RCS file: test/org/apache/commons/httpclient/auth/TestChallengeParser.java diff -N test/org/apache/commons/httpclient/auth/TestChallengeParser.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ test/org/apache/commons/httpclient/auth/TestChallengeParser.java 18 Mar 2004 21:11:21 -0000 @@ -0,0 +1,83 @@ +/* + * $Header$ + * $Revision$ + * $Date$ + * ==================================================================== + * + * Copyright 1999-2004 The Apache Software Foundation + * + * Licensed 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 + * . + * + * [Additional notices, if required by prior licensing conditions] + * + */ + +package org.apache.commons.httpclient.auth; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import java.util.Map; +import org.apache.commons.httpclient.auth.AuthChallengeParser; +import org.apache.commons.httpclient.auth.MalformedChallengeException; + +/** + * Unit tests for {@link AuthChallengeParser}. + * + * @author Oleg Kalnichevski + */ +public class TestChallengeParser extends TestCase { + + // ------------------------------------------------------------ Constructor + public TestChallengeParser(String testName) { + super(testName); + } + + // ------------------------------------------------------------------- Main + public static void main(String args[]) { + String[] testCaseName = { TestChallengeParser.class.getName() }; + junit.textui.TestRunner.main(testCaseName); + } + + // ------------------------------------------------------- TestCase Methods + + public static Test suite() { + return new TestSuite(TestChallengeParser.class); + } + + + public void testParsingChallenge() { + String challenge = + "Basic realm=\"realm1\", test, test1 = stuff, test2 = \"stuff, stuff\", test3=\"crap"; + String scheme = null; + Map elements = null; + try { + scheme = AuthChallengeParser.extractScheme(challenge); + elements = AuthChallengeParser.extractParams(challenge); + } catch (MalformedChallengeException e) { + fail("Unexpected exception: " + e.toString()); + } + assertEquals("basic", scheme); + assertEquals("realm1", elements.get("realm")); + assertEquals(null, elements.get("test")); + assertEquals("stuff", elements.get("test1")); + assertEquals("stuff, stuff", elements.get("test2")); + assertEquals("\"crap", elements.get("test3")); + } +} Index: test/org/apache/commons/httpclient/auth/TestChallengeProcessor.java =================================================================== RCS file: test/org/apache/commons/httpclient/auth/TestChallengeProcessor.java diff -N test/org/apache/commons/httpclient/auth/TestChallengeProcessor.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ test/org/apache/commons/httpclient/auth/TestChallengeProcessor.java 18 Mar 2004 21:11:22 -0000 @@ -0,0 +1,173 @@ +/* + * $Header$ + * $Revision$ + * $Date$ + * ==================================================================== + * + * Copyright 1999-2004 The Apache Software Foundation + * + * Licensed 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 + * . + * + * [Additional notices, if required by prior licensing conditions] + * + */ + +package org.apache.commons.httpclient.auth; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.apache.commons.httpclient.params.DefaultHttpParams; +import org.apache.commons.httpclient.params.HttpParams; + +/** + * Unit tests for {@link testParsingChallenge}. + * + * @author Oleg Kalnichevski + */ +public class TestChallengeProcessor extends TestCase { + + // ------------------------------------------------------------ Constructor + public TestChallengeProcessor(String testName) { + super(testName); + } + + // ------------------------------------------------------------------- Main + public static void main(String args[]) { + String[] testCaseName = { TestChallengeProcessor.class.getName() }; + junit.textui.TestRunner.main(testCaseName); + } + + // ------------------------------------------------------- TestCase Methods + + public static Test suite() { + return new TestSuite(TestChallengeProcessor.class); + } + + + public void testChallengeSelection() throws Exception { + List authPrefs = new ArrayList(3); + authPrefs.add(AuthPolicy.NTLM); + authPrefs.add(AuthPolicy.DIGEST); + authPrefs.add(AuthPolicy.BASIC); + HttpParams httpparams = new DefaultHttpParams(); + httpparams.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs); + + AuthChallengeProcessor processor = new AuthChallengeProcessor(httpparams); + + Map map = new HashMap(); + map.put("unknown", "unknown realm=\"whatever\""); + map.put("basic", "basic realm=\"whatever\""); + + AuthScheme authscheme = processor.selectAuthScheme(map); + assertTrue(authscheme instanceof BasicScheme); + } + + + public void testInvalidChallenge() throws Exception { + List authPrefs = new ArrayList(3); + authPrefs.add("unsupported1"); + authPrefs.add("unsupported2"); + HttpParams httpparams = new DefaultHttpParams(); + httpparams.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs); + + AuthChallengeProcessor processor = new AuthChallengeProcessor(httpparams); + + Map map = new HashMap(); + map.put("unsupported1", "unsupported1 realm=\"whatever\""); + map.put("unsupported2", "unsupported2 realm=\"whatever\""); + try { + AuthScheme authscheme = processor.selectAuthScheme(map); + fail("AuthChallengeException should have been thrown"); + } catch (AuthChallengeException e) { + //ignore + } + } + + + public void testUnsupportedChallenge() throws Exception { + List authPrefs = new ArrayList(3); + authPrefs.add(AuthPolicy.NTLM); + authPrefs.add(AuthPolicy.BASIC); + authPrefs.add(AuthPolicy.DIGEST); + HttpParams httpparams = new DefaultHttpParams(); + httpparams.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs); + + AuthChallengeProcessor processor = new AuthChallengeProcessor(httpparams); + + Map map = new HashMap(); + map.put("unsupported1", "unsupported1 realm=\"whatever\""); + map.put("unsupported2", "unsupported2 realm=\"whatever\""); + + try { + AuthScheme authscheme = processor.selectAuthScheme(map); + fail("AuthChallengeException should have been thrown"); + } catch (AuthChallengeException e) { + //expected + } + } + + public void testChallengeProcessing() throws Exception { + HttpParams httpparams = new DefaultHttpParams(); + AuthChallengeProcessor processor = new AuthChallengeProcessor(httpparams); + + Map map = new HashMap(); + map.put("basic", "basic realm=\"whatever\", param=\"value\""); + + AuthState authstate = new AuthState(); + + AuthScheme authscheme = processor.processChallenge(authstate, map); + assertTrue(authscheme instanceof BasicScheme); + assertEquals("whatever", authscheme.getRealm()); + assertEquals(authscheme, authstate.getAuthScheme()); + assertEquals("value", authscheme.getParameter("param")); + } + + public void testInvalidChallengeProcessing() throws Exception { + HttpParams httpparams = new DefaultHttpParams(); + AuthChallengeProcessor processor = new AuthChallengeProcessor(httpparams); + + Map map = new HashMap(); + map.put("basic", "basic realm=\"whatever\", param=\"value\""); + + AuthState authstate = new AuthState(); + + AuthScheme authscheme = processor.processChallenge(authstate, map); + assertTrue(authscheme instanceof BasicScheme); + assertEquals("whatever", authscheme.getRealm()); + assertEquals(authscheme, authstate.getAuthScheme()); + assertEquals("value", authscheme.getParameter("param")); + + Map map2 = new HashMap(); + map2.put("ntlm", "NTLM"); + try { + // Basic authentication scheme expected + authscheme = processor.processChallenge(authstate, map2); + fail("AuthenticationException should have been thrown"); + } catch (AuthenticationException e) { + //expected + } + } +}