diff options
Diffstat (limited to 'dom/push/PushServiceAndroidGCM.jsm')
-rw-r--r-- | dom/push/PushServiceAndroidGCM.jsm | 317 |
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; + }, + }, +}); |