Bug 1708193 - Remove mozapps/extensions/internal/Content.js r=rpl
[gecko.git] / dom / push / PushServiceAndroidGCM.jsm
blob596f5c4e367dcf6c2ba6d9d3a803bce973f0a9fb
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
8 const { XPCOMUtils } = ChromeUtils.import(
9   "resource://gre/modules/XPCOMUtils.jsm"
12 ChromeUtils.defineModuleGetter(
13   this,
14   "PushDB",
15   "resource://gre/modules/PushDB.jsm"
17 ChromeUtils.defineModuleGetter(
18   this,
19   "PushRecord",
20   "resource://gre/modules/PushRecord.jsm"
22 ChromeUtils.defineModuleGetter(
23   this,
24   "PushCrypto",
25   "resource://gre/modules/PushCrypto.jsm"
27 ChromeUtils.defineModuleGetter(
28   this,
29   "EventDispatcher",
30   "resource://gre/modules/Messaging.jsm"
32 ChromeUtils.defineModuleGetter(
33   this,
34   "Preferences",
35   "resource://gre/modules/Preferences.jsm"
38 XPCOMUtils.defineLazyGetter(this, "Log", () => {
39   return ChromeUtils.import(
40     "resource://gre/modules/AndroidLog.jsm",
41     {}
42   ).AndroidLog.bind("Push");
43 });
45 const EXPORTED_SYMBOLS = ["PushServiceAndroidGCM"];
47 XPCOMUtils.defineLazyGetter(this, "console", () => {
48   let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
49   return new ConsoleAPI({
50     dump: Log.i,
51     maxLogLevelPref: "dom.push.loglevel",
52     prefix: "PushServiceAndroidGCM",
53   });
54 });
56 const kPUSHANDROIDGCMDB_DB_NAME = "pushAndroidGCM";
57 const kPUSHANDROIDGCMDB_DB_VERSION = 5; // Change this if the IndexedDB format changes
58 const kPUSHANDROIDGCMDB_STORE_NAME = "pushAndroidGCM";
60 const FXA_PUSH_SCOPE = "chrome://fxa-push";
62 const prefs = new Preferences("dom.push.");
64 /**
65  * The implementation of WebPush push backed by Android's GCM
66  * delivery.
67  */
68 var PushServiceAndroidGCM = {
69   _mainPushService: null,
70   _serverURI: null,
72   newPushDB() {
73     return new PushDB(
74       kPUSHANDROIDGCMDB_DB_NAME,
75       kPUSHANDROIDGCMDB_DB_VERSION,
76       kPUSHANDROIDGCMDB_STORE_NAME,
77       "channelID",
78       PushRecordAndroidGCM
79     );
80   },
82   observe(subject, topic, data) {
83     switch (topic) {
84       case "nsPref:changed":
85         if (data == "dom.push.debug") {
86           // Reconfigure.
87           let debug = !!prefs.get("debug");
88           console.info(
89             "Debug parameter changed; updating configuration with new debug",
90             debug
91           );
92           this._configure(this._serverURI, debug);
93         }
94         break;
95       case "PushServiceAndroidGCM:ReceivedPushMessage":
96         this._onPushMessageReceived(data);
97         break;
98       default:
99         break;
100     }
101   },
103   _onPushMessageReceived(data) {
104     // TODO: Use Messaging.jsm for this.
105     if (this._mainPushService == null) {
106       // Shouldn't ever happen, but let's be careful.
107       console.error("No main PushService!  Dropping message.");
108       return;
109     }
110     if (!data) {
111       console.error("No data from Java!  Dropping message.");
112       return;
113     }
114     data = JSON.parse(data);
115     console.debug("ReceivedPushMessage with data", data);
117     let { headers, message } = this._messageAndHeaders(data);
119     console.debug("Delivering message to main PushService:", message, headers);
120     this._mainPushService.receivedPushMessage(
121       data.channelID,
122       "",
123       headers,
124       message,
125       record => {
126         // Always update the stored record.
127         return record;
128       }
129     );
130   },
132   _messageAndHeaders(data) {
133     // Default is no data (and no encryption).
134     let message = null;
135     let headers = null;
137     if (data.message) {
138       if (data.enc && (data.enckey || data.cryptokey)) {
139         headers = {
140           encryption_key: data.enckey,
141           crypto_key: data.cryptokey,
142           encryption: data.enc,
143           encoding: data.con,
144         };
145       } else if (data.con == "aes128gcm") {
146         headers = {
147           encoding: data.con,
148         };
149       }
150       // Ciphertext is (urlsafe) Base 64 encoded.
151       message = ChromeUtils.base64URLDecode(data.message, {
152         // The Push server may append padding.
153         padding: "ignore",
154       });
155     }
157     return { headers, message };
158   },
160   _configure(serverURL, debug) {
161     return EventDispatcher.instance.sendRequestForResult({
162       type: "PushServiceAndroidGCM:Configure",
163       endpoint: serverURL.spec,
164       debug,
165     });
166   },
168   init(options, mainPushService, serverURL) {
169     console.debug("init()");
170     this._mainPushService = mainPushService;
171     this._serverURI = serverURL;
173     prefs.observe("debug", this);
174     Services.obs.addObserver(this, "PushServiceAndroidGCM:ReceivedPushMessage");
176     return this._configure(serverURL, !!prefs.get("debug")).then(() => {
177       EventDispatcher.instance.sendRequestForResult({
178         type: "PushServiceAndroidGCM:Initialized",
179       });
180     });
181   },
183   uninit() {
184     console.debug("uninit()");
185     EventDispatcher.instance.sendRequestForResult({
186       type: "PushServiceAndroidGCM:Uninitialized",
187     });
189     this._mainPushService = null;
190     Services.obs.removeObserver(
191       this,
192       "PushServiceAndroidGCM:ReceivedPushMessage"
193     );
194     prefs.ignore("debug", this);
195   },
197   onAlarmFired() {
198     // No action required.
199   },
201   connect(records, broadcastListeners) {
202     console.debug("connect:", records);
203     // It's possible for the registration or subscriptions backing the
204     // PushService to not be registered with the underlying AndroidPushService.
205     // Expire those that are unrecognized.
206     return EventDispatcher.instance
207       .sendRequestForResult({
208         type: "PushServiceAndroidGCM:DumpSubscriptions",
209       })
210       .then(subscriptions => {
211         subscriptions = JSON.parse(subscriptions);
212         console.debug("connect:", subscriptions);
213         // subscriptions maps chid => subscription data.
214         return Promise.all(
215           records.map(record => {
216             if (subscriptions.hasOwnProperty(record.keyID)) {
217               console.debug("connect:", "hasOwnProperty", record.keyID);
218               return Promise.resolve();
219             }
220             console.debug("connect:", "!hasOwnProperty", record.keyID);
221             // Subscription is known to PushService.jsm but not to AndroidPushService.  Drop it.
222             return this._mainPushService
223               .dropRegistrationAndNotifyApp(record.keyID)
224               .catch(error => {
225                 console.error(
226                   "connect: Error dropping registration",
227                   record.keyID,
228                   error
229                 );
230               });
231           })
232         );
233       });
234   },
236   async sendSubscribeBroadcast(serviceId, version) {
237     // Not implemented yet
238   },
240   isConnected() {
241     return this._mainPushService != null;
242   },
244   disconnect() {
245     console.debug("disconnect");
246   },
248   register(record) {
249     console.debug("register:", record);
250     let ctime = Date.now();
251     let appServerKey = record.appServerKey
252       ? ChromeUtils.base64URLEncode(record.appServerKey, {
253           // The Push server requires padding.
254           pad: true,
255         })
256       : null;
257     let message = {
258       type: "PushServiceAndroidGCM:SubscribeChannel",
259       appServerKey,
260     };
261     if (record.scope == FXA_PUSH_SCOPE) {
262       message.service = "fxa";
263     }
264     // Caller handles errors.
265     return EventDispatcher.instance.sendRequestForResult(message).then(data => {
266       data = JSON.parse(data);
267       console.debug("Got data:", data);
268       return PushCrypto.generateKeys().then(
269         exportedKeys =>
270           new PushRecordAndroidGCM({
271             // Straight from autopush.
272             channelID: data.channelID,
273             pushEndpoint: data.endpoint,
274             // Common to all PushRecord implementations.
275             scope: record.scope,
276             originAttributes: record.originAttributes,
277             ctime,
278             systemRecord: record.systemRecord,
279             // Cryptography!
280             p256dhPublicKey: exportedKeys[0],
281             p256dhPrivateKey: exportedKeys[1],
282             authenticationSecret: PushCrypto.generateAuthenticationSecret(),
283             appServerKey: record.appServerKey,
284           })
285       );
286     });
287   },
289   unregister(record) {
290     console.debug("unregister: ", record);
291     return EventDispatcher.instance.sendRequestForResult({
292       type: "PushServiceAndroidGCM:UnsubscribeChannel",
293       channelID: record.keyID,
294     });
295   },
297   reportDeliveryError(messageID, reason) {
298     console.warn(
299       "reportDeliveryError: Ignoring message delivery error",
300       messageID,
301       reason
302     );
303   },
306 function PushRecordAndroidGCM(record) {
307   PushRecord.call(this, record);
308   this.channelID = record.channelID;
311 PushRecordAndroidGCM.prototype = Object.create(PushRecord.prototype, {
312   keyID: {
313     get() {
314       return this.channelID;
315     },
316   },