summaryrefslogtreecommitdiffstats
path: root/services/fxaccounts/FxAccountsPush.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'services/fxaccounts/FxAccountsPush.sys.mjs')
-rw-r--r--services/fxaccounts/FxAccountsPush.sys.mjs315
1 files changed, 315 insertions, 0 deletions
diff --git a/services/fxaccounts/FxAccountsPush.sys.mjs b/services/fxaccounts/FxAccountsPush.sys.mjs
new file mode 100644
index 0000000000..80806db84a
--- /dev/null
+++ b/services/fxaccounts/FxAccountsPush.sys.mjs
@@ -0,0 +1,315 @@
+/* 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/. */
+
+import { Async } from "resource://services-common/async.sys.mjs";
+
+const {
+ FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
+ ONLOGOUT_NOTIFICATION,
+ ON_ACCOUNT_DESTROYED_NOTIFICATION,
+ ON_COLLECTION_CHANGED_NOTIFICATION,
+ ON_COMMAND_RECEIVED_NOTIFICATION,
+ ON_DEVICE_CONNECTED_NOTIFICATION,
+ ON_DEVICE_DISCONNECTED_NOTIFICATION,
+ ON_PASSWORD_CHANGED_NOTIFICATION,
+ ON_PASSWORD_RESET_NOTIFICATION,
+ ON_PROFILE_CHANGE_NOTIFICATION,
+ ON_PROFILE_UPDATED_NOTIFICATION,
+ ON_VERIFY_LOGIN_NOTIFICATION,
+ log,
+} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
+
+/**
+ * FxAccountsPushService manages Push notifications for Firefox Accounts in the browser
+ *
+ * @param [options]
+ * Object, custom options that used for testing
+ * @constructor
+ */
+export function FxAccountsPushService(options = {}) {
+ this.log = log;
+
+ if (options.log) {
+ // allow custom log for testing purposes
+ this.log = options.log;
+ }
+
+ this.log.debug("FxAccountsPush loading service");
+ this.wrappedJSObject = this;
+ this.initialize(options);
+}
+
+FxAccountsPushService.prototype = {
+ /**
+ * Helps only initialize observers once.
+ */
+ _initialized: false,
+ /**
+ * Instance of the nsIPushService or a mocked object.
+ */
+ pushService: null,
+ /**
+ * Instance of FxAccountsInternal or a mocked object.
+ */
+ fxai: null,
+ /**
+ * Component ID of this service, helps register this component.
+ */
+ classID: Components.ID("{1b7db999-2ecd-4abf-bb95-a726896798ca}"),
+ /**
+ * Register used interfaces in this service
+ */
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+ /**
+ * Initialize the service and register all the required observers.
+ *
+ * @param [options]
+ */
+ initialize(options) {
+ if (this._initialized) {
+ return false;
+ }
+
+ this._initialized = true;
+
+ if (options.pushService) {
+ this.pushService = options.pushService;
+ } else {
+ this.pushService = Cc["@mozilla.org/push/Service;1"].getService(
+ Ci.nsIPushService
+ );
+ }
+
+ if (options.fxai) {
+ this.fxai = options.fxai;
+ } else {
+ const { getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ );
+ const fxAccounts = getFxAccountsSingleton();
+ this.fxai = fxAccounts._internal;
+ }
+
+ this.asyncObserver = Async.asyncObserver(this, this.log);
+ // We use an async observer because a device waking up can
+ // observe multiple "Send Tab received" push notifications at the same time.
+ // The way these notifications are handled is as follows:
+ // Read index from storage, make network request, update the index.
+ // You can imagine what happens when multiple calls race: we load
+ // the same index multiple times and receive the same exact tabs, multiple times.
+ // The async observer will ensure we make these network requests serially.
+ Services.obs.addObserver(this.asyncObserver, this.pushService.pushTopic);
+ Services.obs.addObserver(
+ this.asyncObserver,
+ this.pushService.subscriptionChangeTopic
+ );
+ Services.obs.addObserver(this.asyncObserver, ONLOGOUT_NOTIFICATION);
+
+ this.log.debug("FxAccountsPush initialized");
+ return true;
+ },
+ /**
+ * Registers a new endpoint with the Push Server
+ *
+ * @returns {Promise}
+ * Promise always resolves with a subscription or a null if failed to subscribe.
+ */
+ registerPushEndpoint() {
+ this.log.trace("FxAccountsPush registerPushEndpoint");
+
+ return new Promise(resolve => {
+ this.pushService.subscribe(
+ FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ if (Components.isSuccessCode(result)) {
+ this.log.debug("FxAccountsPush got subscription");
+ resolve(subscription);
+ } else {
+ this.log.warn("FxAccountsPush failed to subscribe", result);
+ resolve(null);
+ }
+ }
+ );
+ });
+ },
+ /**
+ * Async observer interface to listen to push messages, changes and logout.
+ *
+ * @param subject
+ * @param topic
+ * @param data
+ * @returns {Promise}
+ */
+ async observe(subject, topic, data) {
+ try {
+ this.log.trace(
+ `observed topic=${topic}, data=${data}, subject=${subject}`
+ );
+ switch (topic) {
+ case this.pushService.pushTopic:
+ if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
+ let message = subject.QueryInterface(Ci.nsIPushMessage);
+ await this._onPushMessage(message);
+ }
+ break;
+ case this.pushService.subscriptionChangeTopic:
+ if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
+ await this._onPushSubscriptionChange();
+ }
+ break;
+ case ONLOGOUT_NOTIFICATION:
+ // user signed out, we need to stop polling the Push Server
+ await this.unsubscribe();
+ break;
+ }
+ } catch (err) {
+ this.log.error(err);
+ }
+ },
+
+ /**
+ * Fired when the Push server sends a notification.
+ *
+ * @private
+ * @returns {Promise}
+ */
+ async _onPushMessage(message) {
+ this.log.trace("FxAccountsPushService _onPushMessage");
+ if (!message.data) {
+ // Use the empty signal to check the verification state of the account right away
+ this.log.debug("empty push message - checking account status");
+ this.fxai.checkVerificationStatus();
+ return;
+ }
+ let payload = message.data.json();
+ this.log.debug(`push command: ${payload.command}`);
+ switch (payload.command) {
+ case ON_COMMAND_RECEIVED_NOTIFICATION:
+ await this.fxai.commands.pollDeviceCommands(payload.data.index);
+ break;
+ case ON_DEVICE_CONNECTED_NOTIFICATION:
+ Services.obs.notifyObservers(
+ null,
+ ON_DEVICE_CONNECTED_NOTIFICATION,
+ payload.data.deviceName
+ );
+ break;
+ case ON_DEVICE_DISCONNECTED_NOTIFICATION:
+ this.fxai._handleDeviceDisconnection(payload.data.id);
+ return;
+ case ON_PROFILE_UPDATED_NOTIFICATION:
+ // We already have a "profile updated" notification sent via WebChannel,
+ // let's just re-use that.
+ Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION);
+ return;
+ case ON_PASSWORD_CHANGED_NOTIFICATION:
+ case ON_PASSWORD_RESET_NOTIFICATION:
+ this._onPasswordChanged();
+ return;
+ case ON_ACCOUNT_DESTROYED_NOTIFICATION:
+ this.fxai._handleAccountDestroyed(payload.data.uid);
+ return;
+ case ON_COLLECTION_CHANGED_NOTIFICATION:
+ Services.obs.notifyObservers(
+ null,
+ ON_COLLECTION_CHANGED_NOTIFICATION,
+ payload.data.collections
+ );
+ return;
+ case ON_VERIFY_LOGIN_NOTIFICATION:
+ Services.obs.notifyObservers(
+ null,
+ ON_VERIFY_LOGIN_NOTIFICATION,
+ JSON.stringify(payload.data)
+ );
+ break;
+ default:
+ this.log.warn("FxA Push command unrecognized: " + payload.command);
+ }
+ },
+ /**
+ * Check the FxA session status after a password change/reset event.
+ * If the session is invalid, reset credentials and notify listeners of
+ * ON_ACCOUNT_STATE_CHANGE_NOTIFICATION that the account may have changed
+ *
+ * @returns {Promise}
+ * @private
+ */
+ _onPasswordChanged() {
+ return this.fxai.withCurrentAccountState(async state => {
+ return this.fxai.checkAccountStatus(state);
+ });
+ },
+ /**
+ * Fired when the Push server drops a subscription, or the subscription identifier changes.
+ *
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#Receiving_Push_Messages
+ *
+ * @returns {Promise}
+ * @private
+ */
+ _onPushSubscriptionChange() {
+ this.log.trace("FxAccountsPushService _onPushSubscriptionChange");
+ return this.fxai.updateDeviceRegistration();
+ },
+ /**
+ * Unsubscribe from the Push server
+ *
+ * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe()
+ *
+ * @returns {Promise} - The promise resolves with a bool to indicate if we successfully unsubscribed.
+ * The promise never rejects.
+ * @private
+ */
+ unsubscribe() {
+ this.log.trace("FxAccountsPushService unsubscribe");
+ return new Promise(resolve => {
+ this.pushService.unsubscribe(
+ FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, ok) => {
+ if (Components.isSuccessCode(result)) {
+ if (ok === true) {
+ this.log.debug("FxAccountsPushService unsubscribed");
+ } else {
+ this.log.debug(
+ "FxAccountsPushService had no subscription to unsubscribe"
+ );
+ }
+ } else {
+ this.log.warn(
+ "FxAccountsPushService failed to unsubscribe",
+ result
+ );
+ }
+ return resolve(ok);
+ }
+ );
+ });
+ },
+
+ /**
+ * Get our Push server subscription.
+ *
+ * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#getSubscription()
+ *
+ * @returns {Promise} - resolves with the subscription or null. Never rejects.
+ */
+ getSubscription() {
+ return new Promise(resolve => {
+ this.pushService.getSubscription(
+ FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ if (!subscription) {
+ this.log.info("FxAccountsPushService no subscription found");
+ return resolve(null);
+ }
+ return resolve(subscription);
+ }
+ );
+ });
+ },
+};