2016-03-05 02:48:09 +03:00
|
|
|
/* jshint moz: true, esnext: true */
|
|
|
|
/* 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 Cc = Components.classes;
|
|
|
|
const Ci = Components.interfaces;
|
|
|
|
const Cu = Components.utils;
|
|
|
|
const Cr = Components.results;
|
|
|
|
|
|
|
|
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
|
|
|
|
const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
|
2016-10-05 18:56:55 +03:00
|
|
|
const {PushCrypto} = Cu.import("resource://gre/modules/PushCrypto.jsm");
|
2017-02-02 01:35:44 +03:00
|
|
|
Cu.import("resource://gre/modules/Messaging.jsm"); /*global: EventDispatcher */
|
2016-03-05 02:48:09 +03:00
|
|
|
Cu.import("resource://gre/modules/Services.jsm"); /*global: Services */
|
|
|
|
Cu.import("resource://gre/modules/Preferences.jsm"); /*global: Preferences */
|
|
|
|
Cu.import("resource://gre/modules/Promise.jsm"); /*global: Promise */
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global: XPCOMUtils */
|
|
|
|
|
|
|
|
const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("Push");
|
|
|
|
|
|
|
|
this.EXPORTED_SYMBOLS = ["PushServiceAndroidGCM"];
|
|
|
|
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "console", () => {
|
|
|
|
let {ConsoleAPI} = Cu.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";
|
|
|
|
|
2016-07-20 20:47:04 +03:00
|
|
|
const FXA_PUSH_SCOPE = "chrome://fxa-push";
|
|
|
|
|
2016-03-05 02:48:09 +03:00
|
|
|
const prefs = new Preferences("dom.push.");
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The implementation of WebPush push backed by Android's GCM
|
|
|
|
* delivery.
|
|
|
|
*/
|
|
|
|
this.PushServiceAndroidGCM = {
|
|
|
|
_mainPushService: null,
|
|
|
|
_serverURI: null,
|
|
|
|
|
|
|
|
newPushDB: function() {
|
|
|
|
return new PushDB(kPUSHANDROIDGCMDB_DB_NAME,
|
|
|
|
kPUSHANDROIDGCMDB_DB_VERSION,
|
|
|
|
kPUSHANDROIDGCMDB_STORE_NAME,
|
|
|
|
"channelID",
|
|
|
|
PushRecordAndroidGCM);
|
|
|
|
},
|
|
|
|
|
|
|
|
validServerURI: function(serverURI) {
|
|
|
|
if (!serverURI) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (serverURI.scheme == "https") {
|
|
|
|
return true;
|
|
|
|
}
|
2016-04-26 06:53:06 +03:00
|
|
|
if (serverURI.scheme == "http") {
|
|
|
|
// Allow insecure server URLs for development and testing.
|
|
|
|
return !!prefs.get("testing.allowInsecureServerURL");
|
2016-03-05 02:48:09 +03:00
|
|
|
}
|
|
|
|
console.info("Unsupported Android GCM dom.push.serverURL scheme", serverURI.scheme);
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
observe: function(subject, topic, data) {
|
2016-07-20 20:47:04 +03:00
|
|
|
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;
|
2016-03-05 02:48:09 +03:00
|
|
|
}
|
2016-07-20 20:47:04 +03:00
|
|
|
},
|
2016-03-05 02:48:09 +03:00
|
|
|
|
2016-07-20 20:47:04 +03:00
|
|
|
_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.");
|
2016-08-17 07:28:51 +03:00
|
|
|
return;
|
2016-07-20 20:47:04 +03:00
|
|
|
}
|
2016-07-20 20:47:04 +03:00
|
|
|
if (!data) {
|
|
|
|
console.error("No data from Java! Dropping message.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
data = JSON.parse(data);
|
|
|
|
console.debug("ReceivedPushMessage with data", data);
|
|
|
|
|
2016-10-05 18:56:55 +03:00
|
|
|
let { headers, message } = this._messageAndHeaders(data);
|
2016-07-20 20:47:04 +03:00
|
|
|
|
2016-10-05 18:56:55 +03:00
|
|
|
console.debug("Delivering message to main PushService:", message, headers);
|
2016-07-20 20:47:04 +03:00
|
|
|
this._mainPushService.receivedPushMessage(
|
2016-10-05 18:56:55 +03:00
|
|
|
data.channelID, "", headers, message, (record) => {
|
2016-07-20 20:47:04 +03:00
|
|
|
// Always update the stored record.
|
|
|
|
return record;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2016-10-05 18:56:55 +03:00
|
|
|
_messageAndHeaders(data) {
|
2016-07-20 20:47:04 +03:00
|
|
|
// Default is no data (and no encryption).
|
|
|
|
let message = null;
|
2016-10-05 18:56:55 +03:00
|
|
|
let headers = null;
|
2016-07-20 20:47:04 +03:00
|
|
|
|
|
|
|
if (data.message && data.enc && (data.enckey || data.cryptokey)) {
|
2016-10-05 18:56:55 +03:00
|
|
|
headers = {
|
2016-07-20 20:47:04 +03:00
|
|
|
encryption_key: data.enckey,
|
|
|
|
crypto_key: data.cryptokey,
|
|
|
|
encryption: data.enc,
|
|
|
|
encoding: data.con,
|
|
|
|
};
|
|
|
|
// Ciphertext is (urlsafe) Base 64 encoded.
|
|
|
|
message = ChromeUtils.base64URLDecode(data.message, {
|
|
|
|
// The Push server may append padding.
|
|
|
|
padding: "ignore",
|
|
|
|
});
|
|
|
|
}
|
2016-10-05 18:56:55 +03:00
|
|
|
return { headers, message };
|
2016-07-20 20:47:04 +03:00
|
|
|
},
|
|
|
|
|
2016-03-05 02:48:09 +03:00
|
|
|
_configure: function(serverURL, debug) {
|
2017-02-02 01:35:44 +03:00
|
|
|
return EventDispatcher.instance.sendRequestForResult({
|
2016-03-05 02:48:09 +03:00
|
|
|
type: "PushServiceAndroidGCM:Configure",
|
|
|
|
endpoint: serverURL.spec,
|
|
|
|
debug: debug,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
init: function(options, mainPushService, serverURL) {
|
|
|
|
console.debug("init()");
|
|
|
|
this._mainPushService = mainPushService;
|
|
|
|
this._serverURI = serverURL;
|
|
|
|
|
|
|
|
prefs.observe("debug", this);
|
2017-04-14 22:51:38 +03:00
|
|
|
Services.obs.addObserver(this, "PushServiceAndroidGCM:ReceivedPushMessage");
|
2016-03-05 02:48:09 +03:00
|
|
|
|
2016-04-14 02:55:25 +03:00
|
|
|
return this._configure(serverURL, !!prefs.get("debug")).then(() => {
|
2017-02-02 01:35:44 +03:00
|
|
|
EventDispatcher.instance.sendRequestForResult({
|
2016-04-14 02:55:25 +03:00
|
|
|
type: "PushServiceAndroidGCM:Initialized"
|
|
|
|
});
|
|
|
|
});
|
2016-03-05 02:48:09 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
uninit: function() {
|
|
|
|
console.debug("uninit()");
|
2017-02-02 01:35:44 +03:00
|
|
|
EventDispatcher.instance.sendRequestForResult({
|
2016-04-14 02:55:25 +03:00
|
|
|
type: "PushServiceAndroidGCM:Uninitialized"
|
|
|
|
});
|
|
|
|
|
2016-03-05 02:48:09 +03:00
|
|
|
this._mainPushService = null;
|
2016-08-17 07:28:51 +03:00
|
|
|
Services.obs.removeObserver(this, "PushServiceAndroidGCM:ReceivedPushMessage");
|
2016-03-05 02:48:09 +03:00
|
|
|
prefs.ignore("debug", this);
|
|
|
|
},
|
|
|
|
|
|
|
|
onAlarmFired: function() {
|
|
|
|
// No action required.
|
|
|
|
},
|
|
|
|
|
|
|
|
connect: function(records) {
|
|
|
|
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.
|
2017-02-02 01:35:44 +03:00
|
|
|
return EventDispatcher.instance.sendRequestForResult({
|
2016-03-05 02:48:09 +03:00
|
|
|
type: "PushServiceAndroidGCM:DumpSubscriptions",
|
|
|
|
})
|
|
|
|
.then(subscriptions => {
|
2017-02-15 20:47:30 +03:00
|
|
|
subscriptions = JSON.parse(subscriptions);
|
2016-03-05 02:48:09 +03:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
isConnected: function() {
|
|
|
|
return this._mainPushService != null;
|
|
|
|
},
|
|
|
|
|
|
|
|
disconnect: function() {
|
|
|
|
console.debug("disconnect");
|
|
|
|
},
|
|
|
|
|
2016-03-28 22:29:25 +03:00
|
|
|
register: function(record) {
|
|
|
|
console.debug("register:", record);
|
2016-03-05 02:48:09 +03:00
|
|
|
let ctime = Date.now();
|
2016-03-22 22:09:31 +03:00
|
|
|
let appServerKey = record.appServerKey ?
|
|
|
|
ChromeUtils.base64URLEncode(record.appServerKey, {
|
|
|
|
// The Push server requires padding.
|
|
|
|
pad: true,
|
|
|
|
}) : null;
|
2016-07-20 20:47:04 +03:00
|
|
|
let message = {
|
2016-03-05 02:48:09 +03:00
|
|
|
type: "PushServiceAndroidGCM:SubscribeChannel",
|
2016-03-22 22:09:31 +03:00
|
|
|
appServerKey: appServerKey,
|
2016-07-20 20:47:04 +03:00
|
|
|
}
|
|
|
|
if (record.scope == FXA_PUSH_SCOPE) {
|
|
|
|
message.service = "fxa";
|
|
|
|
}
|
|
|
|
// Caller handles errors.
|
2017-02-02 01:35:44 +03:00
|
|
|
return EventDispatcher.instance.sendRequestForResult(message)
|
2016-07-20 20:47:04 +03:00
|
|
|
.then(data => {
|
2017-02-15 20:47:30 +03:00
|
|
|
data = JSON.parse(data);
|
2016-03-05 02:48:09 +03:00
|
|
|
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: ctime,
|
2016-07-20 20:47:04 +03:00
|
|
|
systemRecord: record.systemRecord,
|
2016-03-05 02:48:09 +03:00
|
|
|
// Cryptography!
|
|
|
|
p256dhPublicKey: exportedKeys[0],
|
|
|
|
p256dhPrivateKey: exportedKeys[1],
|
|
|
|
authenticationSecret: PushCrypto.generateAuthenticationSecret(),
|
2016-03-22 22:09:31 +03:00
|
|
|
appServerKey: record.appServerKey,
|
2016-03-05 02:48:09 +03:00
|
|
|
})
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2016-03-28 22:29:25 +03:00
|
|
|
unregister: function(record) {
|
|
|
|
console.debug("unregister: ", record);
|
2017-02-02 01:35:44 +03:00
|
|
|
return EventDispatcher.instance.sendRequestForResult({
|
2016-03-05 02:48:09 +03:00
|
|
|
type: "PushServiceAndroidGCM:UnsubscribeChannel",
|
|
|
|
channelID: record.keyID,
|
|
|
|
});
|
|
|
|
},
|
2016-04-21 23:35:26 +03:00
|
|
|
|
|
|
|
reportDeliveryError: function(messageID, reason) {
|
|
|
|
console.warn("reportDeliveryError: Ignoring message delivery error",
|
|
|
|
messageID, reason);
|
|
|
|
},
|
2016-03-05 02:48:09 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
function PushRecordAndroidGCM(record) {
|
|
|
|
PushRecord.call(this, record);
|
|
|
|
this.channelID = record.channelID;
|
|
|
|
}
|
|
|
|
|
|
|
|
PushRecordAndroidGCM.prototype = Object.create(PushRecord.prototype, {
|
|
|
|
keyID: {
|
|
|
|
get() {
|
|
|
|
return this.channelID;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|