package com.vanillaforums;
import java.util.*;
/**
* @author Todd Burry <todd@vanillaforums.com>
* @version 1.0b
* This object contains the client code for Vanilla jsConnect signle-sign-on.
*/
public class jsConnect {
/**
* Convenience method that returns a map representing an error.
* @param code The code of the error.
* @param message A user-readable message for the error.
* @return
*/
protected static Map Error(String code, String message) {
Map result = new HashMap();
result.put("error", code);
result.put("message", message);
return result;
}
/**
* Returns a JSONP formatted string suitable to be consumed by jsConnect.
* This is usually the only method you need to call in order to implement jsConnect.
* @param user A map containing the user information. The map should have the following keys:
* - uniqueid: An ID that uniquely identifies the user in your system. This value should never change for a given user.
* @param request: A map containing the query string for the current request. You usually just pass in request.getParameterMap().
* @param clientID: The client ID for your site. This is usually configured on Vanilla's jsConnect configuration page.
* @param secret: The secret for your site. This is usually configured on Vanilla's jsConnect configuration page.
* @param secure: Whether or not to check security on the request. You can leave this false for testing, but you should make it true in production.
* @return The JSONP formatted string representing the current user.
*/
public static String GetJsConnectString(Map user, Map request, String clientID, String secret, Boolean secure) {
Map error = null;
long timestamp = 0;
try {
timestamp = Long.parseLong(Val(request, "timestamp"));
} catch (Exception ex) {
timestamp = 0;
}
long currentTimestamp = jsConnect.Timestamp();
if (secure) {
if (Val(request, "client_id") == null) {
error = jsConnect.Error("invalid_request", "The client_id parameter is missing.");
} else if (!Val(request, "client_id").equals(clientID)) {
error = jsConnect.Error("invalid_client", "Unknown client " + Val(request, "client_id") + ".");
} else if (Val(request, "timestamp") == null && Val(request, "signature") == null) {
if (user != null && !user.isEmpty()) {
error = new HashMap();
error.put("name", user.get("name"));
error.put("photourl", user.containsKey("photourl") ? user.get("photourl") : "");
} else {
error = new HashMap();
error.put("name", "");
error.put("photourl", "");
}
} else if (timestamp == 0) {
error = jsConnect.Error("invalid_request", "The timestamp is missing or invalid.");
} else if (Val(request, "signature") == null) {
error = jsConnect.Error("invalid_request", "The signature is missing.");
} else if (Math.abs(currentTimestamp - timestamp) > 30 * 60) {
error = jsConnect.Error("invalid_request", "The timestamp is invalid.");
} else {
// Make sure the timestamp's signature checks out.
String timestampSig = jsConnect.MD5(Long.toString(timestamp) + secret);
if (!timestampSig.equals(Val(request, "signature"))) {
error = jsConnect.Error("access_denied", "Signature invalid.");
}
}
}
Map result;
if (error != null) {
result = error;
} else if (user != null && !user.isEmpty()) {
result = new LinkedHashMap(user);
SignJsConnect(result, clientID, secret, true);
} else {
result = new LinkedHashMap();
result.put("name", "");
result.put("photourl", "");
}
String json = jsConnect.JsonEncode(result);
if (Val(request, "callback") == null) {
return json;
} else {
return Val(request, "callback") + "(" + json + ");";
}
}
/**
* JSON encode some data.
* @param data The data to encode.
* @return The JSON encoded data.
*/
public static String JsonEncode(Map data) {
StringBuilder result = new StringBuilder();
Iterator iterator = data.entrySet().iterator();
while (iterator.hasNext()) {
if (result.length() > 0) {
result.append(", ");
}
Map.Entry v = (Map.Entry) iterator.next();
String key = v.getKey().toString();
key = key.replace("\"", "\\\"");
String value = v.getValue().toString();
value = value.replace("\"", "\\\"");
String q = "\"";
result.append(q + key + q + ": " + q + value + q);
}
return "{ " + result.toString() + " }";
}
/**
* Compute the MD5 hash of a string.
* @param password The data to compute the hash on.
* @return A hex encoded string representing the MD5 hash of the string.
*/
public static String MD5(String password) {
try {
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
digest.update(password.getBytes("UTF-8"));
byte[] hash = digest.digest();
StringBuilder ret = new StringBuilder();
for (int i = 0; i < hash.length; i++) {
String hex = Integer.toHexString(0xFF & hash[i]);
if (hex.length() == 1) {
// could use a for loop, but we're only dealing with a single byte
ret.append('0');
}
ret.append(hex);
}
return ret.toString();
} catch (Exception ex) {
return "ERROR";
}
}
/**
* Get a value from a map.
* @param request The map to get the value from.
* @param key The key of the value.
* @param defaultValue The default value if the map doesn't contain the value.
* @return The value from the map or the default if it isn't found.
*/
protected static String Val(Map request, String key, String defaultValue) {
try {
Object result = null;
if (request.containsKey(key)) {
result = request.get(key);
if (result instanceof String[]) {
return ((String[]) request.get(key))[0];
} else {
return result.toString();
}
}
} catch (Exception ex) {
return defaultValue;
}
return defaultValue;
}
/**
* Get a value from a map.
* @param request The map to get the value from.
* @param key The key of the value.
* @return The value from the map or the null if it isn't found.
*/
protected static String Val(Map request, String key) {
return Val(request, key, null);
}
/**
* Sign a jsConnect response. Responses are signed so that the site requesting the response knows that this is a valid site signing in.
* @param data The data to sign.
* @param clientID The client ID of the site. This is usually configured on Vanilla's jsConnect configuration page.
* @param secret The secret of the site. This is usually configured on Vanilla's jsConnect configuration page.
* @param setData Whether or not to add the signature information to the data.
* @return The computed signature of the data.
*/
public static String SignJsConnect(Map data, String clientID, String secret, Boolean setData) {
// Generate a sorted list of the keys.
String[] keys = new String[data.keySet().size()];
data.keySet().toArray(keys);
Arrays.sort(keys, String.CASE_INSENSITIVE_ORDER);
// Generate the String to sign.
StringBuilder sigStr = new StringBuilder();
for (int i = 0; i < keys.length; i++) {
if (sigStr.length() > 0) {
sigStr.append("&");
}
String key = keys[i];
String value = data.get(key).toString();
try {
sigStr.append(java.net.URLEncoder.encode(key.toLowerCase(), "UTF-8"));
sigStr.append("=");
sigStr.append(java.net.URLEncoder.encode(value, "UTF-8"));
} catch (Exception ex) {
if (setData) {
data.put("clientid", clientID);
data.put("signature", "ERROR");
}
return "ERROR";
}
}
// MD5 sign the String with the secret.
String signature = jsConnect.MD5(sigStr.toString() + secret);
if (setData) {
data.put("clientid", clientID);
data.put("signature", signature);
}
return signature;
}
/**
* Returns the current timestamp of the server, suitable for synching with the site.
* @return The current timestamp.
*/
public static long Timestamp() {
long result = System.currentTimeMillis() / 1000;
return result;
}
}