tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

FxAccountsPush.sys.mjs (10009B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { Async } from "resource://services-common/async.sys.mjs";
      6 
      7 import {
      8  FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
      9  ONLOGOUT_NOTIFICATION,
     10  ON_ACCOUNT_DESTROYED_NOTIFICATION,
     11  ON_COLLECTION_CHANGED_NOTIFICATION,
     12  ON_COMMAND_RECEIVED_NOTIFICATION,
     13  ON_DEVICE_CONNECTED_NOTIFICATION,
     14  ON_DEVICE_DISCONNECTED_NOTIFICATION,
     15  ON_PASSWORD_CHANGED_NOTIFICATION,
     16  ON_PASSWORD_RESET_NOTIFICATION,
     17  ON_PROFILE_CHANGE_NOTIFICATION,
     18  ON_PROFILE_UPDATED_NOTIFICATION,
     19  ON_VERIFY_LOGIN_NOTIFICATION,
     20  log,
     21 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
     22 
     23 /**
     24 * FxAccountsPushService manages Push notifications for Firefox Accounts in the browser
     25 *
     26 * @param [options]
     27 *        Object, custom options that used for testing
     28 * @class
     29 */
     30 export function FxAccountsPushService(options = {}) {
     31  this.log = log;
     32 
     33  if (options.log) {
     34    // allow custom log for testing purposes
     35    this.log = options.log;
     36  }
     37 
     38  this.log.debug("FxAccountsPush loading service");
     39  this.wrappedJSObject = this;
     40  this.initialize(options);
     41 }
     42 
     43 FxAccountsPushService.prototype = {
     44  /**
     45   * Helps only initialize observers once.
     46   */
     47  _initialized: false,
     48  /**
     49   * Instance of the nsIPushService or a mocked object.
     50   */
     51  pushService: null,
     52  /**
     53   * Instance of FxAccountsInternal or a mocked object.
     54   */
     55  fxai: null,
     56  /**
     57   * Component ID of this service, helps register this component.
     58   */
     59  classID: Components.ID("{1b7db999-2ecd-4abf-bb95-a726896798ca}"),
     60  /**
     61   * Register used interfaces in this service
     62   */
     63  QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
     64  /**
     65   * Initialize the service and register all the required observers.
     66   *
     67   * @param [options]
     68   */
     69  initialize(options) {
     70    if (this._initialized) {
     71      return false;
     72    }
     73 
     74    this._initialized = true;
     75 
     76    if (options.pushService) {
     77      this.pushService = options.pushService;
     78    } else {
     79      this.pushService = Cc["@mozilla.org/push/Service;1"].getService(
     80        Ci.nsIPushService
     81      );
     82    }
     83 
     84    if (options.fxai) {
     85      this.fxai = options.fxai;
     86    } else {
     87      const { getFxAccountsSingleton } = ChromeUtils.importESModule(
     88        "resource://gre/modules/FxAccounts.sys.mjs"
     89      );
     90      const fxAccounts = getFxAccountsSingleton();
     91      this.fxai = fxAccounts._internal;
     92    }
     93 
     94    this.asyncObserver = Async.asyncObserver(this, this.log);
     95    // We use an async observer because a device waking up can
     96    // observe multiple "Send Tab received" push notifications at the same time.
     97    // The way these notifications are handled is as follows:
     98    // Read index from storage, make network request, update the index.
     99    // You can imagine what happens when multiple calls race: we load
    100    // the same index multiple times and receive the same exact tabs, multiple times.
    101    // The async observer will ensure we make these network requests serially.
    102    Services.obs.addObserver(this.asyncObserver, this.pushService.pushTopic);
    103    Services.obs.addObserver(
    104      this.asyncObserver,
    105      this.pushService.subscriptionChangeTopic
    106    );
    107    Services.obs.addObserver(this.asyncObserver, ONLOGOUT_NOTIFICATION);
    108 
    109    this.log.debug("FxAccountsPush initialized");
    110    return true;
    111  },
    112  /**
    113   * Registers a new endpoint with the Push Server
    114   *
    115   * @returns {Promise}
    116   *          Promise always resolves with a subscription or a null if failed to subscribe.
    117   */
    118  registerPushEndpoint() {
    119    this.log.trace("FxAccountsPush registerPushEndpoint");
    120 
    121    return new Promise(resolve => {
    122      this.pushService.subscribe(
    123        FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
    124        Services.scriptSecurityManager.getSystemPrincipal(),
    125        (result, subscription) => {
    126          if (Components.isSuccessCode(result)) {
    127            this.log.debug("FxAccountsPush got subscription");
    128            resolve(subscription);
    129          } else {
    130            this.log.warn("FxAccountsPush failed to subscribe", result);
    131            resolve(null);
    132          }
    133        }
    134      );
    135    });
    136  },
    137  /**
    138   * Async observer interface to listen to push messages, changes and logout.
    139   *
    140   * @param subject
    141   * @param topic
    142   * @param data
    143   * @returns {Promise}
    144   */
    145  async observe(subject, topic, data) {
    146    try {
    147      this.log.trace(
    148        `observed topic=${topic}, data=${data}, subject=${subject}`
    149      );
    150      switch (topic) {
    151        case this.pushService.pushTopic:
    152          if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
    153            let message = subject.QueryInterface(Ci.nsIPushMessage);
    154            await this._onPushMessage(message);
    155          }
    156          break;
    157        case this.pushService.subscriptionChangeTopic:
    158          if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
    159            await this._onPushSubscriptionChange();
    160          }
    161          break;
    162        case ONLOGOUT_NOTIFICATION:
    163          // user signed out, we need to stop polling the Push Server
    164          await this.unsubscribe();
    165          break;
    166      }
    167    } catch (err) {
    168      this.log.error(err);
    169    }
    170  },
    171 
    172  /**
    173   * Fired when the Push server sends a notification.
    174   *
    175   * @private
    176   * @returns {Promise}
    177   */
    178  async _onPushMessage(message) {
    179    this.log.trace("FxAccountsPushService _onPushMessage");
    180    if (!message.data) {
    181      // Use the empty signal to check the verification state of the account right away
    182      this.log.debug(
    183        "empty push message, but oauth doesn't require checking account status - ignoring"
    184      );
    185      return;
    186    }
    187    let payload = message.data.json();
    188    this.log.debug(`push command: ${payload.command}`);
    189    switch (payload.command) {
    190      case ON_COMMAND_RECEIVED_NOTIFICATION:
    191        await this.fxai.commands.pollDeviceCommands(payload.data.index);
    192        break;
    193      case ON_DEVICE_CONNECTED_NOTIFICATION:
    194        Services.obs.notifyObservers(
    195          null,
    196          ON_DEVICE_CONNECTED_NOTIFICATION,
    197          payload.data.deviceName
    198        );
    199        break;
    200      case ON_DEVICE_DISCONNECTED_NOTIFICATION:
    201        this.fxai._handleDeviceDisconnection(payload.data.id);
    202        return;
    203      case ON_PROFILE_UPDATED_NOTIFICATION:
    204        // We already have a "profile updated" notification sent via WebChannel,
    205        // let's just re-use that.
    206        Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION);
    207        return;
    208      case ON_PASSWORD_CHANGED_NOTIFICATION:
    209      case ON_PASSWORD_RESET_NOTIFICATION:
    210        this._onPasswordChanged();
    211        return;
    212      case ON_ACCOUNT_DESTROYED_NOTIFICATION:
    213        this.fxai._handleAccountDestroyed(payload.data.uid);
    214        return;
    215      case ON_COLLECTION_CHANGED_NOTIFICATION:
    216        Services.obs.notifyObservers(
    217          null,
    218          ON_COLLECTION_CHANGED_NOTIFICATION,
    219          payload.data.collections
    220        );
    221        return;
    222      case ON_VERIFY_LOGIN_NOTIFICATION:
    223        Services.obs.notifyObservers(
    224          null,
    225          ON_VERIFY_LOGIN_NOTIFICATION,
    226          JSON.stringify(payload.data)
    227        );
    228        break;
    229      default:
    230        this.log.warn("FxA Push command unrecognized: " + payload.command);
    231    }
    232  },
    233  /**
    234   * Check the FxA session status after a password change/reset event.
    235   * If the session is invalid, reset credentials and notify listeners of
    236   * ON_ACCOUNT_STATE_CHANGE_NOTIFICATION that the account may have changed
    237   *
    238   * @returns {Promise}
    239   * @private
    240   */
    241  _onPasswordChanged() {
    242    return this.fxai.withCurrentAccountState(async state => {
    243      return this.fxai.checkAccountStatus(state);
    244    });
    245  },
    246  /**
    247   * Fired when the Push server drops a subscription, or the subscription identifier changes.
    248   *
    249   * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#Receiving_Push_Messages
    250   *
    251   * @returns {Promise}
    252   * @private
    253   */
    254  _onPushSubscriptionChange() {
    255    this.log.trace("FxAccountsPushService _onPushSubscriptionChange");
    256    return this.fxai.updateDeviceRegistration();
    257  },
    258  /**
    259   * Unsubscribe from the Push server
    260   *
    261   * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe()
    262   *
    263   * @returns {Promise} - The promise resolves with a bool to indicate if we successfully unsubscribed.
    264   *                      The promise never rejects.
    265   * @private
    266   */
    267  unsubscribe() {
    268    this.log.trace("FxAccountsPushService unsubscribe");
    269    return new Promise(resolve => {
    270      this.pushService.unsubscribe(
    271        FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
    272        Services.scriptSecurityManager.getSystemPrincipal(),
    273        (result, ok) => {
    274          if (Components.isSuccessCode(result)) {
    275            if (ok === true) {
    276              this.log.debug("FxAccountsPushService unsubscribed");
    277            } else {
    278              this.log.debug(
    279                "FxAccountsPushService had no subscription to unsubscribe"
    280              );
    281            }
    282          } else {
    283            this.log.warn(
    284              "FxAccountsPushService failed to unsubscribe",
    285              result
    286            );
    287          }
    288          return resolve(ok);
    289        }
    290      );
    291    });
    292  },
    293 
    294  /**
    295   * Get our Push server subscription.
    296   *
    297   * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#getSubscription()
    298   *
    299   * @returns {Promise} - resolves with the subscription or null. Never rejects.
    300   */
    301  getSubscription() {
    302    return new Promise(resolve => {
    303      this.pushService.getSubscription(
    304        FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
    305        Services.scriptSecurityManager.getSystemPrincipal(),
    306        (result, subscription) => {
    307          if (!subscription) {
    308            this.log.info("FxAccountsPushService no subscription found");
    309            return resolve(null);
    310          }
    311          return resolve(subscription);
    312        }
    313      );
    314    });
    315  },
    316 };