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"));
+ }
+
}