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.23 diff -u -r1.23 Authenticator.java --- java/org/apache/commons/httpclient/Authenticator.java 28 Jul 2002 18:08:57 -0000 1.23 +++ java/org/apache/commons/httpclient/Authenticator.java 7 Aug 2002 19:03:51 -0000 @@ -67,6 +67,7 @@ import java.security.MessageDigest; import java.util.Hashtable; +import java.util.StringTokenizer; /** * Utility methods for HTTP authorization and authentication. @@ -200,7 +201,7 @@ * @see #digest * @see HttpMethod#addRequestHeader */ - private static boolean authenticate(HttpMethod method, HttpState state, Header challengeHeader, String respHeader) + private static boolean authenticate(HttpMethod method, HttpState state, Header authenticateHeader, String respHeader) throws HttpException, UnsupportedOperationException { log.trace("enter Authenticator.authenticate(HttpMethod, HttpState, Header, String)"); @@ -219,7 +220,7 @@ boolean preemptive = ("true".equals(preemptive_str)); //if there is no challenge, attempt to use preemptive authorization - if (challengeHeader == null){ + if (authenticateHeader == null){ if (preemptive){ log.debug("Preemptively sending default basic credentials"); try{ @@ -234,55 +235,81 @@ return false; } } - log.debug("Attempting to authenticate challenge: " + challengeHeader); + log.debug("Attempting to authenticate header: " + authenticateHeader); - // Get the challenge from the header - String challenge = challengeHeader.getValue(); + // XXX: Get the challenge from the header + String authenticateValue = authenticateHeader.getValue(); - // 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("Authentication challenge \'" + challenge + "\'does not contain an authentication scheme"); - } - String authScheme = challenge.substring(0, space); + //FIXME: This fails if the contents of a challenge contains a ',' + StringTokenizer challengeTok = new StringTokenizer(authenticateValue, ","); + Hashtable challengeMap = new Hashtable(7); + while(challengeTok.hasMoreTokens()){ + + // Parse the authentication scheme from the challenge + String chall = challengeTok.nextToken(); + StringTokenizer authTok = new StringTokenizer(chall, " "); + String authScheme = authTok.nextToken(); - // 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"); + // Store the challenge keyed on the lower case authenticaion scheme + challengeMap.put(authScheme.toLowerCase(), chall); } - 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 + if (challengeMap.containsKey("digest")) { + String challenge = (String)challengeMap.get("digest"); + String realm = parseRealmFromChallenge(challenge); requestHeader = Authenticator.digest(realm, method, state, respHeader); - - } else { // unrecognized authentication - throw new UnsupportedOperationException("Authentication type \"" + authScheme + "\" is not recognized."); + } else if (challengeMap.containsKey("basic")) { + String challenge = (String)challengeMap.get("basic"); + String realm = parseRealmFromChallenge(challenge); + requestHeader = Authenticator.basic(realm, state, respHeader); + } else if (challengeMap.size() == 0) { + throw new HttpException("No authentication scheme found in '" + + authenticateValue); + } else { + throw new UnsupportedOperationException("Requested authentication scheme " + + challengeMap.keySet() + " is unsupported"); } + //Add the header and return the result if(requestHeader != null) { // add the header method.addRequestHeader(requestHeader); return true; } else { // don't add the header return false; } + } + /** + * Parse the realm from the authentication challenge + */ + private static String parseRealmFromChallenge(String challenge) + throws HttpException { + // FIXME: Note that this won't work if there is more than one realm within the challenge + try{ + StringTokenizer strtok = new StringTokenizer(challenge, "="); + String realmName = strtok.nextToken().trim(); + String realm = strtok.nextToken().trim(); + int firstq = realm.indexOf('"'); + int lastq = realm.lastIndexOf('"'); + if (firstq+1 < lastq) { + realm = realm.substring(firstq+1, lastq); + } + log.debug("Parsed realm '" + realm + "' from challenge '" + challenge + "'"); + return realm; + } catch (Exception ex) { + throw new HttpException("Failed to parse realm from challenge '" + challenge + "'"); + } + + } + + + /** * Create a Basic Authorization header for the given * realm and state to the given method. @@ -311,7 +338,8 @@ } 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)); } @@ -370,7 +398,7 @@ if(null == cred) { throw new HttpException("No credentials available for the Digest authentication realm \"" + realm + "\"/"); } else { - Hashtable headers = getHTTPDigestCredentials(method); + Hashtable headers = getHTTPDigestCredentials(method, proxy); headers.put( "cnonce", "\""+createCnonce()+"\""); headers.put( "nc", "00000001"); headers.put( "uri", method.getPath() ); @@ -567,31 +595,49 @@ { '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 + * Processes the authenticate HTTP header received from the server that * requires Digest authentication. * - * @param headers The HTTP headers. - * @return The parameters from www-authenticate header as a Hashtable - */ - private static Hashtable getHTTPDigestCredentials(HttpMethod method) { - log.trace("enter Authenticator.getHTTPDigestCredentials(HttpMethod)"); + * @param method The HTTP method. + * @param proxy true if authorizing for a proxy + * @return The parameters from the authenticate header as a Hashtable + * or empty Hashtable if there is no valid authorization. + * + * @see #processDigestToken(String,java.util.Hashtable) + * @see PROXY_AUTH + * @see WWW_AUTH + * @since 2.0 + */ + private static Hashtable getHTTPDigestCredentials(HttpMethod method, boolean proxy) { + log.trace("enter Authenticator.getHTTPDigestCredentials(HttpMethod, boolean)"); + + //Determine wether to use proxy or www header + String authName = proxy ? PROXY_AUTH : WWW_AUTH; + String authHeader = null; + + //Get the authorization header value + try{ + authHeader = method.getResponseHeader(authName).getValue(); + authHeader = authHeader.substring(7).trim(); + } catch (NullPointerException npe){ + return new Hashtable(0); + } + + //Hashtable of digest tokens + Hashtable ht = new Hashtable(17); - String authHeader = method.getResponseHeader("www-authenticate").getValue(); - Hashtable ht = new Hashtable(15); - if (authHeader == null) { - return ht; - } - authHeader = authHeader.substring(7).trim(); + //parse the authenticate header int i = 0; int j = authHeader.indexOf(","); while(j >= 0) { - processDigestToken(authHeader.substring(i,j),ht); + processDigestToken(authHeader.substring(i,j), ht); i = j+1; j = authHeader.indexOf(",",i); } - if (i < authHeader.length()) - processDigestToken(authHeader.substring(i),ht); + if (i < authHeader.length()) { + processDigestToken(authHeader.substring(i), ht); + } return ht; } 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.44 diff -u -r1.44 HttpMethodBase.java --- java/org/apache/commons/httpclient/HttpMethodBase.java 7 Aug 2002 02:13:22 -0000 1.44 +++ java/org/apache/commons/httpclient/HttpMethodBase.java 7 Aug 2002 19:03:51 -0000 @@ -641,10 +641,10 @@ break; } } catch (HttpException httpe) { - log.warn("Exception thrown authenticating: " + httpe.getMessage()); + log.warn(httpe.getMessage()); return true; // finished request } catch (UnsupportedOperationException uoe) { - log.warn("Exception thrown authenticating: " + uoe.getMessage()); + log.warn(uoe.getMessage()); //FIXME: should this return true? } Index: test/org/apache/commons/httpclient/SimpleHttpConection.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/SimpleHttpConection.java,v retrieving revision 1.1 diff -u -r1.1 SimpleHttpConection.java --- test/org/apache/commons/httpclient/SimpleHttpConection.java 2 Aug 2002 11:38:12 -0000 1.1 +++ test/org/apache/commons/httpclient/SimpleHttpConection.java 7 Aug 2002 19:03:51 -0000 @@ -63,24 +63,50 @@ package org.apache.commons.httpclient; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; +import java.util.Vector; + /** - * Hack HttpConnection to test the response header reading mechanism. + * For test-nohost testing purposes only. + * + * @author Jeff Dever */ class SimpleHttpConnection extends HttpConnection { + static Log log = LogFactory.getLog("httpclient.test"); + + int hits = 0; + + Vector headers = new Vector(); + Vector bodies = new Vector(); BufferedReader headerReader = null; ByteArrayInputStream bodyInputStream = null; - public SimpleHttpConnection(String headers, String body) { + public void addResponse(String header) { + addResponse(header, ""); + } + + public void addResponse(String header, String body) { + headers.add(header); + bodies.add(body); + } + + public SimpleHttpConnection(String header, String body) { + this(); + headers.add(header); + bodies.add(body); + } + + public SimpleHttpConnection() { super(null, -1, "localhost", 80, false); - this.headerReader = new BufferedReader(new StringReader(headers)); - bodyInputStream = new ByteArrayInputStream(body.getBytes()); } public SimpleHttpConnection(String host, int port, boolean isSecure){ @@ -88,6 +114,24 @@ } public void open() throws IOException { + if (headerReader != null) return; + + try{ + log.debug("hit: " + hits); + headerReader = new BufferedReader( + new StringReader((String)headers.elementAt(hits))); + bodyInputStream = new ByteArrayInputStream( + ((String)bodies.elementAt(hits)).getBytes()); + hits++; + } catch (ArrayIndexOutOfBoundsException aiofbe) { + throw new IOException("SimpleHttpConnection has been opened more times " + + "than it has responses. You might need to call addResponse()."); + } + } + + public void close() { + headerReader = null; + bodyInputStream = null; } public void write(byte[] data) @@ -100,7 +144,9 @@ public String readLine() throws IOException, IllegalStateException { - return headerReader.readLine(); + String str = headerReader.readLine(); + log.debug("read: " + str); + return str; } public InputStream getResponseInputStream(HttpMethod method) { Index: test/org/apache/commons/httpclient/SimpleHttpMethod.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/SimpleHttpMethod.java,v retrieving revision 1.2 diff -u -r1.2 SimpleHttpMethod.java --- test/org/apache/commons/httpclient/SimpleHttpMethod.java 6 Aug 2002 15:15:32 -0000 1.2 +++ test/org/apache/commons/httpclient/SimpleHttpMethod.java 7 Aug 2002 19:03:51 -0000 @@ -63,12 +63,20 @@ package org.apache.commons.httpclient; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import java.io.IOException; -/** Simple method for testing the HttpMethodBase. + +/** + * For test-nohost testing purposes only. + * + * @author Jeff Dever */ class SimpleHttpMethod extends HttpMethodBase{ + static Log log = LogFactory.getLog("httpclient.test"); Header header = null; SimpleHttpMethod(){ 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.13 diff -u -r1.13 TestAuthenticator.java --- test/org/apache/commons/httpclient/TestAuthenticator.java 6 Aug 2002 15:15:32 -0000 1.13 +++ test/org/apache/commons/httpclient/TestAuthenticator.java 7 Aug 2002 19:03:52 -0000 @@ -6,7 +6,7 @@ * * The Apache Software License, Version 1.1 * - * Copyright (c) 1999 The Apache Software Foundation. All rights + * Copyright (c) 1999-2002 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without @@ -183,8 +183,8 @@ public void testBasicAuthentication() throws Exception { HttpState state = new HttpState(); - state.setCredentials("realm1",new UsernamePasswordCredentials("username","password")); - HttpMethod method = new SimpleHttpMethod(new Header("WWW-Authenticate","Basic realm=\"realm1\"")); + state.setCredentials("realm",new UsernamePasswordCredentials("username","password")); + HttpMethod method = new SimpleHttpMethod(new Header("WWW-Authenticate","Basic realm=\"realm\"")); assertTrue(Authenticator.authenticate(method,state)); assertTrue(null != method.getRequestHeader("Authorization")); String expected = "Basic " + new String(Base64.encode("username:password".getBytes())); @@ -251,16 +251,6 @@ assertTrue(null == method.getRequestHeader("Authorization")); } - public void testMultipleChallenge() throws Exception { - HttpState state = new HttpState(); - HttpMethod method = new SimpleHttpMethod(); - //set both basic and digest response headers - - assertTrue(! Authenticator.authenticate(method,state)); - } - - - // --------------------------------- Test Methods for Digest Authentication public void testDigestAuthenticationWithNoCreds() { @@ -336,7 +326,7 @@ checkAuthorization(cred, method.getName(), method.getRequestHeader("Authorization").getValue()); } - public void testDigestAuthenticationWithMutlipleRealms() throws Exception { + public void testDigestAuthenticationWithMultipleRealms() throws Exception { HttpState state = new HttpState(); UsernamePasswordCredentials cred = new UsernamePasswordCredentials("username","password"); state.setCredentials("realm1", cred); @@ -375,6 +365,115 @@ String digest = Authenticator.createDigest(cred.getUserName(),cred.getPassword(), table); assertEquals(response, digest); } + + + // --------------------------------- Test Methods for Multiple Authentication + + public void testMultipleChallengeBasic() throws Exception { + HttpState state = new HttpState(); + state.setCredentials("Protected", new UsernamePasswordCredentials("name", "pass")); + HttpMethod method = new SimpleHttpMethod(); + SimpleHttpConnection conn = new SimpleHttpConnection(); + conn.addResponse( + "HTTP/1.1 401 Unauthorized\r\n" + + "WWW-Authenticate: NTLM\r\n" + + "WWW-Authenticate: Basic realm=\"Protected\"\r\n" + + "Connection: close\r\n" + + "Server: HttpClient Test/2.0\r\n" + ); + conn.addResponse( + "HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n" + + "Server: HttpClient Test/2.0\r\n" + ); + method.execute(state, conn); + Header authHeader = method.getRequestHeader("Authorization"); + assertNotNull(authHeader); + + String authValue = authHeader.getValue(); + assertTrue(authValue.startsWith("Basic")); + } + + + public void testMultipleChallengeDigest() throws Exception { + HttpState state = new HttpState(); + state.setCredentials("Protected", new UsernamePasswordCredentials("name", "pass")); + HttpMethod method = new SimpleHttpMethod(); + SimpleHttpConnection conn = new SimpleHttpConnection(); + conn.addResponse( + "HTTP/1.1 401 Unauthorized\r\n" + + "WWW-Authenticate: NTLM\r\n" + + "WWW-Authenticate: Digest realm=\"Protected\"\r\n" + + "WWW-Authenticate: Basic realm=\"Protected\"\r\n" + + "Connection: close\r\n" + + "Server: HttpClient Test/2.0\r\n" + ); + conn.addResponse( + "HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n" + + "Server: HttpClient Test/2.0\r\n" + ); + method.execute(state, conn); + Header authHeader = method.getRequestHeader("Authorization"); + assertNotNull(authHeader); + + String authValue = authHeader.getValue(); + assertTrue(authValue.startsWith("Digest")); + } + + + public void testMultipleProxyChallengeBasic() throws Exception { + HttpState state = new HttpState(); + state.setProxyCredentials("Protected", new UsernamePasswordCredentials("name", "pass")); + HttpMethod method = new SimpleHttpMethod(); + SimpleHttpConnection conn = new SimpleHttpConnection(); + conn.addResponse( + "HTTP/1.1 407 Proxy Authentication Required\r\n" + + "Proxy-Authenticate: Basic realm=\"Protected\"\r\n" + + "Proxy-Authenticate: NTLM\r\n" + + "Connection: close\r\n" + + "Server: HttpClient Test/2.0\r\n" + ); + conn.addResponse( + "HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n" + + "Server: HttpClient Test/2.0\r\n" + ); + method.execute(state, conn); + Header authHeader = method.getRequestHeader("Proxy-Authorization"); + assertNotNull(authHeader); + + String authValue = authHeader.getValue(); + assertTrue(authValue.startsWith("Basic")); + } + + + public void testMultipleProxyChallengeDigest() throws Exception { + HttpState state = new HttpState(); + state.setProxyCredentials("Protected", new UsernamePasswordCredentials("name", "pass")); + HttpMethod method = new SimpleHttpMethod(); + SimpleHttpConnection conn = new SimpleHttpConnection(); + conn.addResponse( + "HTTP/1.1 407 Proxy Authentication Required\r\n" + + "Proxy-Authenticate: Basic realm=\"Protected\"\r\n" + + "Proxy-Authenticate: Digest realm=\"Protected\"\r\n" + + "Proxy-Authenticate: NTLM\r\n" + + "Connection: close\r\n" + + "Server: HttpClient Test/2.0\r\n" + ); + conn.addResponse( + "HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n" + + "Server: HttpClient Test/2.0\r\n" + ); + method.execute(state, conn); + Header authHeader = method.getRequestHeader("Proxy-Authorization"); + assertNotNull(authHeader); + + String authValue = authHeader.getValue(); + assertTrue(authValue.startsWith("Digest")); + } + }