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 Hashtable. + * + * @param token the entry to be processed + * @param ht the 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()); + } }