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/. */
7 const { AppConstants } = ChromeUtils.import(
8 "resource://gre/modules/AppConstants.jsm"
10 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
11 const { clearTimeout, setTimeout } = ChromeUtils.import(
12 "resource://gre/modules/Timer.jsm"
14 const { XPCOMUtils } = ChromeUtils.import(
15 "resource://gre/modules/XPCOMUtils.jsm"
18 var PushServiceWebSocket, PushServiceHttp2;
20 XPCOMUtils.defineLazyServiceGetter(
23 "@mozilla.org/push/Notifier;1",
26 XPCOMUtils.defineLazyServiceGetter(
29 "@mozilla.org/network/effective-tld-service;1",
30 "nsIEffectiveTLDService"
32 ChromeUtils.defineModuleGetter(
34 "pushBroadcastService",
35 "resource://gre/modules/PushBroadcastService.jsm"
37 ChromeUtils.defineModuleGetter(
40 "resource://gre/modules/PushCrypto.jsm"
42 ChromeUtils.defineModuleGetter(
44 "PushServiceAndroidGCM",
45 "resource://gre/modules/PushServiceAndroidGCM.jsm"
48 const CONNECTION_PROTOCOLS = (function() {
49 if ("android" != AppConstants.MOZ_WIDGET_TOOLKIT) {
50 ({ PushServiceWebSocket } = ChromeUtils.import(
51 "resource://gre/modules/PushServiceWebSocket.jsm"
53 ({ PushServiceHttp2 } = ChromeUtils.import(
54 "resource://gre/modules/PushServiceHttp2.jsm"
56 return [PushServiceWebSocket, PushServiceHttp2];
58 return [PushServiceAndroidGCM];
61 const EXPORTED_SYMBOLS = ["PushService"];
63 XPCOMUtils.defineLazyGetter(this, "console", () => {
64 let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
65 return new ConsoleAPI({
66 maxLogLevelPref: "dom.push.loglevel",
67 prefix: "PushService",
71 const prefs = Services.prefs.getBranch("dom.push.");
73 const PUSH_SERVICE_UNINIT = 0;
74 const PUSH_SERVICE_INIT = 1; // No serverURI
75 const PUSH_SERVICE_ACTIVATING = 2; // activating db
76 const PUSH_SERVICE_CONNECTION_DISABLE = 3;
77 const PUSH_SERVICE_ACTIVE_OFFLINE = 4;
78 const PUSH_SERVICE_RUNNING = 5;
81 * State is change only in couple of functions:
82 * init - change state to PUSH_SERVICE_INIT if state was PUSH_SERVICE_UNINIT
83 * changeServerURL - change state to PUSH_SERVICE_ACTIVATING if serverURL
84 * present or PUSH_SERVICE_INIT if not present.
85 * changeStateConnectionEnabledEvent - it is call on pref change or during
86 * the service activation and it can
88 * PUSH_SERVICE_CONNECTION_DISABLE
89 * changeStateOfflineEvent - it is called when offline state changes or during
90 * the service activation and it change state to
91 * PUSH_SERVICE_ACTIVE_OFFLINE or
92 * PUSH_SERVICE_RUNNING.
93 * uninit - change state to PUSH_SERVICE_UNINIT.
96 // This is for starting and stopping service.
97 const STARTING_SERVICE_EVENT = 0;
98 const CHANGING_SERVICE_EVENT = 1;
99 const STOPPING_SERVICE_EVENT = 2;
100 const UNINIT_EVENT = 3;
102 // Returns the backend for the given server URI.
103 function getServiceForServerURI(uri) {
104 // Insecure server URLs are allowed for development and testing.
105 let allowInsecure = prefs.getBoolPref(
106 "testing.allowInsecureServerURL",
109 if (AppConstants.MOZ_WIDGET_TOOLKIT == "android") {
110 if (uri.scheme == "https" || (allowInsecure && uri.scheme == "http")) {
111 return CONNECTION_PROTOCOLS;
115 if (uri.scheme == "wss" || (allowInsecure && uri.scheme == "ws")) {
116 return PushServiceWebSocket;
118 if (uri.scheme == "https" || (allowInsecure && uri.scheme == "http")) {
119 return PushServiceHttp2;
125 * Annotates an error with an XPCOM result code. We use this helper
126 * instead of `Components.Exception` because the latter can assert in
127 * `nsXPCComponents_Exception::HasInstance` when inspected at shutdown.
129 function errorWithResult(message, result = Cr.NS_ERROR_FAILURE) {
130 let error = new Error(message);
131 error.result = result;
136 * The implementation of the push system. It uses WebSockets
137 * (PushServiceWebSocket) to communicate with the server and PushDB (IndexedDB)
142 _state: PUSH_SERVICE_UNINIT,
145 _visibleNotifications: new Map(),
147 // Callback that is called after attempting to
148 // reduce the quota for a record. Used for testing purposes.
149 _updateQuotaTestCallback: null,
151 // Set of timeout ID of tasks to reduce quota.
152 _updateQuotaTimeouts: new Set(),
154 // When serverURI changes (this is used for testing), db is cleaned up and a
155 // a new db is started. This events must be sequential.
156 _stateChangeProcessQueue: null,
157 _stateChangeProcessEnqueue(op) {
158 if (!this._stateChangeProcessQueue) {
159 this._stateChangeProcessQueue = Promise.resolve();
162 this._stateChangeProcessQueue = this._stateChangeProcessQueue
166 "stateChangeProcessEnqueue: Error transitioning state",
169 return this._shutdownService();
173 "stateChangeProcessEnqueue: Error shutting down service",
177 return this._stateChangeProcessQueue;
180 // Pending request. If a worker try to register for the same scope again, do
181 // not send a new registration request. Therefore we need queue of pending
182 // register requests. This is the list of scopes which pending registration.
183 _pendingRegisterRequest: {},
184 _notifyActivated: null,
187 if (this._state < PUSH_SERVICE_ACTIVATING) {
188 return Promise.reject(new Error("Push service not active"));
190 if (this._state > PUSH_SERVICE_ACTIVATING) {
191 return Promise.resolve();
193 if (!this._activated) {
194 this._activated = new Promise((resolve, reject) => {
195 this._notifyActivated = { resolve, reject };
198 return this._activated;
201 _makePendingKey(aPageRecord) {
202 return aPageRecord.scope + "|" + aPageRecord.originAttributes;
205 _lookupOrPutPendingRequest(aPageRecord) {
206 let key = this._makePendingKey(aPageRecord);
207 if (this._pendingRegisterRequest[key]) {
208 return this._pendingRegisterRequest[key];
211 return (this._pendingRegisterRequest[key] = this._registerWithServer(
216 _deletePendingRequest(aPageRecord) {
217 let key = this._makePendingKey(aPageRecord);
218 if (this._pendingRegisterRequest[key]) {
219 delete this._pendingRegisterRequest[key];
223 _setState(aNewState) {
232 if (this._state == aNewState) {
236 if (this._state == PUSH_SERVICE_ACTIVATING) {
237 // It is not important what is the new state as soon as we leave
238 // PUSH_SERVICE_ACTIVATING
239 if (this._notifyActivated) {
240 if (aNewState < PUSH_SERVICE_ACTIVATING) {
241 this._notifyActivated.reject(new Error("Push service not active"));
243 this._notifyActivated.resolve();
246 this._notifyActivated = null;
247 this._activated = null;
249 this._state = aNewState;
252 async _changeStateOfflineEvent(offline, calledFromConnEnabledEvent) {
253 console.debug("changeStateOfflineEvent()", offline);
256 this._state < PUSH_SERVICE_ACTIVE_OFFLINE &&
257 this._state != PUSH_SERVICE_ACTIVATING &&
258 !calledFromConnEnabledEvent
264 if (this._state == PUSH_SERVICE_RUNNING) {
265 this._service.disconnect();
267 this._setState(PUSH_SERVICE_ACTIVE_OFFLINE);
271 if (this._state == PUSH_SERVICE_RUNNING) {
272 // PushService was not in the offline state, but got notification to
273 // go online (a offline notification has not been sent).
275 this._service.disconnect();
278 let broadcastListeners = await pushBroadcastService.getListeners();
280 // In principle, a listener could be added to the
281 // pushBroadcastService here, after we have gotten listeners and
282 // before we're RUNNING, but this can't happen in practice because
283 // the only caller that can add listeners is PushBroadcastService,
284 // and it waits on the same promise we are before it can add
285 // listeners. If PushBroadcastService gets woken first, it will
286 // update the value that is eventually returned from
288 this._setState(PUSH_SERVICE_RUNNING);
290 this._service.connect(broadcastListeners);
293 _changeStateConnectionEnabledEvent(enabled) {
294 console.debug("changeStateConnectionEnabledEvent()", enabled);
297 this._state < PUSH_SERVICE_CONNECTION_DISABLE &&
298 this._state != PUSH_SERVICE_ACTIVATING
300 return Promise.resolve();
304 return this._changeStateOfflineEvent(Services.io.offline, true);
307 if (this._state == PUSH_SERVICE_RUNNING) {
308 this._service.disconnect();
310 this._setState(PUSH_SERVICE_CONNECTION_DISABLE);
311 return Promise.resolve();
315 changeTestServer(url, options = {}) {
316 console.debug("changeTestServer()");
318 return this._stateChangeProcessEnqueue(_ => {
319 if (this._state < PUSH_SERVICE_ACTIVATING) {
320 console.debug("changeTestServer: PushService not activated?");
321 return Promise.resolve();
324 return this._changeServerURL(url, CHANGING_SERVICE_EVENT, options);
328 observe: function observe(aSubject, aTopic, aData) {
331 * We need to call uninit() on shutdown to clean up things that modules
332 * aren't very good at automatically cleaning up, so we don't get shutdown
333 * leaks on browser shutdown.
335 case "quit-application":
338 case "network:offline-status-changed":
339 this._stateChangeProcessEnqueue(_ =>
340 this._changeStateOfflineEvent(aData === "offline", false)
344 case "nsPref:changed":
345 if (aData == "serverURL") {
347 "observe: dom.push.serverURL changed for websocket",
348 prefs.getStringPref("serverURL")
350 this._stateChangeProcessEnqueue(_ =>
351 this._changeServerURL(
352 prefs.getStringPref("serverURL"),
353 CHANGING_SERVICE_EVENT
356 } else if (aData == "connection.enabled") {
357 this._stateChangeProcessEnqueue(_ =>
358 this._changeStateConnectionEnabledEvent(
359 prefs.getBoolPref("connection.enabled")
366 this._dropExpiredRegistrations().catch(error => {
367 console.error("Failed to drop expired registrations on idle", error);
372 this._onPermissionChange(aSubject, aData).catch(error => {
374 "onPermissionChange: Error updating registrations:",
380 case "clear-origin-attributes-data":
381 this._clearOriginData(aData).catch(error => {
382 console.error("clearOriginData: Error clearing origin data:", error);
388 _clearOriginData(data) {
389 console.log("clearOriginData()");
392 return Promise.resolve();
395 let pattern = JSON.parse(data);
396 return this._dropRegistrationsIf(record =>
397 record.matchesOriginAttributes(pattern)
402 * Sends an unregister request to the server in the background. If the
403 * service is not connected, this function is a no-op.
405 * @param {PushRecord} record The record to unregister.
406 * @param {Number} reason An `nsIPushErrorReporter` unsubscribe reason,
407 * indicating why this record was removed.
409 _backgroundUnregister(record, reason) {
410 console.debug("backgroundUnregister()");
412 if (!this._service.isConnected() || !record) {
416 console.debug("backgroundUnregister: Notifying server", record);
417 this._sendUnregister(record, reason)
419 gPushNotifier.notifySubscriptionModified(
425 console.error("backgroundUnregister: Error notifying server", e);
429 _findService(serverURL) {
430 console.debug("findService()");
433 console.warn("findService: No dom.push.serverURL found");
439 uri = Services.io.newURI(serverURL);
442 "findService: Error creating valid URI from",
443 "dom.push.serverURL",
449 let service = getServiceForServerURI(uri);
450 return [service, uri];
453 _changeServerURL(serverURI, event, options = {}) {
454 console.debug("changeServerURL()");
458 return this._stopService(event);
460 case STARTING_SERVICE_EVENT: {
461 let [service, uri] = this._findService(serverURI);
463 this._setState(PUSH_SERVICE_INIT);
464 return Promise.resolve();
466 return this._startService(service, uri, options).then(_ =>
467 this._changeStateConnectionEnabledEvent(
468 prefs.getBoolPref("connection.enabled")
472 case CHANGING_SERVICE_EVENT:
473 let [service, uri] = this._findService(serverURI);
475 if (this._state == PUSH_SERVICE_INIT) {
476 this._setState(PUSH_SERVICE_ACTIVATING);
477 // The service has not been running - start it.
478 return this._startService(service, uri, options).then(_ =>
479 this._changeStateConnectionEnabledEvent(
480 prefs.getBoolPref("connection.enabled")
484 this._setState(PUSH_SERVICE_ACTIVATING);
485 // If we already had running service - stop service, start the new
486 // one and check connection.enabled and offline state(offline state
487 // check is called in changeStateConnectionEnabledEvent function)
488 return this._stopService(CHANGING_SERVICE_EVENT)
489 .then(_ => this._startService(service, uri, options))
491 this._changeStateConnectionEnabledEvent(
492 prefs.getBoolPref("connection.enabled")
496 if (this._state == PUSH_SERVICE_INIT) {
497 return Promise.resolve();
499 // The new serverUri is empty or misconfigured - stop service.
500 this._setState(PUSH_SERVICE_INIT);
501 return this._stopService(STOPPING_SERVICE_EVENT);
504 console.error("Unexpected event in _changeServerURL", event);
505 return Promise.reject(new Error(`Unexpected event ${event}`));
510 * PushService initialization is divided into 4 parts:
511 * init() - start listening for quit-application and serverURL changes.
512 * state is change to PUSH_SERVICE_INIT
513 * startService() - if serverURL is present this function is called. It starts
514 * listening for broadcasted messages, starts db and
515 * PushService connection (WebSocket).
516 * state is change to PUSH_SERVICE_ACTIVATING.
517 * startObservers() - start other observers.
518 * changeStateConnectionEnabledEvent - checks prefs and offline state.
519 * It changes state to:
520 * PUSH_SERVICE_RUNNING,
521 * PUSH_SERVICE_ACTIVE_OFFLINE or
522 * PUSH_SERVICE_CONNECTION_DISABLE.
524 async init(options = {}) {
525 console.debug("init()");
527 if (this._state > PUSH_SERVICE_UNINIT) {
531 this._setState(PUSH_SERVICE_ACTIVATING);
533 prefs.addObserver("serverURL", this);
534 Services.obs.addObserver(this, "quit-application");
536 if (options.serverURI) {
537 // this is use for xpcshell test.
539 await this._stateChangeProcessEnqueue(_ =>
540 this._changeServerURL(
542 STARTING_SERVICE_EVENT,
547 // This is only used for testing. Different tests require connecting to
548 // slightly different URLs.
549 await this._stateChangeProcessEnqueue(_ =>
550 this._changeServerURL(
551 prefs.getStringPref("serverURL"),
552 STARTING_SERVICE_EVENT
559 console.debug("startObservers()");
561 if (this._state != PUSH_SERVICE_ACTIVATING) {
565 Services.obs.addObserver(this, "clear-origin-attributes-data");
567 // The offline-status-changed event is used to know
568 // when to (dis)connect. It may not fire if the underlying OS changes
569 // networks; in such a case we rely on timeout.
570 Services.obs.addObserver(this, "network:offline-status-changed");
572 // Used to monitor if the user wishes to disable Push.
573 prefs.addObserver("connection.enabled", this);
575 // Prunes expired registrations and notifies dormant service workers.
576 Services.obs.addObserver(this, "idle-daily");
578 // Prunes registrations for sites for which the user revokes push
580 Services.obs.addObserver(this, "perm-changed");
583 _startService(service, serverURI, options) {
584 console.debug("startService()");
586 if (this._state != PUSH_SERVICE_ACTIVATING) {
587 return Promise.reject();
590 this._service = service;
592 this._db = options.db;
594 this._db = this._service.newPushDB();
597 return this._service.init(options, this, serverURI).then(() => {
598 this._startObservers();
599 return this._dropExpiredRegistrations();
604 * PushService uninitialization is divided into 3 parts:
605 * stopObservers() - stot observers started in startObservers.
606 * stopService() - It stops listening for broadcasted messages, stops db and
607 * PushService connection (WebSocket).
608 * state is changed to PUSH_SERVICE_INIT.
609 * uninit() - stop listening for quit-application and serverURL changes.
610 * state is change to PUSH_SERVICE_UNINIT
612 _stopService(event) {
613 console.debug("stopService()");
615 if (this._state < PUSH_SERVICE_ACTIVATING) {
616 return Promise.resolve();
619 this._stopObservers();
621 this._service.disconnect();
622 this._service.uninit();
623 this._service = null;
625 this._updateQuotaTimeouts.forEach(timeoutID => clearTimeout(timeoutID));
626 this._updateQuotaTimeouts.clear();
629 return Promise.resolve();
631 if (event == UNINIT_EVENT) {
632 // If it is uninitialized just close db.
635 return Promise.resolve();
638 return this.dropUnexpiredRegistrations().then(
651 console.debug("stopObservers()");
653 if (this._state < PUSH_SERVICE_ACTIVATING) {
657 prefs.removeObserver("connection.enabled", this);
659 Services.obs.removeObserver(this, "network:offline-status-changed");
660 Services.obs.removeObserver(this, "clear-origin-attributes-data");
661 Services.obs.removeObserver(this, "idle-daily");
662 Services.obs.removeObserver(this, "perm-changed");
666 let promiseChangeURL = this._changeServerURL("", UNINIT_EVENT);
667 this._setState(PUSH_SERVICE_UNINIT);
668 console.debug("shutdownService: shutdown complete!");
669 return promiseChangeURL;
673 console.debug("uninit()");
675 if (this._state == PUSH_SERVICE_UNINIT) {
679 prefs.removeObserver("serverURL", this);
680 Services.obs.removeObserver(this, "quit-application");
682 await this._stateChangeProcessEnqueue(_ => this._shutdownService());
686 * Drops all active registrations and notifies the associated service
687 * workers. This function is called when the user switches Push servers,
688 * or when the server invalidates all existing registrations.
690 * We ignore expired registrations because they're already handled in other
691 * code paths. Registrations that expired after exceeding their quotas are
692 * evicted at startup, or on the next `idle-daily` event. Registrations that
693 * expired because the user revoked the notification permission are evicted
694 * once the permission is reinstated.
696 dropUnexpiredRegistrations() {
697 return this._db.clearIf(record => {
698 if (record.isExpired()) {
701 this._notifySubscriptionChangeObservers(record);
706 _notifySubscriptionChangeObservers(record) {
710 gPushNotifier.notifySubscriptionChange(record.scope, record.principal);
714 * Drops a registration and notifies the associated service worker. If the
715 * registration does not exist, this function is a no-op.
717 * @param {String} keyID The registration ID to remove.
718 * @returns {Promise} Resolves once the worker has been notified.
720 dropRegistrationAndNotifyApp(aKeyID) {
723 .then(record => this._notifySubscriptionChangeObservers(record));
727 * Replaces an existing registration and notifies the associated service
730 * @param {String} aOldKey The registration ID to replace.
731 * @param {PushRecord} aNewRecord The new record.
732 * @returns {Promise} Resolves once the worker has been notified.
734 updateRegistrationAndNotifyApp(aOldKey, aNewRecord) {
735 return this.updateRecordAndNotifyApp(aOldKey, _ => aNewRecord);
738 * Updates a registration and notifies the associated service worker.
740 * @param {String} keyID The registration ID to update.
741 * @param {Function} updateFunc Returns the updated record.
742 * @returns {Promise} Resolves with the updated record once the worker
745 updateRecordAndNotifyApp(aKeyID, aUpdateFunc) {
746 return this._db.update(aKeyID, aUpdateFunc).then(record => {
747 this._notifySubscriptionChangeObservers(record);
752 ensureCrypto(record) {
754 record.hasAuthenticationSecret() &&
755 record.p256dhPublicKey &&
756 record.p256dhPrivateKey
758 return Promise.resolve(record);
761 let keygen = Promise.resolve([]);
762 if (!record.p256dhPublicKey || !record.p256dhPrivateKey) {
763 keygen = PushCrypto.generateKeys();
765 // We do not have a encryption key. so we need to generate it. This
766 // is only going to happen on db upgrade from version 4 to higher.
768 ([pubKey, privKey]) => {
769 return this.updateRecordAndNotifyApp(record.keyID, record => {
770 if (!record.p256dhPublicKey || !record.p256dhPrivateKey) {
771 record.p256dhPublicKey = pubKey;
772 record.p256dhPrivateKey = privKey;
774 if (!record.hasAuthenticationSecret()) {
775 record.authenticationSecret = PushCrypto.generateAuthenticationSecret();
781 return this.dropRegistrationAndNotifyApp(record.keyID).then(() =>
782 Promise.reject(error)
789 * Dispatches an incoming message to a service worker, recalculating the
790 * quota for the associated push registration. If the quota is exceeded,
791 * the registration and message will be dropped, and the worker will not
794 * @param {String} keyID The push registration ID.
795 * @param {String} messageID The message ID, used to report service worker
796 * delivery failures. For Web Push messages, this is the version. If empty,
797 * failures will not be reported.
798 * @param {Object} headers The encryption headers.
799 * @param {ArrayBuffer|Uint8Array} data The encrypted message data.
800 * @param {Function} updateFunc A function that receives the existing
801 * registration record as its argument, and returns a new record. If the
802 * function returns `null` or `undefined`, the record will not be updated.
803 * `PushServiceWebSocket` uses this to drop incoming updates with older
805 * @returns {Promise} Resolves with an `nsIPushErrorReporter` ack status
806 * code, indicating whether the message was delivered successfully.
808 receivedPushMessage(keyID, messageID, headers, data, updateFunc) {
809 console.debug("receivedPushMessage()");
811 return this._updateRecordAfterPush(keyID, updateFunc)
813 if (record.quotaApplies()) {
814 // Update quota after the delay, at which point
815 // we check for visible notifications.
816 let timeoutID = setTimeout(_ => {
817 this._updateQuota(keyID);
818 if (!this._updateQuotaTimeouts.delete(timeoutID)) {
820 "receivedPushMessage: quota update timeout missing?"
823 }, prefs.getIntPref("quotaUpdateDelay"));
824 this._updateQuotaTimeouts.add(timeoutID);
826 return this._decryptAndNotifyApp(record, messageID, headers, data);
829 console.error("receivedPushMessage: Error notifying app", error);
830 return Ci.nsIPushErrorReporter.ACK_NOT_DELIVERED;
835 * Dispatches a broadcast notification to the BroadcastService.
837 * @param {Object} message The reply received by PushServiceWebSocket
838 * @param {Object} context Additional information about the context in which the
839 * notification was received.
841 receivedBroadcastMessage(message, context) {
843 .receivedBroadcastMessage(message.broadcasts, context)
850 * Updates a registration record after receiving a push message.
852 * @param {String} keyID The push registration ID.
853 * @param {Function} updateFunc The function passed to `receivedPushMessage`.
854 * @returns {Promise} Resolves with the updated record, or rejects if the
855 * record was not updated.
857 _updateRecordAfterPush(keyID, updateFunc) {
858 return this.getByKeyID(keyID)
861 throw new Error("No record for key ID " + keyID);
866 // As a special case, don't notify the service worker if the user
867 // cleared their history.
868 if (!isFinite(lastVisit)) {
869 throw new Error("Ignoring message sent to unvisited origin");
874 // Update the record, resetting the quota if the user has visited the
875 // site since the last push.
876 return this._db.update(keyID, record => {
877 let newRecord = updateFunc(record);
881 // Because `unregister` is advisory only, we can still receive messages
882 // for stale Simple Push registrations from the server. To work around
883 // this, we check if the record has expired before *and* after updating
885 if (newRecord.isExpired()) {
888 newRecord.receivedPush(lastVisit);
894 gPushNotifier.notifySubscriptionModified(
903 * Decrypts an incoming message and notifies the associated service worker.
905 * @param {PushRecord} record The receiving registration.
906 * @param {String} messageID The message ID.
907 * @param {Object} headers The encryption headers.
908 * @param {ArrayBuffer|Uint8Array} data The encrypted message data.
909 * @returns {Promise} Resolves with an ack status code.
911 _decryptAndNotifyApp(record, messageID, headers, data) {
912 return PushCrypto.decrypt(
913 record.p256dhPrivateKey,
914 record.p256dhPublicKey,
915 record.authenticationSecret,
919 message => this._notifyApp(record, messageID, message),
922 "decryptAndNotifyApp: Error decrypting message",
928 let message = error.format(record.scope);
929 gPushNotifier.notifyError(
933 Ci.nsIScriptError.errorFlag
935 return Ci.nsIPushErrorReporter.ACK_DECRYPTION_ERROR;
940 _updateQuota(keyID) {
941 console.debug("updateQuota()");
944 .update(keyID, record => {
945 // Record may have expired from an earlier quota update.
946 if (record.isExpired()) {
948 "updateQuota: Trying to update quota for expired record",
953 // If there are visible notifications, don't apply the quota penalty
955 if (record.uri && !this._visibleNotifications.has(record.uri.prePath)) {
956 record.reduceQuota();
961 if (record.isExpired()) {
962 // Drop the registration in the background. If the user returns to the
963 // site, the service worker will be notified on the next `idle-daily`
965 this._backgroundUnregister(
967 Ci.nsIPushErrorReporter.UNSUBSCRIBE_QUOTA_EXCEEDED
970 gPushNotifier.notifySubscriptionModified(
975 if (this._updateQuotaTestCallback) {
976 // Callback so that test may be notified when the quota update is complete.
977 this._updateQuotaTestCallback();
981 console.debug("updateQuota: Error while trying to update quota", error);
985 notificationForOriginShown(origin) {
986 console.debug("notificationForOriginShown()", origin);
988 if (this._visibleNotifications.has(origin)) {
989 count = this._visibleNotifications.get(origin);
993 this._visibleNotifications.set(origin, count + 1);
996 notificationForOriginClosed(origin) {
997 console.debug("notificationForOriginClosed()", origin);
999 if (this._visibleNotifications.has(origin)) {
1000 count = this._visibleNotifications.get(origin);
1003 "notificationForOriginClosed: closing notification that has not been shown?"
1008 this._visibleNotifications.set(origin, count - 1);
1010 this._visibleNotifications.delete(origin);
1014 reportDeliveryError(messageID, reason) {
1015 console.debug("reportDeliveryError()", messageID, reason);
1016 if (this._state == PUSH_SERVICE_RUNNING && this._service.isConnected()) {
1017 // Only report errors if we're initialized and connected.
1018 this._service.reportDeliveryError(messageID, reason);
1022 _notifyApp(aPushRecord, messageID, message) {
1025 !aPushRecord.scope ||
1026 aPushRecord.originAttributes === undefined
1028 console.error("notifyApp: Invalid record", aPushRecord);
1029 return Ci.nsIPushErrorReporter.ACK_NOT_DELIVERED;
1032 console.debug("notifyApp()", aPushRecord.scope);
1034 // If permission has been revoked, trash the message.
1035 if (!aPushRecord.hasPermission()) {
1036 console.warn("notifyApp: Missing push permission", aPushRecord);
1037 return Ci.nsIPushErrorReporter.ACK_NOT_DELIVERED;
1040 let payload = ArrayBuffer.isView(message)
1041 ? new Uint8Array(message.buffer)
1044 if (aPushRecord.quotaApplies()) {
1045 // Don't record telemetry for chrome push messages.
1046 Services.telemetry.getHistogramById("PUSH_API_NOTIFY").add();
1050 gPushNotifier.notifyPushWithData(
1052 aPushRecord.principal,
1057 gPushNotifier.notifyPush(
1059 aPushRecord.principal,
1064 return Ci.nsIPushErrorReporter.ACK_DELIVERED;
1067 getByKeyID(aKeyID) {
1068 return this._db.getByKeyID(aKeyID);
1072 return this._db.getAllUnexpired();
1075 _sendRequest(action, ...params) {
1076 if (this._state == PUSH_SERVICE_CONNECTION_DISABLE) {
1077 return Promise.reject(new Error("Push service disabled"));
1079 if (this._state == PUSH_SERVICE_ACTIVE_OFFLINE) {
1080 return Promise.reject(new Error("Push service offline"));
1082 // Ensure the backend is ready. `getByPageRecord` already checks this, but
1083 // we need to check again here in case the service was restarted in the
1085 return this._checkActivated().then(_ => {
1088 return this._service.register(...params);
1090 return this._service.unregister(...params);
1092 return Promise.reject(new Error("Unknown request type: " + action));
1097 * Called on message from the child process. aPageRecord is an object sent by
1098 * the push manager, identifying the sending page and other fields.
1100 _registerWithServer(aPageRecord) {
1101 console.debug("registerWithServer()", aPageRecord);
1103 return this._sendRequest("register", aPageRecord)
1105 record => this._onRegisterSuccess(record),
1106 err => this._onRegisterError(err)
1110 this._deletePendingRequest(aPageRecord);
1111 gPushNotifier.notifySubscriptionModified(
1115 return record.toSubscription();
1118 this._deletePendingRequest(aPageRecord);
1124 _sendUnregister(aRecord, aReason) {
1125 return this._sendRequest("unregister", aRecord, aReason);
1129 * Exceptions thrown in _onRegisterSuccess are caught by the promise obtained
1130 * from _service.request, causing the promise to be rejected instead.
1132 _onRegisterSuccess(aRecord) {
1133 console.debug("_onRegisterSuccess()");
1135 return this._db.put(aRecord).catch(error => {
1136 // Unable to save. Destroy the subscription in the background.
1137 this._backgroundUnregister(
1139 Ci.nsIPushErrorReporter.UNSUBSCRIBE_MANUAL
1146 * Exceptions thrown in _onRegisterError are caught by the promise obtained
1147 * from _service.request, causing the promise to be rejected instead.
1149 _onRegisterError(reply) {
1150 console.debug("_onRegisterError()");
1154 "onRegisterError: Called without valid error message!",
1157 throw new Error("Registration error");
1162 notificationsCleared() {
1163 this._visibleNotifications.clear();
1166 _getByPageRecord(pageRecord) {
1167 return this._checkActivated().then(_ =>
1168 this._db.getByIdentifiers(pageRecord)
1172 register(aPageRecord) {
1173 console.debug("register()", aPageRecord);
1176 if (aPageRecord.appServerKey && aPageRecord.appServerKey.length != 0) {
1177 let keyView = new Uint8Array(aPageRecord.appServerKey);
1178 keyPromise = PushCrypto.validateAppServerKey(keyView).catch(error => {
1179 // Normalize Web Crypto exceptions. `nsIPushService` will forward the
1180 // error result to the DOM API implementation in `PushManager.cpp` or
1181 // `Push.js`, which will convert it to the correct `DOMException`.
1182 throw errorWithResult(
1183 "Invalid app server key",
1184 Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR
1188 keyPromise = Promise.resolve(null);
1191 return Promise.all([keyPromise, this._getByPageRecord(aPageRecord)]).then(
1192 ([appServerKey, record]) => {
1193 aPageRecord.appServerKey = appServerKey;
1195 return this._lookupOrPutPendingRequest(aPageRecord);
1197 if (!record.matchesAppServerKey(appServerKey)) {
1198 throw errorWithResult(
1199 "Mismatched app server key",
1200 Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR
1203 if (record.isExpired()) {
1206 .then(isChanged => {
1208 // If the user revisited the site, drop the expired push
1209 // registration and re-register.
1210 return this.dropRegistrationAndNotifyApp(record.keyID);
1212 throw new Error("Push subscription expired");
1214 .then(_ => this._lookupOrPutPendingRequest(aPageRecord));
1216 return record.toSubscription();
1222 * Called only by the PushBroadcastService on the receipt of a new
1223 * subscription. Don't call this directly. Go through PushBroadcastService.
1225 async subscribeBroadcast(broadcastId, version) {
1226 if (this._state != PUSH_SERVICE_RUNNING) {
1227 // Ignore any request to subscribe before we send a hello.
1228 // We'll send all the broadcast listeners as part of the hello
1233 await this._service.sendSubscribeBroadcast(broadcastId, version);
1237 * Called on message from the child process.
1239 * Why is the record being deleted from the local database before the server
1242 * Unregistration is for the benefit of the app and the AppServer
1243 * so that the AppServer does not keep pinging a channel the UserAgent isn't
1244 * watching The important part of the transaction in this case is left to the
1245 * app, to tell its server of the unregistration. Even if the request to the
1246 * PushServer were to fail, it would not affect correctness of the protocol,
1247 * and the server GC would just clean up the channelID/subscription
1248 * eventually. Since the appserver doesn't ping it, no data is lost.
1250 * If rather we were to unregister at the server and update the database only
1251 * on success: If the server receives the unregister, and deletes the
1252 * channelID/subscription, but the response is lost because of network
1253 * failure, the application is never informed. In addition the application may
1254 * retry the unregister when it fails due to timeout (websocket) or any other
1255 * reason at which point the server will say it does not know of this
1256 * unregistration. We'll have to make the registration/unregistration phases
1257 * have retries and attempts to resend messages from the server, and have the
1258 * client acknowledge. On a server, data is cheap, reliable notification is
1261 unregister(aPageRecord) {
1262 console.debug("unregister()", aPageRecord);
1264 return this._getByPageRecord(aPageRecord).then(record => {
1265 if (record === null) {
1269 let reason = Ci.nsIPushErrorReporter.UNSUBSCRIBE_MANUAL;
1270 return Promise.all([
1271 this._sendUnregister(record, reason),
1272 this._db.delete(record.keyID).then(rec => {
1274 gPushNotifier.notifySubscriptionModified(rec.scope, rec.principal);
1277 ]).then(([success]) => success);
1282 return this._checkActivated()
1284 return this._dropRegistrationsIf(
1286 info.domain == "*" ||
1288 eTLDService.hasRootDomain(record.uri.prePath, info.domain))
1293 "clear: Error dropping subscriptions for domain",
1297 return Promise.resolve();
1301 registration(aPageRecord) {
1302 console.debug("registration()");
1304 return this._getByPageRecord(aPageRecord).then(record => {
1308 if (record.isExpired()) {
1309 return record.quotaChanged().then(isChanged => {
1311 return this.dropRegistrationAndNotifyApp(record.keyID).then(
1318 return record.toSubscription();
1322 _dropExpiredRegistrations() {
1323 console.debug("dropExpiredRegistrations()");
1325 return this._db.getAllExpired().then(records => {
1327 records.map(record =>
1330 .then(isChanged => {
1332 // If the user revisited the site, drop the expired push
1333 // registration and notify the associated service worker.
1334 this.dropRegistrationAndNotifyApp(record.keyID);
1339 "dropExpiredRegistrations: Error dropping registration",
1349 _onPermissionChange(subject, data) {
1350 console.debug("onPermissionChange()");
1352 if (data == "cleared") {
1353 return this._clearPermissions();
1356 let permission = subject.QueryInterface(Ci.nsIPermission);
1357 if (permission.type != "desktop-notification") {
1358 return Promise.resolve();
1361 return this._updatePermission(permission, data);
1364 _clearPermissions() {
1365 console.debug("clearPermissions()");
1367 return this._db.clearIf(record => {
1368 if (!record.quotaApplies()) {
1369 // Only drop registrations that are subject to quota.
1372 this._backgroundUnregister(
1374 Ci.nsIPushErrorReporter.UNSUBSCRIBE_PERMISSION_REVOKED
1380 _updatePermission(permission, type) {
1381 console.debug("updatePermission()");
1383 let isAllow = permission.capability == Ci.nsIPermissionManager.ALLOW_ACTION;
1384 let isChange = type == "added" || type == "changed";
1386 if (isAllow && isChange) {
1387 // Permission set to "allow". Drop all expired registrations for this
1388 // site, notify the associated service workers, and reset the quota
1389 // for active registrations.
1390 return this._forEachPrincipal(permission.principal, (record, cursor) =>
1391 this._permissionAllowed(record, cursor)
1393 } else if (isChange || (isAllow && type == "deleted")) {
1394 // Permission set to "block" or "always ask," or "allow" permission
1395 // removed. Expire all registrations for this site.
1396 return this._forEachPrincipal(permission.principal, (record, cursor) =>
1397 this._permissionDenied(record, cursor)
1401 return Promise.resolve();
1404 _forEachPrincipal(principal, callback) {
1405 return this._db.forEachOrigin(
1406 principal.URI.prePath,
1407 ChromeUtils.originAttributesToSuffix(principal.originAttributes),
1413 * The update function called for each registration record if the push
1414 * permission is revoked. We only expire the record so we can notify the
1415 * service worker as soon as the permission is reinstated. If we just
1416 * deleted the record, the worker wouldn't be notified until the next visit
1419 * @param {PushRecord} record The record to expire.
1420 * @param {IDBCursor} cursor The IndexedDB cursor.
1422 _permissionDenied(record, cursor) {
1423 console.debug("permissionDenied()");
1425 if (!record.quotaApplies() || record.isExpired()) {
1426 // Ignore already-expired records.
1429 // Drop the registration in the background.
1430 this._backgroundUnregister(
1432 Ci.nsIPushErrorReporter.UNSUBSCRIBE_PERMISSION_REVOKED
1435 cursor.update(record);
1439 * The update function called for each registration record if the push
1440 * permission is granted. If the record has expired, it will be dropped;
1441 * otherwise, its quota will be reset to the default value.
1443 * @param {PushRecord} record The record to update.
1444 * @param {IDBCursor} cursor The IndexedDB cursor.
1446 _permissionAllowed(record, cursor) {
1447 console.debug("permissionAllowed()");
1449 if (!record.quotaApplies()) {
1452 if (record.isExpired()) {
1453 // If the registration has expired, drop and notify the worker
1455 this._notifySubscriptionChangeObservers(record);
1459 record.resetQuota();
1460 cursor.update(record);
1464 * Drops all matching registrations from the database. Notifies the
1465 * associated service workers if permission is granted, and removes
1466 * unexpired registrations from the server.
1468 * @param {Function} predicate A function called for each record.
1469 * @returns {Promise} Resolves once the registrations have been dropped.
1471 _dropRegistrationsIf(predicate) {
1472 return this._db.clearIf(record => {
1473 if (!predicate(record)) {
1476 if (record.hasPermission()) {
1477 // "Clear Recent History" and the Forget button remove permissions
1478 // before clearing registrations, but it's possible for the worker to
1479 // resubscribe if the "dom.push.testing.ignorePermission" pref is set.
1480 this._notifySubscriptionChangeObservers(record);
1482 if (!record.isExpired()) {
1483 // Only unregister active registrations, since we already told the
1484 // server about expired ones.
1485 this._backgroundUnregister(
1487 Ci.nsIPushErrorReporter.UNSUBSCRIBE_MANUAL