? PreemptiveAuthorization.patch Index: java/org/apache/commons/httpclient/Authenticator.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/Authenticator.java,v retrieving revision 1.17 diff -u -r1.17 Authenticator.java --- java/org/apache/commons/httpclient/Authenticator.java 16 Jul 2002 13:52:57 -0000 1.17 +++ java/org/apache/commons/httpclient/Authenticator.java 16 Jul 2002 22:32:56 -0000 @@ -69,24 +69,49 @@ import java.security.MessageDigest; /** - *

Utility methods for HTTP authorization and authentication.

- *

+ * Utility methods for HTTP authorization and authentication. + * * This class provides utility methods for generating * responses to HTTP www and proxy authentication challenges. - *

+ *

+ * Preemptive authentication can be turned on by using the property value of + * #PREEMPTIVE_PROPERTY. If left unspecified, it has the default value of + * #PREEMPTIVE_DEFAULT. This configurable behaviour conforms to rcf2617: + *

+ * A client SHOULD assume that all paths at or deeper than the depth of + * the last symbolic element in the path field of the Request-URI also + * are within the protection space specified by the Basic realm value of + * the current challenge. A client MAY preemptively send the + * corresponding Authorization header with requests for resources in + * that space without receipt of another challenge from the server. + * Similarly, when a client sends a request to a proxy, it may reuse a + * userid and password in the Proxy-Authorization header field without + * receiving another challenge from the proxy server. + *
+ * * @author Remy Maucherat * @author Rodney Waldhoff - * @author Jeff Dever + * @author Jeff Dever * @version $Revision: 1.17 $ $Date: 2002/07/16 13:52:57 $ */ class Authenticator { - + /** org.apache.commons.httpclient.Authenticator log. */ private static final Log log = LogSource.getInstance("org.apache.commons.httpclient.Authenticator"); /** Base 64 encoder. */ private static Base64 base64 = new Base64(); + /** + * The boolean property name to turn on preemptive authentication. + */ + static final String PREEMPTIVE_PROPERTY = "httpclient.authentication.preemptive"; + + /** + * The default property value for #PREEMPTIVE_PROPERTY. + */ + static final String PREEMPTIVE_DEFAULT = "false"; + /** * The www authenticate challange header */ @@ -107,138 +132,157 @@ */ public static final String PROXY_AUTH_RESP = "Proxy-Authorization"; + /** - * Add requisite authentication credentials to the given - * {@link HttpMethod}, if possible. + * Add requisite authentication credentials to the given method + * in the given state if possible. * - * @see HttpState#setCredentials(String, Credentials) HttpState.setCredentials - * @see #authenticate(HttpMethod,HttpState,Header,String) - * - * @param method a {@link HttpMethod} which requires authentication - * @param state a {@link HttpState} object providing {@link Credentials} + * @param method the HttpMethod which requires authentication + * @param state the HttpState object providing Credentials * * @throws HttpException when a parsing or other error occurs - * @throws UnsupportedOperationException when the given challenge type is not supported - * @return true if only if a response header was added + * @throws UnsupportedOperationException when the challenge type is not supported + * @return true if the Authenticate response header was added + * + * @see HttpState#setCredentials(String,Credentials) + * @see #authenticate(HttpMethod,HttpState,Header,String) */ static boolean authenticate(HttpMethod method, HttpState state) throws HttpException, UnsupportedOperationException { - log.debug("Authenticator.authenticate(HttpMethod, HttpState)"); + log.debug("enter Authenticator.authenticate(HttpMethod, HttpState)"); + Header challengeHeader = method.getResponseHeader(WWW_AUTH); - Header challengeHeader = method.getResponseHeader(WWW_AUTH); - if(null == challengeHeader) { return false; } - - return authenticate(method, state, challengeHeader, WWW_AUTH_RESP); - } + return authenticate(method, state, challengeHeader, WWW_AUTH_RESP); + } /** - * Add requisite proxy authentication credentials to the given - * {@link HttpMethod}, if possible. + * Add requisite proxy authentication credentials to the given method + * in the given state if possible. * - * @see HttpState#setProxyCredentials(String, Credentials) HttpState.setProxyCredentials - * @see #authenticate(HttpMethod,HttpState,Header,String) - * - * @param method a {@link HttpMethod} which requires authentication - * @param state a {@link HttpState} object providing {@link Credentials} + * @param method the HttpMethod which requires authentication + * @param state the HttpState object providing Credentials * * @throws HttpException when a parsing or other error occurs * @throws UnsupportedOperationException when the given challenge type is not supported - * @return true if only if a response header was added + * @return true if the Authenticate response header was added + * + * @see HttpState#setProxyCredentials(String,Credentials) + * @see #authenticate(HttpMethod,HttpState,Header,String) */ static boolean authenticateProxy(HttpMethod method, HttpState state) throws HttpException, UnsupportedOperationException { - log.debug("Authenticator.authenticateProxy(HttpMethod, HttpState)"); + log.debug("enter Authenticator.authenticateProxy(HttpMethod, HttpState)"); + Header challengeHeader = method.getResponseHeader(PROXY_AUTH); + + return authenticate(method, state, challengeHeader, PROXY_AUTH_RESP); + } - Header challengeHeader = method.getResponseHeader(PROXY_AUTH); - if (null == challengeHeader) { return false; } - return authenticate(method, state, challengeHeader, PROXY_AUTH_RESP); - } /** * Add requisite authentication credentials to the given - * {@link HttpMethod}, if possible, using the given response header + * method using the given the challengeHeader. * - * Currently only Basic authentication is supported. - * - * @param method the {@link HttpMethod http method} to add the authentication - * details to - * @param challengeHeader the header the web server created to challenge the - * credentials - * @param state a {@link HttpState} object providing {@link Credentials} + * Currently Basic and Digest authentication are supported. + * If the challengeHeader is null, the default authentication credentials + * will be sent. + * + * @param method the http method to add the authentication header to + * @param challengeHeader the header the web server created to challenge the credentials + * @param state the http state object providing {@link Credentials} * @param respHeader the response header to add (e.g. proxy or standard) * * @throws HttpException when an error occurs parsing the challenge * @throws UnsupportedOperationException when the given challenge type is not supported - * @return true if only if a response header was added - */ - private static boolean authenticate(HttpMethod method, HttpState state, Header challengeHeader, String respHeader) - throws HttpException, UnsupportedOperationException { + * @return true if a response header was added + * + * @see #basic + * @see #digest + * @see HttpMethod#addRequestHeader + */ + private static boolean authenticate(HttpMethod method, HttpState state, Header challengeHeader, String respHeader) throws HttpException, UnsupportedOperationException { + log.debug("enter Authenticator.authenticate(HttpMethod, HttpState, Header, String)"); + + //check the preemptive policy + //TODO: this needs to be a service from some configuration class + String preemptive_str = System.getProperties().getProperty( + PREEMPTIVE_PROPERTY, PREEMPTIVE_DEFAULT); + preemptive_str = preemptive_str.trim().toLowerCase(); + + if (! (preemptive_str.equals("true") || preemptive_str.equals("false")) ){ //property problem + log.warn("Configuration property " + PREEMPTIVE_PROPERTY + + " must be either true or false. Using default: " + PREEMPTIVE_DEFAULT); + preemptive_str = PREEMPTIVE_DEFAULT; + } + boolean preemptive = ("true".equals(preemptive_str)); + log.debug("Using preemptive authorization: " + preemptive); + + + //set the header preemptively if necessisary + if (challengeHeader == null){ + if (preemptive){ + Header requestHeader = Authenticator.basic(null, state, respHeader); + if(requestHeader != null) { // default credentials exist, add the header + log.debug("Preemptively sending default basic credentials"); + method.addRequestHeader(requestHeader); + return true; + } else { // no default credentials, don't add the header + log.debug("No default credentials to preemptively send"); + return false; + } + } else { + return false; + } + } + log.debug("challenge header is: " + challengeHeader); + + // Get the challenge from the header String challenge = challengeHeader.getValue(); - if(null == challenge) { return false; } + + // Parse the authentication scheme from the challenge + // TODO: Use regular expression pattern matching to parse the challenge int space = challenge.indexOf(' '); if(space < 0) { - throw new HttpException("Unable to parse authentication challenge \"" + challenge + "\", expected space"); + throw new HttpException("Authentication challenge \'" + challenge + "\'does not contain an authentication scheme"); } String authScheme = challenge.substring(0, space); - if ("basic".equalsIgnoreCase(authScheme)) { - // FIXME: Note that this won't work if there - // is more than one realm within - // the challenge - // FIXME: We could probably make it a bit - // more flexible in parsing as well. - - // parse the realm from the authentication challenge - if(challenge.length() < space + 1) { - throw new HttpException("Unable to parse authentication challenge \"" + challenge + "\", expected realm"); - } - String realmstr = challenge.substring(space+1,challenge.length()); - realmstr.trim(); - if(realmstr.length()<"realm=\"\"".length()) { - throw new HttpException("Unable to parse authentication challenge \"" + challenge + "\", expected realm"); - } - String realm = realmstr.substring("realm=\"".length(),realmstr.length()-1); - log.debug("Parsed realm \"" + realm + "\" from challenge \"" + challenge + "\"."); - Header header = Authenticator.basic(realm, state, respHeader); - if(null != header) { - method.addRequestHeader(header); - return true; - } else { - return false; - } - } else if ("digest".equalsIgnoreCase(authScheme)) { - // FIXME: Note that this won't work if there - // is more than one realm within - // the challenge - // FIXME: We could probably make it a bit - // more flexible in parsing as well. - - // parse the realm from the authentication challenge - if(challenge.length() < space + 1) { - throw new HttpException("Unable to parse authentication challenge \"" + challenge + "\", expected realm"); - } - String realmstr = challenge.substring(space+1,challenge.length()); - realmstr.trim(); - if(realmstr.length()<"realm=\"\"".length()) { - throw new HttpException("Unable to parse authentication challenge \"" + challenge + "\", expected realm"); - } - String realm = realmstr.substring("realm=\"".length(),realmstr.length()-1); - log.debug("Parsed realm \"" + realm + "\" from challenge \"" + challenge + "\"."); - Header header = Authenticator.digest(realm, method, state, respHeader); - if(null != header) { - method.addRequestHeader(header); - return true; - } else { - return false; - } - } else { + // Parse the realm from the authentication challenge + // FIXME: Note that this won't work if there is more than one realm within the challenge + if (challenge.length() < space + 1) { + throw new HttpException("Unable to parse authentication challenge \"" + challenge + "\", expected realm"); + } + String realmstr = challenge.substring(space+1, challenge.length()); + realmstr.trim(); + if (realmstr.length() < "realm=\"\"".length()) { + throw new HttpException("Unable to parse authentication challenge \"" + challenge + "\", expected realm"); + } + String realm = realmstr.substring("realm=\"".length(), realmstr.length()-1); + log.debug("Parsed realm \"" + realm + "\" from challenge \"" + challenge + "\"."); + + // Check for the authentication type, and add header if necessisary + Header requestHeader = null; + if ("basic".equalsIgnoreCase(authScheme)) { // Basic authentication + requestHeader = Authenticator.basic(realm, state, respHeader); + + } else if ("digest".equalsIgnoreCase(authScheme)) { // Digest authentication + requestHeader = Authenticator.digest(realm, method, state, respHeader); + + } else { // unrecognized authentication throw new UnsupportedOperationException("Authentication type \"" + authScheme + "\" is not recognized."); } - } + + if(requestHeader != null) { // add the header + method.addRequestHeader(requestHeader); + return true; + } else { // don't add the header + return false; + } + } + /** * Create a Basic Authorization header for the given @@ -254,7 +298,8 @@ * @throws HttpException when no matching credentials are available */ static Header basic(String realm, HttpState state, String respHeader) throws HttpException { - log.debug("Authenticator.basic(String,HttpState)"); + + log.debug("enter Authenticator.basic(String, HttpState)"); boolean proxy = PROXY_AUTH_RESP.equals(respHeader); UsernamePasswordCredentials cred = null; try { @@ -264,20 +309,9 @@ } catch(ClassCastException e) { throw new HttpException("UsernamePasswordCredentials required for Basic authentication."); } + if(null == cred) { - if(log.isInfoEnabled()) { - log.info("No credentials found for realm \"" + realm + "\", attempting to use default credentials."); - } - try { - cred = (UsernamePasswordCredentials)( proxy ? - state.getProxyCredentials(null) : - state.getCredentials(null)); - } catch(ClassCastException e) { - throw new HttpException("UsernamePasswordCredentials required for Basic authentication."); - } - } - if(null == cred) { - throw new HttpException("No credentials available for the Basic authentication realm \"" + realm + "\"/"); + throw new HttpException("No credentials available for the Basic authentication realm \'" + realm + "\'"); } else { return new Header(respHeader, Authenticator.basic(cred)); } @@ -374,9 +408,9 @@ MessageDigest md5Helper; try { - md5Helper = MessageDigest.getInstance(digAlg); + md5Helper = MessageDigest.getInstance(digAlg); } catch (Exception e) { - System.out.println("ERROR! Unsupported algorithm in HTTP Digest authentication: "+digAlg); + log.error("ERROR! Unsupported algorithm in HTTP Digest authentication: "+digAlg); HttpException he = new HttpException("Unsupported algorithm in HTTP Digest authentication: "+digAlg); throw he; } @@ -389,7 +423,7 @@ String serverDigestValue; if (qop==null) serverDigestValue = md5a1 + ":" + nonce + ":" + md5a2; else serverDigestValue = md5a1 + ":" + nonce + ":" + nc + ":" + - cnonce + ":" + qop + ":" + md5a2; + cnonce + ":" + qop + ":" + md5a2; String serverDigest = encode(md5Helper.digest(serverDigestValue.getBytes())); return serverDigest; } @@ -406,9 +440,9 @@ String digAlg = "MD5"; MessageDigest md5Helper; try { - md5Helper = MessageDigest.getInstance(digAlg); + md5Helper = MessageDigest.getInstance(digAlg); } catch (Exception e) { - System.out.println("ERROR! Unsupported algorithm in HTTP Digest authentication: "+digAlg); + log.error("ERROR! Unsupported algorithm in HTTP Digest authentication: "+digAlg); HttpException he = new HttpException("Unsupported algorithm in HTTP Digest authentication: "+digAlg); throw he; } @@ -442,15 +476,15 @@ String algorithm = "MD5"; //we only support MD5 sb.append("username=\""+uname+"\"") - .append(", realm=\""+realm+"\"") - .append(", nonce=\""+nonce+"\"") - .append(", uri=\""+uri+"\"") - .append((qop==null?"":", qop=\""+qop+"\"")) - .append(", algorithm=\""+algorithm+"\"") - .append((qop==null?"":", nc="+nc)) - .append((qop==null?"":", cnonce=\""+cnonce+"\"")) - .append(", response=\""+response+"\"") - .append(", opaque=\""+opaque+"\""); + .append(", realm=\""+realm+"\"") + .append(", nonce=\""+nonce+"\"") + .append(", uri=\""+uri+"\"") + .append((qop==null?"":", qop=\""+qop+"\"")) + .append(", algorithm=\""+algorithm+"\"") + .append((qop==null?"":", nc="+nc)) + .append((qop==null?"":", cnonce=\""+cnonce+"\"")) + .append(", response=\""+response+"\"") + .append(", opaque=\""+opaque+"\""); return sb.toString(); } @@ -467,15 +501,15 @@ * str */ private static String removeQuotes(String str) { - if (str == null) - return null; + if (str == null) + return null; - int fqpos = str.indexOf("\"")+1; - int lqpos = str.lastIndexOf("\""); - if (fqpos > 0 && lqpos > fqpos) - return str.substring(fqpos,lqpos); - else - return str; + int fqpos = str.indexOf("\"")+1; + int lqpos = str.lastIndexOf("\""); + if (fqpos > 0 && lqpos > fqpos) + return str.substring(fqpos,lqpos); + else + return str; } /** @@ -488,19 +522,19 @@ */ private static String encode( byte[] binaryData ) { - if (binaryData.length != 16) - return null; + if (binaryData.length != 16) + return null; - char[] buffer = new char[32]; + char[] buffer = new char[32]; - for (int i=0; i<16; i++) { - int low = (int) (binaryData[i] & 0x0f); - int high = (int) ((binaryData[i] & 0xf0) >> 4); - buffer[i*2] = hexadecimal[high]; - buffer[i*2 + 1] = hexadecimal[low]; - } + for (int i=0; i<16; i++) { + int low = (int) (binaryData[i] & 0x0f); + int high = (int) ((binaryData[i] & 0xf0) >> 4); + buffer[i*2] = hexadecimal[high]; + buffer[i*2 + 1] = hexadecimal[low]; + } - return new String(buffer); + return new String(buffer); } /** @@ -509,9 +543,8 @@ * * @see #encode(byte[]) */ - private static final char[] hexadecimal = - {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'a', 'b', 'c', 'd', 'e', 'f'}; + private static final char[] hexadecimal = + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; /** * Processes the www-authenticate HTTP header received from the server that @@ -549,9 +582,12 @@ * entry is placed (only if it has "xxx=yyy" format). */ private static void processDigestToken(String token, Hashtable ht) { - int eqpos = token.indexOf("="); + int eqpos = token.indexOf("="); - if (eqpos > 0 && eqpos < token.length()-1) - ht.put(token.substring(0,eqpos).trim(),token.substring(eqpos+1).trim()); + if (eqpos > 0 && eqpos < token.length()-1) + ht.put(token.substring(0,eqpos).trim(),token.substring(eqpos+1).trim()); } + + + } 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.31 diff -u -r1.31 HttpMethodBase.java --- java/org/apache/commons/httpclient/HttpMethodBase.java 13 Jul 2002 09:06:29 -0000 1.31 +++ java/org/apache/commons/httpclient/HttpMethodBase.java 16 Jul 2002 22:32:57 -0000 @@ -112,7 +112,7 @@ * @author Rodney Waldhoff * @author Sean C. Sullivan * @author dIon Gillard - * @author Jeff Dever + * @author Jeff Dever * @version $Revision: 1.31 $ $Date: 2002/07/13 09:06:29 $ */ public abstract class HttpMethodBase implements HttpMethod { @@ -233,10 +233,14 @@ } /** - * Add the specified request header, NOT overwriting any - * previous value. + * Add the specified request header. + * + * If a header of the same name already exists, the new value will be + * appended onto the the existing value list. + * A header value of null will be ignored. * Note that header-name matching is case insensitive. - * @param header the header + * + * @param header the header to add to the request */ public void addRequestHeader(Header header) { // "It must be possible to combine the multiple header fields into @@ -244,6 +248,13 @@ // semantics of the message, by appending each subsequent field-value // to the first, each separated by a comma." // - HTTP/1.0 (4.3) + log.debug("HttpMethodBase.addRequestHeader(Header)"); + + if (header == null){ + log.debug("null header value ignored"); + } + + // Preserve the original header if it exists Header orig = (Header)(requestHeaders.get(header.getName().toLowerCase())); if (null == orig) { orig = header; @@ -252,7 +263,7 @@ ", " + (null == header.getValue() ? "" : header.getValue())); } - requestHeaders.put(orig.getName().toLowerCase(),orig); + requestHeaders.put(orig.getName().toLowerCase(), orig); } /** @@ -431,7 +442,7 @@ */ public int execute(HttpState state, HttpConnection connection) throws HttpException, IOException { if (log.isDebugEnabled()) { - log.debug("HttpMethodBase.execute(HttpState,HttpConnection)"); + log.debug("enter HttpMethodBase.execute(HttpState, HttpConnection)"); } if (null == state) { @@ -450,6 +461,9 @@ throw new HttpException("Not valid"); } + //pre-emptively add the authorization header, if required. + Authenticator.authenticate(this, state); + Set visited = new HashSet(); Set realms = new HashSet(); int retryCount = 0; @@ -460,6 +474,7 @@ log.debug("HttpMethodBase.execute(): looping."); } + try{ if (!connection.isOpen()) { if (log.isDebugEnabled()) { @@ -520,9 +535,10 @@ realms.add(pathAndCreds); } + removeRequestHeader(Authenticator.WWW_AUTH_RESP); //remove preemptively header boolean authenticated = false; try { - authenticated = Authenticator.authenticate(this,state); + authenticated = Authenticator.authenticate(this, state); } catch (HttpException httpe) { log.warn(httpe.getMessage()); } catch (UnsupportedOperationException uoe) { Index: test/org/apache/commons/httpclient/TestAuthenticator.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestAuthenticator.java,v retrieving revision 1.9 diff -u -r1.9 TestAuthenticator.java --- test/org/apache/commons/httpclient/TestAuthenticator.java 16 Jul 2002 13:52:57 -0000 1.9 +++ test/org/apache/commons/httpclient/TestAuthenticator.java 16 Jul 2002 22:32:58 -0000 @@ -73,7 +73,7 @@ * Unit tests for {@link Authenticator}. * * @author Rodney Waldhoff - * @author Jeff Dever + * @author Jeff Dever * @version $Id: TestAuthenticator.java,v 1.9 2002/07/16 13:52:57 jsdever Exp $ */ public class TestAuthenticator extends TestCase { @@ -158,7 +158,8 @@ public void testBasicAuthenticationWithNoChallenge() throws Exception { HttpState state = new HttpState(); HttpMethod method = new SimpleHttpMethod(null); - assertTrue(false == Authenticator.authenticate(method,state)); + + assertEquals(false, Authenticator.authenticate(method,state)); } public void testBasicAuthenticationWithNullHttpState() throws Exception { @@ -171,18 +172,6 @@ } } - public void testDigestAuthenticationScheme() throws Exception { - HttpState state = new HttpState(); - state.setCredentials("realm1",new UsernamePasswordCredentials("username","password")); - { - HttpMethod method = new SimpleHttpMethod(new Header("WWW-Authenticate","Basic realm=\"realm1\"")); - assertTrue(Authenticator.authenticate(method,state)); - assertTrue(null != method.getRequestHeader("Authorization")); - //TODO: test for correctness - } - - } - public void testInvalidAuthenticationScheme() throws Exception { HttpState state = new HttpState(); state.setCredentials(null,new UsernamePasswordCredentials("username","password")); @@ -246,6 +235,40 @@ } } + public void testPreemptiveAuthorizationDefault() throws Exception { + HttpState state = new HttpState(); + HttpMethod method = new SimpleHttpMethod(null); + state.setCredentials(null, new UsernamePasswordCredentials("username","password")); + + assertTrue(! Authenticator.authenticate(method,state)); + assertTrue(null == method.getRequestHeader("Authorization")); + } + + public void testPreemptiveAuthorizationTrue() throws Exception { + HttpState state = new HttpState(); + HttpMethod method = new SimpleHttpMethod(null); + state.setCredentials(null, new UsernamePasswordCredentials("username","password")); + + System.getProperties().setProperty(Authenticator.PREEMPTIVE_PROPERTY, "true"); + assertTrue(Authenticator.authenticate(method,state)); + assertTrue(null != method.getRequestHeader("Authorization")); + String expected = "Basic " + new String(Base64.encode("username:password".getBytes())); + assertEquals(expected, method.getRequestHeader("Authorization").getValue()); + } + + public void testPreemptiveAuthorizationFalse() throws Exception { + HttpState state = new HttpState(); + HttpMethod method = new SimpleHttpMethod(null); + + System.getProperties().setProperty(Authenticator.PREEMPTIVE_PROPERTY, "false"); + state.setCredentials(null, new UsernamePasswordCredentials("username","password")); + assertTrue(! Authenticator.authenticate(method,state)); + assertTrue(null == method.getRequestHeader("Authorization")); + } + + + + // --------------------------------- Test Methods for Digest Authentication public void testDigestAuthenticationWithNoCreds() { @@ -359,4 +382,6 @@ String digest = Authenticator.createDigest(cred.getUserName(),cred.getPassword(), table); assertEquals(response, digest); } + + }