A couple of updates to make it easier to use OAuth1 instead of cookies from the extension.

- Use JSON because it's cleaner and url-encoded with oauth1 is mostly broken.
- Refactor common code
- Update the version of the appengine SDK
- change to version 9 ( will not be live ) to avoid breaking existign devices. In theory everything should be backward compatible.
This commit is contained in:
costin
2011-04-27 16:48:38 +00:00
parent 5f34997cab
commit 88cdc19a72
13 changed files with 553 additions and 302 deletions

View File

@@ -3,6 +3,7 @@
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="c2dm"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="com.google.appengine.eclipse.core.GAE_CONTAINER/appengine-java-sdk-1.3.8"/>
<classpathentry kind="con" path="com.google.appengine.eclipse.core.GAE_CONTAINER"/>
<classpathentry kind="con" path="com.google.appengine.eclipse.core.GAE_CONTAINER/App Engine"/>
<classpathentry kind="output" path="war/WEB-INF/classes"/>
</classpath>

View File

@@ -1,4 +1,4 @@
#Sun Oct 24 20:19:26 BST 2010
#Mon Apr 25 17:13:56 PDT 2011
eclipse.preferences.version=1
filesCopiedToWebInfLib=appengine-api-1.0-sdk-1.3.8.jar|appengine-api-labs-1.3.8.jar|appengine-jsr107cache-1.3.8.jar|jsr107cache-1.1.jar|datanucleus-appengine-1.0.7.final.jar|datanucleus-core-1.1.5.jar|datanucleus-jpa-1.1.5.jar|geronimo-jpa_3.0_spec-1.1.1.jar|geronimo-jta_1.1_spec-1.1.1.jar|jdo2-api-2.3-eb.jar
filesCopiedToWebInfLib=appengine-jsr107cache-1.4.3.jar|jdo2-api-2.3-eb.jar|geronimo-jpa_3.0_spec-1.1.1.jar|geronimo-jta_1.1_spec-1.1.1.jar|datanucleus-jpa-1.1.5.jar|datanucleus-appengine-1.0.8.final.jar|datanucleus-core-1.1.5.jar|appengine-api-labs-1.4.3.jar|appengine-api-1.0-sdk-1.4.3.jar|jsr107cache-1.1.jar
ormEnhancementInclusions=src/**|c2dm/**

View File

@@ -35,8 +35,18 @@ public final class C2DMConfig {
@Persistent
private String authToken;
@Persistent
private String oauth2RefreshToken;
@Persistent
private String clientId;
@Persistent
private String clientSecret;
public static final String DATAMESSAGING_SEND_ENDPOINT =
"https://android.clients.google.com/c2dm/send";
"https://android.apis.google.com/c2dm/send";
@Persistent
private String c2dmUrl;
@@ -64,4 +74,28 @@ public final class C2DMConfig {
return c2dmUrl;
}
}
public void setOauth2RefreshToken(String oauth2RefreshToken) {
this.oauth2RefreshToken = oauth2RefreshToken;
}
public String getOauth2RefreshToken() {
return oauth2RefreshToken;
}
public void setOAuth2ClientId(String clientId) {
this.clientId = clientId;
}
public String getOAuth2ClientId() {
return clientId;
}
public void setOAuth2ClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public String getOAuth2ClientSecret() {
return clientSecret;
}
}

View File

@@ -33,7 +33,7 @@ import com.google.appengine.api.datastore.KeyFactory;
* Stores config information related to data messaging.
*
*/
class C2DMConfigLoader {
public class C2DMConfigLoader {
private final PersistenceManagerFactory PMF;
private static final Logger log = Logger.getLogger(C2DMConfigLoader.class.getName());
@@ -99,7 +99,7 @@ class C2DMConfigLoader {
}
}
private C2DMConfig getDataMessagingConfig(PersistenceManager pm) {
public static C2DMConfig getDataMessagingConfig(PersistenceManager pm) {
Key key = KeyFactory.createKey(C2DMConfig.class.getSimpleName(), 1);
C2DMConfig dmConfig = null;
try {
@@ -110,7 +110,7 @@ class C2DMConfigLoader {
dmConfig.setKey(key);
// Must be in classpath, before sending. Do not checkin !
try {
InputStream is = this.getClass().getClassLoader().getResourceAsStream("/dataMessagingToken.txt");
InputStream is = C2DMConfigLoader.class.getClassLoader().getResourceAsStream("/dataMessagingToken.txt");
if (is != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String token = reader.readLine();

View File

@@ -27,6 +27,13 @@ import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
/**
* Handles login/logout requests by redirecting to the cookie-based login page.
* Has logic to handle redirect limitations, the redirect URL can't be a chrome
* URL.
*
* Not needed if OAuth1 is used.
*/
@SuppressWarnings("serial")
public class AuthServlet extends HttpServlet {
private static final Logger log =
@@ -45,7 +52,7 @@ public class AuthServlet extends HttpServlet {
resp.getWriter().println(ERROR_STATUS + " (extret parameter missing)");
return;
}
// If login/logout is complete, redirect to the extension page. Otherwise, send user to
// login/logout, setting the continue page back to this servlet (since UserService does
// not understand chrome-extension:// URLs)
@@ -69,8 +76,9 @@ public class AuthServlet extends HttpServlet {
log.warning("Invalid redirect " + extRet);
}
} else {
String followOnURL = req.getRequestURI() + "?completed=true&extret=" +
URLEncoder.encode(extRet, "UTF-8");
// Called directly from extension, redirect
String followOnURL = req.getRequestURI() + "?completed=true" +
"&extret=" + URLEncoder.encode(extRet, "UTF-8");
UserService userService = UserServiceFactory.getUserService();
resp.sendRedirect(signIn ? userService.createLoginURL(followOnURL) :
userService.createLogoutURL(followOnURL));

View File

@@ -0,0 +1,19 @@
/*
*/
package com.google.android.chrometophone.server;
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class DebugServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// Nothing, we're just looking for logs to find response times and delivery
// confirmation.
// TODO: use memcache to dynamically get statistics.
}
}

View File

@@ -17,6 +17,7 @@
package com.google.android.chrometophone.server;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
@@ -32,157 +33,156 @@ import com.google.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.channel.ChannelServiceFactory;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.oauth.OAuthService;
import com.google.appengine.api.oauth.OAuthServiceFactory;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.repackaged.org.json.JSONArray;
import com.google.appengine.repackaged.org.json.JSONException;
import com.google.appengine.repackaged.org.json.JSONObject;
@SuppressWarnings("serial")
public class RegisterServlet extends HttpServlet {
private static final Logger log =
Logger.getLogger(RegisterServlet.class.getName());
private static final String OK_STATUS = "OK";
private static final String LOGIN_REQUIRED_STATUS = "LOGIN_REQUIRED";
private static final String ERROR_STATUS = "ERROR";
private static int MAX_DEVICES = 5;
private static int MAX_DEVICES = 10;
/**
* Get the user using the UserService.
*
* If not logged in, return an error message.
*
* @return user, or null if not logged in.
* @throws IOException
* For debug - and possibly show the info, allow device selection.
*/
static User checkUser(HttpServletRequest req, HttpServletResponse resp,
boolean errorIfNotLoggedIn) throws IOException {
// Is it OAuth ?
User user = null;
OAuthService oauthService = OAuthServiceFactory.getOAuthService();
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
RequestInfo reqInfo = RequestInfo.processRequest(req, resp, getServletContext());
if (reqInfo == null) {
return;
}
resp.setContentType("application/json");
JSONObject regs = new JSONObject();
try {
user = oauthService.getCurrentUser();
if (user != null) {
log.info("Found OAuth user " + user);
return user;
regs.put("user", reqInfo.userName);
JSONArray devices = new JSONArray();
for (DeviceInfo di: reqInfo.devices) {
JSONObject dijson = new JSONObject();
dijson.put("key", di.getKey().toString());
dijson.put("name", di.getName());
dijson.put("type", di.getType());
dijson.put("regid", di.getDeviceRegistrationID());
dijson.put("ts", di.getRegistrationTimestamp());
devices.put(dijson);
}
} catch (Throwable t) {
user = null;
}
regs.put("devices", devices);
UserService userService = UserServiceFactory.getUserService();
user = userService.getCurrentUser();
if (user == null && errorIfNotLoggedIn) {
// TODO: redirect to OAuth/user service login, or send the URL
// TODO: 401 instead of 400
resp.setStatus(400);
resp.getWriter().println(LOGIN_REQUIRED_STATUS);
PrintWriter out = resp.getWriter();
regs.write(out);
} catch (JSONException e) {
throw new IOException(e);
}
return user;
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
// Basic XSRF protection
if (req.getHeader("X-Same-Domain") == null) {
log.warning("Blocked XSRF");
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (Missing X-Same-Domain header)");
RequestInfo reqInfo = RequestInfo.processRequest(req, resp,
getServletContext());
if (reqInfo == null) {
return;
}
String deviceRegistrationId = req.getParameter("devregid");
if (deviceRegistrationId == null || "".equals(deviceRegistrationId.trim())) {
if (reqInfo.deviceRegistrationID == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + "(Must specify devregid)");
log.severe("Missing registration id ");
return;
}
String deviceName = req.getParameter("deviceName");
String deviceName = reqInfo.getParameter("deviceName");
if (deviceName == null) {
deviceName = "Phone";
}
// TODO: generate the device name by adding a number suffix for multiple
// devices of same type. Change android app to send model/type.
String deviceType = req.getParameter("deviceType");
String deviceType = reqInfo.getParameter("deviceType");
if (deviceType == null) {
deviceType = "ac2dm";
}
// Because the deviceRegistrationId isn't static, we use a static
// identifier for the device. (Can be null in older clients)
String deviceId = req.getParameter("deviceId");
String deviceId = reqInfo.getParameter("deviceId");
// Context-shared PMF.
PersistenceManager pm =
C2DMessaging.getPMF(getServletContext()).getPersistenceManager();
User user = checkUser(req, resp, true);
if (user != null) {
// Context-shared PMF.
PersistenceManager pm =
C2DMessaging.getPMF(getServletContext()).getPersistenceManager();
try {
List<DeviceInfo> registrations = DeviceInfo.getDeviceInfoForUser(pm,
user.getEmail());
if (registrations.size() > MAX_DEVICES) {
// we could return an error - but user can't handle it yet.
// we can't let it grow out of bounds.
// TODO: we should also define a 'ping' message and expire/remove
// unused registrations
DeviceInfo oldest = registrations.get(0);
if (oldest.getRegistrationTimestamp() == null) {
pm.deletePersistent(oldest);
} else {
long oldestTime = oldest.getRegistrationTimestamp().getTime();
for (int i = 1; i < registrations.size(); i++) {
if (registrations.get(i).getRegistrationTimestamp().getTime() <
oldestTime) {
oldest = registrations.get(i);
oldestTime = oldest.getRegistrationTimestamp().getTime();
}
try {
List<DeviceInfo> registrations = reqInfo.devices;
if (registrations.size() > MAX_DEVICES) {
// we could return an error - but user can't handle it yet.
// we can't let it grow out of bounds.
// TODO: we should also define a 'ping' message and expire/remove
// unused registrations
DeviceInfo oldest = registrations.get(0);
if (oldest.getRegistrationTimestamp() == null) {
pm.deletePersistent(oldest);
} else {
long oldestTime = oldest.getRegistrationTimestamp().getTime();
for (int i = 1; i < registrations.size(); i++) {
if (registrations.get(i).getRegistrationTimestamp().getTime() <
oldestTime) {
oldest = registrations.get(i);
oldestTime = oldest.getRegistrationTimestamp().getTime();
}
pm.deletePersistent(oldest);
}
pm.deletePersistent(oldest);
}
// Get device if it already exists, else create
String suffix =
(deviceId != null ? "#" + Long.toHexString(Math.abs(deviceId.hashCode())) : "");
Key key = KeyFactory.createKey(DeviceInfo.class.getSimpleName(),
user.getEmail() + suffix);
DeviceInfo device = null;
try {
device = pm.getObjectById(DeviceInfo.class, key);
} catch (JDOObjectNotFoundException e) { }
if (device == null) {
device = new DeviceInfo(key, deviceRegistrationId);
device.setType(deviceType);
} else {
// update registration id
device.setDeviceRegistrationID(deviceRegistrationId);
device.setRegistrationTimestamp(new Date());
}
device.setName(deviceName); // update display name
pm.makePersistent(device);
if (device.getType().equals(DeviceInfo.TYPE_CHROME)) {
String channelId =
ChannelServiceFactory.getChannelService().createChannel(deviceRegistrationId);
resp.getWriter().println(OK_STATUS + " " + channelId);
} else {
resp.getWriter().println(OK_STATUS);
}
} catch (Exception e) {
resp.setStatus(500);
resp.getWriter().println(ERROR_STATUS + " (Error registering device)");
log.log(Level.WARNING, "Error registering device.", e);
} finally {
pm.close();
}
// Get device if it already exists, else create
String suffix =
(deviceId != null ? "#" + Long.toHexString(Math.abs(deviceId.hashCode())) : "");
Key key = KeyFactory.createKey(DeviceInfo.class.getSimpleName(),
reqInfo.userName + suffix);
DeviceInfo device = null;
try {
device = pm.getObjectById(DeviceInfo.class, key);
} catch (JDOObjectNotFoundException e) { }
if (device == null) {
device = new DeviceInfo(key, reqInfo.deviceRegistrationID);
device.setType(deviceType);
} else {
// update registration id
device.setDeviceRegistrationID(reqInfo.deviceRegistrationID);
device.setRegistrationTimestamp(new Date());
}
device.setName(deviceName); // update display name
// TODO: only need to write if something changed, for chrome nothing
// changes, we just create a new channel
pm.makePersistent(device);
log.log(Level.INFO, "Registered device " + reqInfo.userName + " " +
deviceType);
if (device.getType().equals(DeviceInfo.TYPE_CHROME)) {
String channelId =
ChannelServiceFactory.getChannelService().createChannel(reqInfo.deviceRegistrationID);
resp.getWriter().println(OK_STATUS + " " + channelId);
} else {
resp.getWriter().println(OK_STATUS);
}
} catch (Exception e) {
resp.setStatus(500);
resp.getWriter().println(ERROR_STATUS + " (Error registering device)");
log.log(Level.WARNING, "Error registering device.", e);
} finally {
pm.close();
}
}
}

View File

@@ -0,0 +1,227 @@
/*
*/
package com.google.android.chrometophone.server;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.oauth.OAuthService;
import com.google.appengine.api.oauth.OAuthServiceFactory;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.repackaged.org.json.JSONException;
import com.google.appengine.repackaged.org.json.JSONObject;
/**
* Common code and helpers to handle a request and manipulate device info.
*
*/
public class RequestInfo {
private static final Logger log =
Logger.getLogger(RequestInfo.class.getName());
private static final String ERROR_STATUS = "ERROR";
private static final String LOGIN_REQUIRED_STATUS = "LOGIN_REQUIRED";
public List<DeviceInfo> devices = new ArrayList<DeviceInfo>();
public String userName;
private ServletContext ctx;
public String deviceRegistrationID;
// Request parameters - transitioning to JSON, but need to support existing
// code.
Map<String, String[]> parameterMap;
JSONObject jsonParams;
public boolean isAuth() {
return userName != null;
}
/**
* Authenticate the user, check headers and pull the registration data.
*
* @return null if authentication fails.
* @throws IOException
*/
public static RequestInfo processRequest(HttpServletRequest req,
HttpServletResponse resp, ServletContext ctx) throws IOException {
// Basic XSRF protection
if (req.getHeader("X-Same-Domain") == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (Missing X-Same-Domain header)");
log.warning("Missing X-Same-Domain");
return null;
}
User user = null;
RequestInfo ri = new RequestInfo();
ri.ctx= ctx;
OAuthService oauthService = OAuthServiceFactory.getOAuthService();
try {
user = oauthService.getCurrentUser();
if (user != null) {
ri.userName = user.getEmail();
}
} catch (Throwable t) {
log.log(Level.SEVERE, "Oauth error ", t);
user = null;
}
if (user == null) {
// Try ClientLogin
UserService userService = UserServiceFactory.getUserService();
user = userService.getCurrentUser();
if (user != null) {
ri.userName = user.getEmail();
}
}
if ("application/json".equals(req.getContentType())) {
Reader reader = req.getReader();
// where is readFully ?
char[] tmp = new char[2048];
StringBuffer body = new StringBuffer();
while (true) {
int cnt = reader.read(tmp);
if (cnt <= 0) {
break;
}
body.append(tmp, 0, cnt);
}
try {
ri.jsonParams = new JSONObject(body.toString());
} catch (JSONException e) {
resp.setStatus(500);
return null;
}
} else {
ri.parameterMap = req.getParameterMap();
}
ri.deviceRegistrationID = ri.getParameter("devregid");
if (ri.deviceRegistrationID != null) {
ri.deviceRegistrationID = ri.deviceRegistrationID.trim();
if ("".equals(ri.deviceRegistrationID)) {
ri.deviceRegistrationID = null;
}
}
if (ri.userName == null) {
resp.setStatus(400); // what android app expects. Should fix this
resp.getWriter().println(LOGIN_REQUIRED_STATUS);
log.warning("Missing user");
return null;
}
if (ctx != null) {
ri.initDevices(ctx);
}
return ri;
}
public String getParameter(String name) {
if (jsonParams != null) {
return jsonParams.optString(name, null);
} else {
String res[] = parameterMap.get(name);
if (res == null || res.length == 0) {
return null;
}
return res[0];
}
}
/**
* Authenticate using the req, fetch devices.
*/
private RequestInfo() {
}
public String toString() {
return userName + " " + devices.size() + " " + jsonParams;
}
public RequestInfo(String userN, ServletContext ctx) {
this.userName = userN;
this.ctx= ctx;
if (ctx != null) {
initDevices(ctx);
}
}
private void initDevices(ServletContext ctx) {
// Context-shared PMF.
PersistenceManager pm =
C2DMessaging.getPMF(ctx).getPersistenceManager();
try {
devices = DeviceInfo.getDeviceInfoForUser(pm,
userName);
// cleanup for multi-device
if (devices.size() > 1) {
// Make sure there is no 'bare' registration
// Keys are sorted - check the first
DeviceInfo first = devices.get(0);
Key oldKey = first.getKey();
if (oldKey.toString().indexOf("#") < 0) {
log.warning("Removing old-style key " + oldKey.toString());
// multiple devices, first is old-style.
devices.remove(0);
pm.deletePersistent(first);
}
}
} catch (Exception e) {
log.log(Level.WARNING, "Error loading registrations ", e);
} finally {
pm.close();
}
}
// We need to iterate again - can be avoided with a query.
// delete will fail if the pm is different than the one used to
// load the object - we must close the object when we're done
public void deleteRegistration(String regId) {
if (ctx == null) {
return;
}
PersistenceManager pm =
C2DMessaging.getPMF(ctx).getPersistenceManager();
try {
List<DeviceInfo> registrations = DeviceInfo.getDeviceInfoForUser(pm, userName);
for (int i = 0; i < registrations.size(); i++) {
DeviceInfo deviceInfo = registrations.get(i);
if (deviceInfo.getDeviceRegistrationID().equals(regId)) {
pm.deletePersistent(deviceInfo);
// Keep looping in case of duplicates
}
}
} catch (JDOObjectNotFoundException e) {
log.warning("User unknown");
} catch (Exception e) {
log.warning("Error unregistering device: " + e.getMessage());
} finally {
pm.close();
}
}
}

View File

@@ -17,10 +17,8 @@
package com.google.android.chrometophone.server;
import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;
import javax.jdo.PersistenceManager;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -28,15 +26,12 @@ import javax.servlet.http.HttpServletResponse;
import com.google.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.channel.ChannelMessage;
import com.google.appengine.api.channel.ChannelServiceFactory;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.users.User;
@SuppressWarnings("serial")
public class SendServlet extends HttpServlet {
static final Logger log =
Logger.getLogger(SendServlet.class.getName());
private static final String OK_STATUS = "OK";
private static final String LOGIN_REQUIRED_STATUS = "LOGIN_REQUIRED";
private static final String DEVICE_NOT_REGISTERED_STATUS = "DEVICE_NOT_REGISTERED";
private static final String ERROR_STATUS = "ERROR";
@@ -46,41 +41,43 @@ public class SendServlet extends HttpServlet {
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
// Basic XSRF protection (TODO: remove X-Extension in a future release for consistency)
if (req.getHeader("X-Same-Domain") == null && req.getHeader("X-Extension") == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (Missing header)");
log.warning("Missing header");
RequestInfo reqInfo = RequestInfo.processRequest(req, resp,
getServletContext());
if (reqInfo == null) {
return;
}
String sel = req.getParameter("sel");
String sel = reqInfo.getParameter("sel");
if (sel == null) sel = ""; // optional
String title = req.getParameter("title");
String title = reqInfo.getParameter("title");
if (title == null) title = ""; // optional
String url = req.getParameter("url");
String url = reqInfo.getParameter("url");
if (url == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (Must specify url parameter)");
return;
}
String[] deviceNames = req.getParameter("deviceName") != null ? req.getParameter("deviceName").split(",") : null;
String deviceType = req.getParameter("deviceType");
String deviceName = reqInfo.getParameter("deviceName");
String[] deviceNames = deviceName != null ?
deviceName.split(",") : null;
User user = RegisterServlet.checkUser(req, resp, false);
if (user != null) {
doSendToDevice(url, title, sel, user.getEmail(),
deviceNames, deviceType, resp);
} else {
resp.getWriter().println(LOGIN_REQUIRED_STATUS);
String deviceType = reqInfo.getParameter("deviceType");
String id = doSendToDevice(url, title, sel, reqInfo,
deviceNames, deviceType);
if (id.startsWith(ERROR_STATUS)) {
resp.setStatus(500);
}
resp.getWriter().println(id);
}
protected boolean doSendToDevice(String url, String title, String sel, String userAccount,
String deviceNames[], String deviceType, HttpServletResponse resp) throws IOException {
protected String doSendToDevice(String url, String title,
String sel, RequestInfo reqInfo,
String deviceNames[], String deviceType) throws IOException {
// ok = we sent to at least one device.
boolean ok = false;
@@ -90,94 +87,76 @@ public class SendServlet extends HttpServlet {
boolean res = false;
String collapseKey = "" + url.hashCode();
boolean reqDebug = "1".equals(reqInfo.getParameter("debug"));
PersistenceManager pm =
C2DMessaging.getPMF(getServletContext()).getPersistenceManager();
// delete will fail if the pm is different than the one used to
// load the object - we must close the object when we're done
List<DeviceInfo> registrations = null;
try {
registrations = DeviceInfo.getDeviceInfoForUser(pm, userAccount);
// Deal with upgrades and multi-device:
// If user has one device with an old version and few new ones -
// the old registration will be deleted.
if (registrations.size() > 1) {
// Make sure there is no 'bare' registration
// Keys are sorted - check the first
DeviceInfo first = registrations.get(0);
Key oldKey = first.getKey();
if (oldKey.toString().indexOf("#") < 0) {
log.warning("Removing old-style key " + oldKey.toString());
// multiple devices, first is old-style.
registrations.remove(0); // don't send to it
pm.deletePersistent(first);
int ac2dmCnt = 0;
for (DeviceInfo deviceInfo : reqInfo.devices) {
if ("ac2dm".equals(deviceInfo.getType())) {
ac2dmCnt++;
}
if (deviceNames != null) {
boolean found = false;
for (int i = 0; i < deviceNames.length; i++) {
if (deviceNames[i].equals(deviceInfo.getName())) {
found = true;
break;
}
}
if (!found) continue; // user-specified device name
}
int numSendAttempts = 0;
for (DeviceInfo deviceInfo : registrations) {
if (deviceNames != null) {
boolean found = false;
for (int i = 0; i < deviceNames.length; i++) {
if (deviceNames[i].equals(deviceInfo.getName())) {
found = true;
break;
}
}
if (!found) continue; // user-specified device name
}
if (deviceType != null && !deviceType.equals(deviceInfo.getType())) {
continue; // user-specified device type
}
try {
if (deviceInfo.getType().equals(DeviceInfo.TYPE_CHROME)) {
res = doSendViaBrowserChannel(url, deviceInfo);
} else {
res = doSendViaC2dm(url, title, sel, push, collapseKey, deviceInfo);
}
numSendAttempts++;
if (res) {
log.info("Link sent to phone! collapse_key:" + collapseKey);
ok = true;
} else {
log.warning("Error: Unable to send link to phone.");
}
} catch (IOException ex) {
if ("NotRegistered".equals(ex.getMessage()) ||
"InvalidRegistration".equals(ex.getMessage())) {
// Prune device, it no longer works
pm.deletePersistent(deviceInfo);
} else {
throw ex;
}
}
if (deviceType != null && !deviceType.equals(deviceInfo.getType())) {
continue; // user-specified device type
}
if (ok) {
resp.getWriter().println(OK_STATUS);
return true;
} else if (numSendAttempts == 0) {
log.warning("Device not registered " + userAccount);
resp.getWriter().println(DEVICE_NOT_REGISTERED_STATUS);
return false;
try {
if (deviceInfo.getType().equals(DeviceInfo.TYPE_CHROME)) {
res = doSendViaBrowserChannel(url, deviceInfo);
} else {
res = doSendViaC2dm(url, title, sel, push, collapseKey,
deviceInfo, reqDebug);
}
if (res) {
log.info("Link sent to phone! collapse_key:" + collapseKey);
ok = true;
} else {
log.warning("Error: Unable to send link to device: " +
deviceInfo.getDeviceRegistrationID());
}
} catch (IOException ex) {
if ("NotRegistered".equals(ex.getMessage()) ||
"InvalidRegistration".equals(ex.getMessage())) {
// Prune device, it no longer works
reqInfo.deleteRegistration(deviceInfo.getDeviceRegistrationID());
reqInfo.devices.remove(deviceInfo);
ac2dmCnt--;
} else {
throw ex;
}
}
}
if (ok) {
// TODO: return a count of devices we sent to, maybe names as well
return OK_STATUS;
} else {
// Show the 'no devices' if only the browser is registered.
// We should also clarify that 'error status' mean no matching
// device found ( when the extension allow specifying the destination )
if (ac2dmCnt == 0 && "ac2dm".equals(deviceType)) {
log.warning("No device registered for " + reqInfo.userName);
return DEVICE_NOT_REGISTERED_STATUS;
} else {
resp.setStatus(500);
resp.getWriter().println(ERROR_STATUS + " (Unable to send link)");
return false;
return ERROR_STATUS + " (Unable to send link)";
}
} finally {
pm.close();
}
}
boolean doSendViaC2dm(String url, String title, String sel, C2DMessaging push,
String collapseKey, DeviceInfo deviceInfo) throws IOException {
private boolean doSendViaC2dm(String url, String title, String sel, C2DMessaging push,
String collapseKey, DeviceInfo deviceInfo, boolean reqDebug) throws IOException {
// Trim title, sel if needed.
if (url.length() + title.length() + sel.length() > 1000) {
@@ -201,11 +180,11 @@ public class SendServlet extends HttpServlet {
"url", url,
"title", title,
"sel", sel,
"debug", deviceInfo.getDebug() ? "1" : null);
"debug", deviceInfo.getDebug() || reqDebug ? "1" : null);
return res;
}
boolean doSendViaBrowserChannel(String url, DeviceInfo deviceInfo) {
private boolean doSendViaBrowserChannel(String url, DeviceInfo deviceInfo) {
String channelToken = deviceInfo.getDeviceRegistrationID();
ChannelServiceFactory.getChannelService().sendMessage(
new ChannelMessage(channelToken, url));

View File

@@ -17,24 +17,13 @@
package com.google.android.chrometophone.server;
import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
@SuppressWarnings("serial")
public class UnregisterServlet extends HttpServlet {
private static final Logger log =
Logger.getLogger(RegisterServlet.class.getName());
private static final String OK_STATUS = "OK";
private static final String ERROR_STATUS = "ERROR";
@@ -42,50 +31,19 @@ public class UnregisterServlet extends HttpServlet {
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
// Basic XSRF protection
if (req.getHeader("X-Same-Domain") == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (Missing X-Same-Domain header)");
RequestInfo reqInfo = RequestInfo.processRequest(req, resp,
getServletContext());
if (reqInfo == null) {
return;
}
String deviceRegistrationID = req.getParameter("devregid");
if (deviceRegistrationID == null) {
if (reqInfo.deviceRegistrationID == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (Must specify devregid)");
return;
}
// Authorize & store device info
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
if (user != null) {
PersistenceManager pm =
C2DMessaging.getPMF(getServletContext()).getPersistenceManager();
try {
List<DeviceInfo> registrations = DeviceInfo.getDeviceInfoForUser(pm, user.getEmail());
for (int i = 0; i < registrations.size(); i++) {
DeviceInfo deviceInfo = registrations.get(i);
if (deviceInfo.getDeviceRegistrationID().equals(deviceRegistrationID)) {
pm.deletePersistent(deviceInfo);
// Keep looping in case of duplicates
}
}
resp.getWriter().println(OK_STATUS);
} catch (JDOObjectNotFoundException e) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (User unknown)");
log.warning("User unknown");
} catch (Exception e) {
resp.setStatus(500);
resp.getWriter().println(ERROR_STATUS + " (Error unregistering device)");
log.warning("Error unregistering device: " + e.getMessage());
} finally {
pm.close();
}
} else {
resp.getWriter().println(ERROR_STATUS + " (Not authorized)");
}
reqInfo.deleteRegistration(reqInfo.deviceRegistrationID);
resp.getWriter().println(OK_STATUS);
}
}

View File

@@ -24,9 +24,9 @@ import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.users.User;
import com.google.appengine.api.xmpp.JID;
import com.google.appengine.api.xmpp.Message;
import com.google.appengine.api.xmpp.MessageBuilder;
import com.google.appengine.api.xmpp.XMPPService;
import com.google.appengine.api.xmpp.XMPPServiceFactory;
@@ -58,6 +58,10 @@ public class XMPPSendServlet extends SendServlet {
if (resIdx > 0) {
jid = jid.substring(0, resIdx);
}
if (body.equals("register")) {
return;
}
Map<String, String> params = new HashMap<String, String>();
@@ -87,8 +91,17 @@ public class XMPPSendServlet extends SendServlet {
log.info("Sending " + jid);
doSendToDevice(url, title, sel, jid,
RequestInfo reqInfo = new RequestInfo(jid, getServletContext());
String id = doSendToDevice(url, title, sel, reqInfo,
deviceName == null ? null : new String[] {deviceName},
deviceType, resp);
deviceType);
// Confirm
Message respmsg =
new MessageBuilder()
.withBody(id)
.withRecipientJids(fromJid)
.withFromJid(message.getRecipientJids()[0]).build();
xmpp.sendMessage(respmsg);
}
}

View File

@@ -16,7 +16,7 @@
-->
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<application>chrometophone</application>
<version>8</version>
<version>9</version>
<system-properties>
<property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
</system-properties>

View File

@@ -22,48 +22,60 @@
com.google.android.chrometophone.server.RegisterServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>UnregisterServlet</servlet-name>
<servlet-class>
com.google.android.chrometophone.server.UnregisterServlet
</servlet-class>
</servlet>
<servlet-name>UnregisterServlet</servlet-name>
<servlet-class>
com.google.android.chrometophone.server.UnregisterServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>DebugServlet</servlet-name>
<servlet-class>
com.google.android.chrometophone.server.DebugServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>SendServlet</servlet-name>
<servlet-class>com.google.android.chrometophone.server.SendServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>XMPPSendServlet</servlet-name>
<servlet-class>com.google.android.chrometophone.server.XMPPSendServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>AuthServlet</servlet-name>
<servlet-class>com.google.android.chrometophone.server.AuthServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>dataMessagingServlet</servlet-name>
<servlet-class>
com.google.android.c2dm.server.C2DMRetryServlet
<servlet>
<servlet-name>XMPPSendServlet</servlet-name>
<servlet-class>com.google.android.chrometophone.server.XMPPSendServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>AuthServlet</servlet-name>
<servlet-class>com.google.android.chrometophone.server.AuthServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>dataMessagingServlet</servlet-name>
<servlet-class>
com.google.android.c2dm.server.C2DMRetryServlet
</servlet-class>
</servlet>
</servlet>
<servlet-mapping>
<servlet-name>RegisterServlet</servlet-name>
<url-pattern>/register</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>UnregisterServlet</servlet-name>
<url-pattern>/unregister</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>UnregisterServlet</servlet-name>
<url-pattern>/unregister</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>DebugServlet</servlet-name>
<url-pattern>/debug</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>XMPPSendServlet</servlet-name>