summaryrefslogtreecommitdiffstats
path: root/dom/push/PushServiceAndroidGCM.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'dom/push/PushServiceAndroidGCM.jsm')
-rw-r--r--dom/push/PushServiceAndroidGCM.jsm317
1 files changed, 317 insertions, 0 deletions
diff --git a/dom/push/PushServiceAndroidGCM.jsm b/dom/push/PushServiceAndroidGCM.jsm
new file mode 100644
index 0000000000..596f5c4e36
--- /dev/null
+++ b/dom/push/PushServiceAndroidGCM.jsm
@@ -0,0 +1,317 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PushDB",
+ "resource://gre/modules/PushDB.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PushRecord",
+ "resource://gre/modules/PushRecord.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PushCrypto",
+ "resource://gre/modules/PushCrypto.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "EventDispatcher",
+ "resource://gre/modules/Messaging.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "Preferences",
+ "resource://gre/modules/Preferences.jsm"
+);
+
+XPCOMUtils.defineLazyGetter(this, "Log", () => {
+ return ChromeUtils.import(
+ "resource://gre/modules/AndroidLog.jsm",
+ {}
+ ).AndroidLog.bind("Push");
+});
+
+const EXPORTED_SYMBOLS = ["PushServiceAndroidGCM"];
+
+XPCOMUtils.defineLazyGetter(this, "console", () => {
+ let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
+ return new ConsoleAPI({
+ dump: Log.i,
+ maxLogLevelPref: "dom.push.loglevel",
+ prefix: "PushServiceAndroidGCM",
+ });
+});
+
+const kPUSHANDROIDGCMDB_DB_NAME = "pushAndroidGCM";
+const kPUSHANDROIDGCMDB_DB_VERSION = 5; // Change this if the IndexedDB format changes
+const kPUSHANDROIDGCMDB_STORE_NAME = "pushAndroidGCM";
+
+const FXA_PUSH_SCOPE = "chrome://fxa-push";
+
+const prefs = new Preferences("dom.push.");
+
+/**
+ * The implementation of WebPush push backed by Android's GCM
+ * delivery.
+ */
+var PushServiceAndroidGCM = {
+ _mainPushService: null,
+ _serverURI: null,
+
+ newPushDB() {
+ return new PushDB(
+ kPUSHANDROIDGCMDB_DB_NAME,
+ kPUSHANDROIDGCMDB_DB_VERSION,
+ kPUSHANDROIDGCMDB_STORE_NAME,
+ "channelID",
+ PushRecordAndroidGCM
+ );
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "nsPref:changed":
+ if (data == "dom.push.debug") {
+ // Reconfigure.
+ let debug = !!prefs.get("debug");
+ console.info(
+ "Debug parameter changed; updating configuration with new debug",
+ debug
+ );
+ this._configure(this._serverURI, debug);
+ }
+ break;
+ case "PushServiceAndroidGCM:ReceivedPushMessage":
+ this._onPushMessageReceived(data);
+ break;
+ default:
+ break;
+ }
+ },
+
+ _onPushMessageReceived(data) {
+ // TODO: Use Messaging.jsm for this.
+ if (this._mainPushService == null) {
+ // Shouldn't ever happen, but let's be careful.
+ console.error("No main PushService! Dropping message.");
+ return;
+ }
+ if (!data) {
+ console.error("No data from Java! Dropping message.");
+ return;
+ }
+ data = JSON.parse(data);
+ console.debug("ReceivedPushMessage with data", data);
+
+ let { headers, message } = this._messageAndHeaders(data);
+
+ console.debug("Delivering message to main PushService:", message, headers);
+ this._mainPushService.receivedPushMessage(
+ data.channelID,
+ "",
+ headers,
+ message,
+ record => {
+ // Always update the stored record.
+ return record;
+ }
+ );
+ },
+
+ _messageAndHeaders(data) {
+ // Default is no data (and no encryption).
+ let message = null;
+ let headers = null;
+
+ if (data.message) {
+ if (data.enc && (data.enckey || data.cryptokey)) {
+ headers = {
+ encryption_key: data.enckey,
+ crypto_key: data.cryptokey,
+ encryption: data.enc,
+ encoding: data.con,
+ };
+ } else if (data.con == "aes128gcm") {
+ headers = {
+ encoding: data.con,
+ };
+ }
+ // Ciphertext is (urlsafe) Base 64 encoded.
+ message = ChromeUtils.base64URLDecode(data.message, {
+ // The Push server may append padding.
+ padding: "ignore",
+ });
+ }
+
+ return { headers, message };
+ },
+
+ _configure(serverURL, debug) {
+ return EventDispatcher.instance.sendRequestForResult({
+ type: "PushServiceAndroidGCM:Configure",
+ endpoint: serverURL.spec,
+ debug,
+ });
+ },
+
+ init(options, mainPushService, serverURL) {
+ console.debug("init()");
+ this._mainPushService = mainPushService;
+ this._serverURI = serverURL;
+
+ prefs.observe("debug", this);
+ Services.obs.addObserver(this, "PushServiceAndroidGCM:ReceivedPushMessage");
+
+ return this._configure(serverURL, !!prefs.get("debug")).then(() => {
+ EventDispatcher.instance.sendRequestForResult({
+ type: "PushServiceAndroidGCM:Initialized",
+ });
+ });
+ },
+
+ uninit() {
+ console.debug("uninit()");
+ EventDispatcher.instance.sendRequestForResult({
+ type: "PushServiceAndroidGCM:Uninitialized",
+ });
+
+ this._mainPushService = null;
+ Services.obs.removeObserver(
+ this,
+ "PushServiceAndroidGCM:ReceivedPushMessage"
+ );
+ prefs.ignore("debug", this);
+ },
+
+ onAlarmFired() {
+ // No action required.
+ },
+
+ connect(records, broadcastListeners) {
+ console.debug("connect:", records);
+ // It's possible for the registration or subscriptions backing the
+ // PushService to not be registered with the underlying AndroidPushService.
+ // Expire those that are unrecognized.
+ return EventDispatcher.instance
+ .sendRequestForResult({
+ type: "PushServiceAndroidGCM:DumpSubscriptions",
+ })
+ .then(subscriptions => {
+ subscriptions = JSON.parse(subscriptions);
+ console.debug("connect:", subscriptions);
+ // subscriptions maps chid => subscription data.
+ return Promise.all(
+ records.map(record => {
+ if (subscriptions.hasOwnProperty(record.keyID)) {
+ console.debug("connect:", "hasOwnProperty", record.keyID);
+ return Promise.resolve();
+ }
+ console.debug("connect:", "!hasOwnProperty", record.keyID);
+ // Subscription is known to PushService.jsm but not to AndroidPushService. Drop it.
+ return this._mainPushService
+ .dropRegistrationAndNotifyApp(record.keyID)
+ .catch(error => {
+ console.error(
+ "connect: Error dropping registration",
+ record.keyID,
+ error
+ );
+ });
+ })
+ );
+ });
+ },
+
+ async sendSubscribeBroadcast(serviceId, version) {
+ // Not implemented yet
+ },
+
+ isConnected() {
+ return this._mainPushService != null;
+ },
+
+ disconnect() {
+ console.debug("disconnect");
+ },
+
+ register(record) {
+ console.debug("register:", record);
+ let ctime = Date.now();
+ let appServerKey = record.appServerKey
+ ? ChromeUtils.base64URLEncode(record.appServerKey, {
+ // The Push server requires padding.
+ pad: true,
+ })
+ : null;
+ let message = {
+ type: "PushServiceAndroidGCM:SubscribeChannel",
+ appServerKey,
+ };
+ if (record.scope == FXA_PUSH_SCOPE) {
+ message.service = "fxa";
+ }
+ // Caller handles errors.
+ return EventDispatcher.instance.sendRequestForResult(message).then(data => {
+ data = JSON.parse(data);
+ console.debug("Got data:", data);
+ return PushCrypto.generateKeys().then(
+ exportedKeys =>
+ new PushRecordAndroidGCM({
+ // Straight from autopush.
+ channelID: data.channelID,
+ pushEndpoint: data.endpoint,
+ // Common to all PushRecord implementations.
+ scope: record.scope,
+ originAttributes: record.originAttributes,
+ ctime,
+ systemRecord: record.systemRecord,
+ // Cryptography!
+ p256dhPublicKey: exportedKeys[0],
+ p256dhPrivateKey: exportedKeys[1],
+ authenticationSecret: PushCrypto.generateAuthenticationSecret(),
+ appServerKey: record.appServerKey,
+ })
+ );
+ });
+ },
+
+ unregister(record) {
+ console.debug("unregister: ", record);
+ return EventDispatcher.instance.sendRequestForResult({
+ type: "PushServiceAndroidGCM:UnsubscribeChannel",
+ channelID: record.keyID,
+ });
+ },
+
+ reportDeliveryError(messageID, reason) {
+ console.warn(
+ "reportDeliveryError: Ignoring message delivery error",
+ messageID,
+ reason
+ );
+ },
+};
+
+function PushRecordAndroidGCM(record) {
+ PushRecord.call(this, record);
+ this.channelID = record.channelID;
+}
+
+PushRecordAndroidGCM.prototype = Object.create(PushRecord.prototype, {
+ keyID: {
+ get() {
+ return this.channelID;
+ },
+ },
+});