cvs -z9 diff Authenticator.java (in directory D:\jakarta\jakarta-commons\httpclient\src\java\org\apache\commons\httpclient\) Index: Authenticator.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/Authenticator.java,v retrieving revision 1.15 diff -d -u -b -B -w -r1.15 Authenticator.java --- Authenticator.java 14 Jul 2002 02:14:59 -0000 1.15 +++ Authenticator.java 15 Jul 2002 12:58:22 -0000 @@ -65,6 +65,9 @@ import org.apache.commons.httpclient.log.Log; import org.apache.commons.httpclient.log.LogSource; +import java.util.Hashtable; +import java.security.MessageDigest; + /** *
Utility methods for HTTP authorization and authentication.
*
@@ -208,7 +211,30 @@
return false;
}
} else if ("digest".equalsIgnoreCase(authScheme)) {
- throw new UnsupportedOperationException("Digest authentication is not supported.");
+ // 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 {
throw new UnsupportedOperationException("Authentication type \"" + authScheme + "\" is not recognized.");
}
@@ -266,4 +292,266 @@
return "Basic " + new String(base64.encode(authString.getBytes()));
}
+ /**
+ * Create a Digest Authorization header for the given
+ * realm and state to the given method.
+ *
+ * @param realm the basic realm to authenticate to
+ * @param state a {@link HttpState} object providing {@link Credentials}
+ * @param respHeader the header's name to store the authentication response
+ * in. PROXY_AUTH_RESP will force the proxy credentials to be used.
+ *
+ * @return a digest Authorization header
+ *
+ * @throws HttpException when no matching credentials are available
+ */
+ static Header digest(String realm, HttpMethod method, HttpState state, String respHeader) throws HttpException {
+ log.debug("Authenticator.digest(String,HttpState)");
+ boolean proxy = PROXY_AUTH_RESP.equals(respHeader);
+ UsernamePasswordCredentials cred = null;
+ try {
+ cred = (UsernamePasswordCredentials) ( proxy ?
+ state.getProxyCredentials(realm) :
+ state.getCredentials(realm));
+ } catch(ClassCastException e) {
+ throw new HttpException("UsernamePasswordCredentials required for Digest 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 Digest authentication.");
+ }
+ }
+ if(null == cred) {
+ throw new HttpException("No credentials available for the Digest authentication realm \"" + realm + "\"/");
+ } else {
+ method.addRequestHeader(new Header("cnonce","\""+createCnonce()+"\""));
+ method.addRequestHeader(new Header("nc", "00000001"));
+ Hashtable headers = getHTTPDigestCredentials(method);
+ String digest = createDigest(cred.getUserName(), cred.getPassword(), headers);
+ return new Header(respHeader, Authenticator.digest(cred, headers, digest));
+ }
+ }
+
+ /**
+ * Return a Digest Authorization header value for the
+ * given {@link UsernamePasswordCredentials}.
+ */
+ static String digest(UsernamePasswordCredentials cred, Hashtable headers, String digest) throws HttpException {
+ return "Digest " + createDigestHeader(cred.getUserName(), headers, digest);
+ }
+
+ /**
+ * @todo + Add createDigest() method
+ * Creates an MD5 response digest.
+ *
+ * @param uname Username
+ * @param pwd Password
+ * @param dCreds Hashtable containing necessary header parameters to
+ * construct the digest. It must/can contain: uri, realm, nonce, cnonce,
+ * qop, nc.
+ * @return The created digest as string. This will be the response tag's
+ * value in the Authentication HTTP header.
+ */
+ private static String createDigest(String uname, String pwd, Hashtable dCreds) throws HttpException {
+ String digAlg = "MD5";
+ String method = "POST";
+
+ // Collecting required tokens
+ String uri = removeQuotes((String)dCreds.get("uri"));
+ String realm = removeQuotes((String)dCreds.get("realm"));
+ String nonce = removeQuotes((String)dCreds.get("nonce"));
+ String nc = removeQuotes((String)dCreds.get("nc"));
+ String cnonce = removeQuotes((String)dCreds.get("cnonce"));
+ String qop = removeQuotes((String)dCreds.get("qop"));
+ if (qop!=null) qop = "auth";
+
+ MessageDigest md5Helper;
+ try {
+ md5Helper = MessageDigest.getInstance(digAlg);
+ } catch (Exception e) {
+ System.out.println("ERROR! Unsupported algorithm in HTTP Digest authentication: "+digAlg);
+ HttpException he = new HttpException("Unsupported algorithm in HTTP Digest authentication: "+digAlg);
+ throw he;
+ }
+
+ // Calculating digest according to rfc 2617
+ String a2 = method + ":" + uri;
+ String md5a2 = encode(md5Helper.digest(a2.getBytes()));
+ String digestValue = uname + ":" + realm + ":" + pwd;
+ String md5a1 = encode(md5Helper.digest(digestValue.getBytes()));
+ String serverDigestValue;
+ if (qop==null) serverDigestValue = md5a1 + ":" + nonce + ":" + md5a2;
+ else serverDigestValue = md5a1 + ":" + nonce + ":" + nc + ":" +
+ cnonce + ":" + qop + ":" + md5a2;
+ String serverDigest = encode(md5Helper.digest(serverDigestValue.getBytes()));
+ return serverDigest;
+ }
+
+ /**
+ * @todo + Add createCnonce() method
+ * Creates a random cnonce value based on the current time.
+ *
+ * @return The cnonce value as String.
+ * @throws AxisFault if MD5 algorithm is not supported.
+ */
+ private static String createCnonce() throws HttpException {
+ String cnonce;
+ String digAlg = "MD5";
+ MessageDigest md5Helper;
+ try {
+ md5Helper = MessageDigest.getInstance(digAlg);
+ } catch (Exception e) {
+ System.out.println("ERROR! Unsupported algorithm in HTTP Digest authentication: "+digAlg);
+ HttpException he = new HttpException("Unsupported algorithm in HTTP Digest authentication: "+digAlg);
+ throw he;
+ }
+ cnonce = Long.toString(System.currentTimeMillis());
+ cnonce = encode(md5Helper.digest(cnonce.getBytes()));
+ return cnonce;
+ }
+
+ /**
+ * @todo + Add createDigestHeader() method
+ * Creates the header information that must be specified after the "Digest"
+ * string in the HTTP Authorization header (digest-response in RFC2617).
+ *
+ * @param uname Username
+ * @param dCreds Hashtable containing header information (uri, realm,
+ * nonce, nc, cnonce, opaque, qop).
+ * @digest The response tag's value as String.
+ * @return The digest-response as String.
+ */
+ private static String createDigestHeader(String uname, Hashtable dCreds, String digest) {
+ StringBuffer sb = new StringBuffer();
+ String uri = removeQuotes((String)dCreds.get("uri"));
+ String realm = removeQuotes((String)dCreds.get("realm"));
+ String nonce = removeQuotes((String)dCreds.get("nonce"));
+ String nc = removeQuotes((String)dCreds.get("nc"));
+ String cnonce = removeQuotes((String)dCreds.get("cnonce"));
+ String opaque = removeQuotes((String)dCreds.get("opaque"));
+ String response = digest;
+ String qop= removeQuotes((String)dCreds.get("qop"));
+ if (qop!=null) qop = "auth"; //we only support auth
+ 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+"\"");
+ return sb.toString();
+ }
+
+ /**
+ * Takes a String and cuts its prefix until the first double
+ * quotation mark and its suffix from the last double quotation mark (and cuts
+ * also the quotation marks).
+ *
+ * @param str the String from which the prefix and suffix is to
+ * be cut.
+ *
+ * @return the stumped String if the format of str
+ * is *"*"*. Otherwise the return value is same as
+ * str
+ */
+ private static String removeQuotes(String str) {
+ 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;
+ }
+
+ /**
+ * @todo + Add encode() method
+ * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters log
+ * String according to RFC 2617.
+ *
+ * @param binaryData array containing the digest
+ * @return encoded MD5, or null if encoding failed
+ */
+ private static String encode( byte[] binaryData ) {
+
+ if (binaryData.length != 16)
+ return null;
+
+ 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];
+ }
+
+ return new String(buffer);
+ }
+
+ /**
+ * Hexa values used when creating 32 character long digest in HTTP Digest in
+ * case of authentication.
+ *
+ * @see #encode(byte[])
+ */
+ 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
+ * requires Digest authentication.
+ *
+ * @param headers The HTTP headers.
+ * @return The parameters from www-authenticate header as a Hashtable
+ */
+ private static Hashtable getHTTPDigestCredentials(HttpMethod method) {
+ String authHeader = method.getResponseHeader("www-authenticate").getValue();
+ Hashtable ht = new Hashtable(15);
+ if (authHeader == null) return ht;
+ authHeader = authHeader.substring(7).trim();
+ int i = 0;
+ int j = authHeader.indexOf(",");
+ while(j >= 0) {
+ processDigestToken(authHeader.substring(i,j),ht);
+ i = j+1;
+ j = authHeader.indexOf(",",i);
+ }
+
+ if (i < authHeader.length())
+ processDigestToken(authHeader.substring(i),ht);
+ return ht;
+ }
+
+ /**
+ * @todo + Add processDigestToken() method
+ * Takes an entry of "xxx=yyy" format, partitions into a key and
+ * a value (key will be the left side, value will be the right side of the
+ * equal sign) and places that into a java.util.Hashtable into which the processed
+ * entry is placed (only if it has "xxx=yyy" format).
+ */
+ private static void processDigestToken(String token, Hashtable ht) {
+ int eqpos = token.indexOf("=");
+
+ if (eqpos > 0 && eqpos < token.length()-1)
+ ht.put(token.substring(0,eqpos).trim(),token.substring(eqpos+1).trim());
+ }
}