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 16:14:26 -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)); } 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 16:14:26 -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 16:14:26 -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 16:14:26 -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 16:14:27 -0000 @@ -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,12 +251,53 @@ assertTrue(null == method.getRequestHeader("Authorization")); } - public void testMultipleChallenge() throws Exception { + public void testMultipleProxyChallenge() throws Exception { HttpState state = new HttpState(); + state.setProxyCredentials("Protected", new UsernamePasswordCredentials("name", "pass")); HttpMethod method = new SimpleHttpMethod(); - //set both basic and digest response headers - - assertTrue(! Authenticator.authenticate(method,state)); + 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); + //assertEquals("Basic realm=\"Protected\", NTLM", + // method.getResponseHeader("Proxy-Authenticate").getValue().trim()); + assertEquals("Basic " + new String(Base64.encode("name:pass".getBytes())), + method.getRequestHeader("Proxy-Authorization").getValue().trim()); + } + + + public void testMultipleUnauthorizedChallenge() 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); + //assertEquals("NTLM, Basic realm=\"Protected\"", + // method.getResponseHeader("WWW-Authenticate").getValue().trim()); + assertEquals("Basic " + new String(Base64.encode("name:pass".getBytes())), + method.getRequestHeader("Authorization").getValue().trim()); } @@ -336,7 +377,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);