/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.impl;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* This is an exact copy of
* org.springframework.security.web.authentication.www.DigestAuthUtils;
*
* The Spring class has package visibility, no idea why.
* The functionally is used for test cases and may be
* used by a client agent using the geoserver library
*
* @author mcr
*
*/
public class DigestAuthUtils {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
public static String encodePasswordInA1Format(String username, String realm, String password) {
String a1 = username + ":" + realm + ":" + password;
String a1Md5 = md5Hex(a1);
return a1Md5;
}
public static String[] splitIgnoringQuotes(String str, char separatorChar) {
if (str == null) {
return null;
}
int len = str.length();
if (len == 0) {
return EMPTY_STRING_ARRAY;
}
List<String> list = new ArrayList<String>();
int i = 0;
int start = 0;
boolean match = false;
while (i < len) {
if (str.charAt(i) == '"') {
i++;
while (i < len) {
if (str.charAt(i) == '"') {
i++;
break;
}
i++;
}
match = true;
continue;
}
if (str.charAt(i) == separatorChar) {
if (match) {
list.add(str.substring(start, i));
match = false;
}
start = ++i;
continue;
}
match = true;
i++;
}
if (match) {
list.add(str.substring(start, i));
}
return list.toArray(new String[list.size()]);
}
/**
* Computes the <code>response</code> portion of a Digest authentication header. Both the server and user
* agent should compute the <code>response</code> independently. Provided as a static method to simplify the
* coding of user agents.
*
* @param passwordAlreadyEncoded true if the password argument is already encoded in the correct format. False if
* it is plain text.
* @param username the user's login name.
* @param realm the name of the realm.
* @param password the user's password in plaintext or ready-encoded.
* @param httpMethod the HTTP request method (GET, POST etc.)
* @param uri the request URI.
* @param qop the qop directive, or null if not set.
* @param nonce the nonce supplied by the server
* @param nc the "nonce-count" as defined in RFC 2617.
* @param cnonce opaque string supplied by the client when qop is set.
* @return the MD5 of the digest authentication response, encoded in hex
* @throws IllegalArgumentException if the supplied qop value is unsupported.
*/
public static String generateDigest(boolean passwordAlreadyEncoded, String username, String realm, String password,
String httpMethod, String uri, String qop, String nonce, String nc, String cnonce)
throws IllegalArgumentException {
String a1Md5 = null;
String a2 = httpMethod + ":" + uri;
String a2Md5 = md5Hex(a2);
if (passwordAlreadyEncoded) {
a1Md5 = password;
} else {
a1Md5 = DigestAuthUtils.encodePasswordInA1Format(username, realm, password);
}
String digest;
if (qop == null) {
// as per RFC 2069 compliant clients (also reaffirmed by RFC 2617)
digest = a1Md5 + ":" + nonce + ":" + a2Md5;
} else if ("auth".equals(qop)) {
// As per RFC 2617 compliant clients
digest = a1Md5 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + a2Md5;
} else {
throw new IllegalArgumentException("This method does not support a qop: '" + qop + "'");
}
String digestMd5 = new String(md5Hex(digest));
return digestMd5;
}
/**
* Takes an array of <code>String</code>s, and for each element removes any instances of
* <code>removeCharacter</code>, and splits the element based on the <code>delimiter</code>. A <code>Map</code> is
* then generated, with the left of the delimiter providing the key, and the right of the delimiter providing the
* value.<p>Will trim both the key and value before adding to the <code>Map</code>.</p>
*
* @param array the array to process
* @param delimiter to split each element using (typically the equals symbol)
* @param removeCharacters one or more characters to remove from each element prior to attempting the split
* operation (typically the quotation mark symbol) or <code>null</code> if no removal should occur
* @return a <code>Map</code> representing the array contents, or <code>null</code> if the array to process was
* null or empty
*/
public static Map<String, String> splitEachArrayElementAndCreateMap(String[] array, String delimiter, String removeCharacters) {
if ((array == null) || (array.length == 0)) {
return null;
}
Map<String, String> map = new HashMap<String, String>();
for (int i = 0; i < array.length; i++) {
String postRemove;
if (removeCharacters == null) {
postRemove = array[i];
} else {
postRemove = StringUtils.replace(array[i], removeCharacters, "");
}
String[] splitThisArrayElement = split(postRemove, delimiter);
if (splitThisArrayElement == null) {
continue;
}
map.put(splitThisArrayElement[0].trim(), splitThisArrayElement[1].trim());
}
return map;
}
/**
* Splits a <code>String</code> at the first instance of the delimiter.<p>Does not include the delimiter in
* the response.</p>
*
* @param toSplit the string to split
* @param delimiter to split the string up with
* @return a two element array with index 0 being before the delimiter, and index 1 being after the delimiter
* (neither element includes the delimiter)
* @throws IllegalArgumentException if an argument was invalid
*/
public static String[] split(String toSplit, String delimiter) {
Assert.hasLength(toSplit, "Cannot split a null or empty string");
Assert.hasLength(delimiter, "Cannot use a null or empty delimiter to split a string");
if (delimiter.length() != 1) {
throw new IllegalArgumentException("Delimiter can only be one character in length");
}
int offset = toSplit.indexOf(delimiter);
if (offset < 0) {
return null;
}
String beforeDelimiter = toSplit.substring(0, offset);
String afterDelimiter = toSplit.substring(offset + 1);
return new String[]{beforeDelimiter, afterDelimiter};
}
public static String md5Hex(String data) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No MD5 algorithm available!");
}
return new String(Hex.encode(digest.digest(data.getBytes())));
}
}