package com.apress.progwt.server.web.controllers.facebook;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Map.Entry;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.facebook.api.FacebookParam;
import com.facebook.api.FacebookRestClient;
/**
* Utility class to handle authorization and authentication of requests. Objects
* of this class are meant to be created for every request. They are stateless
* and are not supposed to be kept in the session.
*
* @author yoni
*
*/
public class Facebook {
private HttpServletRequest request;
private HttpServletResponse response;
protected FacebookRestClient apiClient;
protected String apiKey;
protected String secret;
protected Map<String, String> fbParams;
protected Long user;
private static String FACEBOOK_URL_PATTERN = "^https?://([^/]*\\.)?facebook\\.com(:\\d+)?/.*";
public Facebook(HttpServletRequest request, HttpServletResponse response,
String apiKey, String secret) {
this.request = request;
this.response = response;
this.apiKey = apiKey;
this.secret = secret;
this.apiClient = new FacebookRestClient(this.apiKey, this.secret);
validateFbParams();
// caching of friends
String friends = fbParams.get("friends");
if (friends!=null && !friends.equals("")) {
List<Long> friendsList = new ArrayList<Long> ();
for (String friend : friends.split(",")) {
friendsList.add(Long.parseLong(friend));
}
apiClient._setFriendsList(friendsList);
}
// caching of the "added" value
String added = fbParams.get("added");
if (added != null) {
apiClient.added = new Boolean (added.equals("1"));
}
}
/**
* Returns the internal FacebookRestClient object.
*
* @return
*/
public FacebookRestClient getFacebookRestClient() {
return apiClient;
}
/**
* Synonym for {@link #getFacebookRestClient()}
*
* @return
*/
public FacebookRestClient get_api_client() {
return getFacebookRestClient();
}
/**
* Returns the secret key used to initialize this object.
*
* @return
*/
public String getSecret() {
return secret;
}
/**
* Returns the api key used to initialize this object.
*
* @return
*/
public String getApiKey() {
return apiKey;
}
private void validateFbParams() {
// first we analyze the request parameters
fbParams = getValidFbParams(_getRequestParams(), 48 * 3600,
FacebookParam.SIGNATURE.toString());
if (fbParams != null && !fbParams.isEmpty()) {
// this comment block is copied from the official php client,
// it explains a lot :)
//
// If we got any fb_params passed in at all, then either:
// - they included an fb_user / fb_session_key, which we should
// assume
// to be correct
// - they didn't include an fb_user / fb_session_key, which means
// the
// user doesn't have a valid session and if we want to get one we'll
// need to use require_login(). (Calling set_user with null values
// for user/session_key will work properly.)
// - Note that we should *not* use our cookies in this scenario,
// since
// they may be referring to the wrong user.
//
// parsing the user, session, and expiry info
String tmpSt = fbParams.get(FacebookParam.USER.getSignatureName());
Long user_id = tmpSt != null ? Long.valueOf(tmpSt) : null;
String session_key = fbParams.get(FacebookParam.SESSION_KEY
.getSignatureName());
tmpSt = fbParams.get(FacebookParam.EXPIRES.getSignatureName());
Long expires = tmpSt != null ? Long.valueOf(tmpSt) : null;
setUser(user_id, session_key, expires);
} else {
// fallback to checking cookies
Map<String, String> cookieParams = _getCookiesParams();
fbParams = getValidFbParams(cookieParams, null, this.apiKey);
if (fbParams != null && !fbParams.isEmpty()) {
// parsing the user and session
String tmpSt = fbParams.get(FacebookParam.USER
.getSignatureName());
Long user_id = tmpSt != null ? Long.valueOf(tmpSt) : null;
String session_key = fbParams.get(FacebookParam.SESSION_KEY
.getSignatureName());
setUser(user_id, session_key, null);
}
// finally we check the auth_token for a round-trip from the
// facebook login page
else if (request.getParameter("auth_token") != null) {
try {
doGetSession(request
.getParameter("auth_token"));
setUser(apiClient._getUserId(), apiClient._getSessionKey(), apiClient._getExpires());
} catch (Exception e) {
// if auth_token is stale (browser url doesn't change,
// server is restarted), then auth_getSession throws
// an exception. This happens a lot during development. To
// recover, we do nothing. Then when requireLogin or
// requireAdd
// kick in, a new auth_token is created by redirecting the
// user.
// e.printStackTrace(System.err);
}
}
}
}
public String doGetSession (String authToken) {
try {
return apiClient.auth_getSession(authToken);
} catch (Exception e) {
throw new RuntimeException (e);
}
}
/**
* Sets the user. This method also saves the user and session information in
* the HttpSession
*
* @param user_id
* @param session_key
* @param expires
*/
private void setUser(Long user_id, String session_key, Long expires) {
// place the data in the session for future requests that may not have
// the
// facebook parameters
if (!inFbCanvas()) {
Map<String, String> cookiesInfo = _getCookiesParams();
String cookieUser = cookiesInfo.get(this.apiKey + "_user");
if (cookieUser == null || !cookieUser.equals(user_id + "")) {
// map of parameters, but without the api_key prefix
Map<String, String> cookies = new HashMap<String, String> ();
cookies.put("user", user_id + "");
cookies.put("session_key", session_key);
String sig = generateSig(cookies, this.secret);
int age = 0;
if (expires!=null) {
age = (int) (expires.longValue() - (System.currentTimeMillis()/1000));
}
for (Map.Entry<String, String> entry : cookies.entrySet()) {
addCookie (this.apiKey + "_" + entry.getKey(), entry.getValue(), age);
}
addCookie (this.apiKey, sig, age);
}
}
this.user = user_id;
this.apiClient._setSessionKey(session_key);
}
private void addCookie (String key, String value, int age) {
Cookie cookie = new Cookie (key, value);
if (age > 0) {
cookie.setMaxAge(age);
}
cookie.setPath(request.getContextPath());
response.addCookie(cookie);
}
private Map<String, String> getValidFbParams(Map<String, String> params,
Integer timeout, String namespace) {
if (namespace == null)
namespace = "fb_sig";
String prefix = namespace + "_";
int prefix_len = prefix.length();
Map<String, String> fb_params = new HashMap<String, String>();
for (Entry<String, String> requestParam : params.entrySet()) {
if (requestParam.getKey().indexOf(prefix) == 0) {
fb_params.put(requestParam.getKey().substring(prefix_len),
requestParam.getValue());
}
}
if (timeout != null) {
if (!fb_params.containsKey(FacebookParam.TIME.getSignatureName())) {
return new HashMap<String, String>();
}
String tmpTime = fb_params.get(FacebookParam.TIME
.getSignatureName());
if (tmpTime.indexOf('.') > 0)
tmpTime = tmpTime.substring(0, tmpTime.indexOf('.'));
long time = Long.parseLong(tmpTime);
if (System.currentTimeMillis() / 1000 - time > timeout) {
return new HashMap<String, String>();
}
}
if (!params.containsKey(namespace)
|| !verifySignature(fb_params, params.get(namespace))) {
return new HashMap<String, String>();
}
return fb_params;
}
private void redirect(String url) {
try {
// fbml redirect
if (inFbCanvas()) {
String out = "<fb:redirect url=\"" + url + "\"/>";
response.getWriter().print(out);
response.flushBuffer();
}
// javascript "frame-bypassing" redirect
else if (url.matches(FACEBOOK_URL_PATTERN)) {
String out = "<html><script type=\"text/javascript\">\ntop.location.href = \""
+ url + "\";\n</script></html>";
response.setHeader("Content-Type", "text/html");
response.getWriter().print(out);
response.flushBuffer();
} else {
// last fallback
response.sendRedirect(url);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Returns true if the application is in a frame or a canvas.
*
* @return
*/
public boolean inFrame() {
return fbParams.containsKey(FacebookParam.IN_CANVAS.getSignatureName())
|| fbParams.containsKey(FacebookParam.IN_IFRAME
.getSignatureName());
}
/**
* Returns true if the application is in a canvas.
*
* @return
*/
public boolean inFbCanvas() {
return fbParams.containsKey(FacebookParam.IN_CANVAS.getSignatureName());
}
public boolean isAdded() {
return "1".equals(fbParams.get(FacebookParam.ADDED.getSignatureName()));
}
public boolean isLogin() {
return getUser() != null;
}
/**
* Synonym for {@link #getUser()}
*
* @return
*/
public Long get_loggedin_user() {
return getUser();
}
/**
* Returns the user id of the logged in user associated with this object
*
* @return
*/
public Long getUser() {
return this.user;
}
/**
* Returns the url of the currently requested page
*
* @return
*/
private String currentUrl() {
String url = request.getScheme() + "://" + request.getServerName();
int port = request.getServerPort();
if (port != 80) {
url += ":" + port;
}
url += request.getRequestURI();
return url;
}
/**
* Forces the user to log in to this application. If the user hasn't logged
* in yet, this method issues a url redirect.
*
* @param next
* the value for the 'next' request paramater that is appended to
* facebook's login screen.
* @return true if the user hasn't logged in yet and a redirect was issued.
*/
public boolean requireLogin(String next) {
if (getUser() != null)
return false;
redirect(getLoginUrl(next, inFrame()));
return true;
}
/**
* Forces the user to add this application. If the user hasn't added it yet,
* this method issues a url redirect.
*
* @param next
* the value for the 'next' request paramater that is appended to
* facebook's add screen.
* @return true if the user hasn't added the application yet and a redirect
* was issued.
*/
public boolean requireAdd(String next) {
if (getUser() != null && isAdded())
return false;
redirect(getAddUrl(next));
return true;
}
/**
* Forces the application to be in a frame. If it is not in a frame, this
* method issues a url redirect.
*
* @param next
* the value for the 'next' request paramater that is appended to
* facebook's login screen.
* @return true if a redirect was issued, false otherwise.
*/
public boolean requireFrame(String next) {
if (!inFrame()) {
redirect(getLoginUrl(next, true));
return true;
}
return false;
}
/**
* Returns the url that facebook uses to prompt the user to login to this
* application.
*
* @param next
* indicates the page to which facebook should redirect the user
* has logged in.
* @return
*/
public String getLoginUrl(String next, boolean canvas) {
String url = getFacebookUrl(null) + "/login.php?v=1.0&api_key="
+ apiKey;
try {
url += next != null ? "&next=" + URLEncoder.encode(next, "UTF-8")
: "";
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
url += canvas ? "&canvas" : "";
return url;
}
/**
* Returns the url that facebook uses to prompt the user to add this
* application.
*
* @param next
* indicates the page to which facebook should redirect the user
* after the application is added.
* @return
*/
public String getAddUrl(String next) {
String url = getFacebookUrl(null) + "/add.php?api_key=" + apiKey;
try {
url += next != null ? "&next=" + URLEncoder.encode(next, "UTF-8")
: "";
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return url;
}
/**
* Returns a url to a facebook sub-domain
*
* @param subDomain
* @return
*/
public static String getFacebookUrl(String subDomain) {
if (subDomain == null || subDomain.equals(""))
subDomain = "www";
return "http://" + subDomain + ".facebook.com";
}
public static String generateSig(Map<String, String> params, String secret) {
SortedSet<String> keys = new TreeSet<String>(params.keySet());
// make sure that the signature paramater is not included
keys.remove(FacebookParam.SIGNATURE.toString());
String str = "";
for (String key : keys) {
str += key + "=" + params.get(key);
}
str += secret;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes("UTF-8"));
StringBuilder result = new StringBuilder();
for (byte b : md.digest()) {
result.append(Integer.toHexString((b & 0xf0) >>> 4));
result.append(Integer.toHexString(b & 0x0f));
}
return result.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Verifies that the signature of the parameters is valid
*
* @param params
* a map of the parameters. Typically these are the request
* parameters that start with "fb_sig"
* @param expected_sig
* the expected signature
* @return
*/
public boolean verifySignature(Map<String, String> params,
String expected_sig) {
return generateSig(params, secret).equals(expected_sig);
}
/**
* returns a String->String map of the request parameters. It doesn't matter
* if the request method is GET or POST.
*
* @return
*/
private Map<String, String> _getRequestParams() {
Map<String, String> results = new HashMap<String, String>();
Map<String, String[]> map = request.getParameterMap();
for (Entry<String, String[]> entry : map.entrySet()) {
results.put(entry.getKey(), entry.getValue()[0]);
}
return results;
}
private Map<String, String> _getCookiesParams() {
Map<String, String> results = new HashMap<String, String> ();
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
results.put(cookie.getName(), cookie.getValue());
}
}
return results;
}
}