tor-browser

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

PushBroadcastService.sys.mjs (8695B)


      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/. */
      4 
      5 const lazy = {};
      6 ChromeUtils.defineESModuleGetters(lazy, {
      7  JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
      8  PushService: "resource://gre/modules/PushService.sys.mjs",
      9 });
     10 
     11 // BroadcastService is exported for test purposes.
     12 // We are supposed to ignore any updates with this version.
     13 const DUMMY_VERSION_STRING = "____NOP____";
     14 
     15 ChromeUtils.defineLazyGetter(lazy, "console", () => {
     16  return console.createInstance({
     17    maxLogLevelPref: "dom.push.loglevel",
     18    prefix: "BroadcastService",
     19  });
     20 });
     21 
     22 class InvalidSourceInfo extends Error {
     23  constructor(message) {
     24    super(message);
     25    this.name = "InvalidSourceInfo";
     26  }
     27 }
     28 
     29 const BROADCAST_SERVICE_VERSION = 1;
     30 
     31 export var BroadcastService = class {
     32  constructor(pushService, path) {
     33    this.PHASES = {
     34      HELLO: "hello",
     35      REGISTER: "register",
     36      BROADCAST: "broadcast",
     37    };
     38 
     39    this.pushService = pushService;
     40    this.jsonFile = new lazy.JSONFile({
     41      path,
     42      dataPostProcessor: this._initializeJSONFile,
     43    });
     44    this.initializePromise = this.jsonFile.load();
     45  }
     46 
     47  /**
     48   * Convert the listeners from our on-disk format to the format
     49   * needed by a hello message.
     50   */
     51  async getListeners() {
     52    await this.initializePromise;
     53    return Object.entries(this.jsonFile.data.listeners).reduce(
     54      (acc, [k, v]) => {
     55        acc[k] = v.version;
     56        return acc;
     57      },
     58      {}
     59    );
     60  }
     61 
     62  _initializeJSONFile(data) {
     63    if (!data.version) {
     64      data.version = BROADCAST_SERVICE_VERSION;
     65    }
     66    if (!data.hasOwnProperty("listeners")) {
     67      data.listeners = {};
     68    }
     69    return data;
     70  }
     71 
     72  /**
     73   * Reset to a state akin to what you would get in a new profile.
     74   * In particular, wipe anything from storage.
     75   *
     76   * Used mainly for testing.
     77   */
     78  async _resetListeners() {
     79    await this.initializePromise;
     80    this.jsonFile.data = this._initializeJSONFile({});
     81    this.initializePromise = Promise.resolve();
     82  }
     83 
     84  /**
     85   * Ensure that a sourceInfo is correct (has the expected fields).
     86   */
     87  _validateSourceInfo(sourceInfo) {
     88    const { moduleURI, symbolName } = sourceInfo;
     89    if (typeof moduleURI !== "string") {
     90      throw new InvalidSourceInfo(
     91        `moduleURI must be a string (got ${typeof moduleURI})`
     92      );
     93    }
     94    if (typeof symbolName !== "string") {
     95      throw new InvalidSourceInfo(
     96        `symbolName must be a string (got ${typeof symbolName})`
     97      );
     98    }
     99  }
    100 
    101  /**
    102   * Add an entry for a given listener if it isn't present, or update
    103   * one if it is already present.
    104   *
    105   * Note that this means only a single listener can be set for a
    106   * given subscription. This is a limitation in the current API that
    107   * stems from the fact that there can only be one source of truth
    108   * for the subscriber's version. As a workaround, you can define a
    109   * listener which calls multiple other listeners.
    110   *
    111   * @param {string} broadcastId The broadcastID to listen for
    112   * @param {string} version The most recent version we have for
    113   *   updates from this broadcastID
    114   * @param {object} sourceInfo A description of the handler for
    115   *   updates on this broadcastID
    116   */
    117  async addListener(broadcastId, version, sourceInfo) {
    118    lazy.console.info(
    119      "addListener: adding listener",
    120      broadcastId,
    121      version,
    122      sourceInfo
    123    );
    124    await this.initializePromise;
    125    this._validateSourceInfo(sourceInfo);
    126    if (typeof version !== "string") {
    127      throw new TypeError("version should be a string");
    128    }
    129    if (!version) {
    130      throw new TypeError("version should not be an empty string");
    131    }
    132 
    133    const isNew = !this.jsonFile.data.listeners.hasOwnProperty(broadcastId);
    134    const oldVersion =
    135      !isNew && this.jsonFile.data.listeners[broadcastId].version;
    136    if (!isNew && oldVersion != version) {
    137      lazy.console.warn(
    138        "Versions differ while adding listener for",
    139        broadcastId,
    140        ". Got",
    141        version,
    142        "but JSON file says",
    143        oldVersion,
    144        "."
    145      );
    146    }
    147 
    148    // Update listeners before telling the pushService to subscribe,
    149    // in case it would disregard the update in the small window
    150    // between getting listeners and setting state to RUNNING.
    151    //
    152    // Keep the old version (if we have it) because Megaphone is
    153    // really the source of truth for the current version of this
    154    // broadcaster, and the old version is whatever we've either
    155    // gotten from Megaphone or what we've told to Megaphone and
    156    // haven't been corrected.
    157    this.jsonFile.data.listeners[broadcastId] = {
    158      version: oldVersion || version,
    159      sourceInfo,
    160    };
    161    this.jsonFile.saveSoon();
    162 
    163    if (isNew) {
    164      await this.pushService.subscribeBroadcast(broadcastId, version);
    165    }
    166  }
    167 
    168  /**
    169   * Call the listeners of the specified broadcasts.
    170   *
    171   * @param {Array<object>} broadcasts Map between broadcast ids and versions.
    172   * @param {object} context Additional information about the context in which the
    173   *  broadcast notification was originally received. This is transmitted to listeners.
    174   * @param {string} context.phase One of `BroadcastService.PHASES`
    175   */
    176  async receivedBroadcastMessage(broadcasts, context) {
    177    lazy.console.info("receivedBroadcastMessage:", broadcasts, context);
    178    await this.initializePromise;
    179    for (const broadcastId in broadcasts) {
    180      const version = broadcasts[broadcastId];
    181      if (version === DUMMY_VERSION_STRING) {
    182        lazy.console.info(
    183          "Ignoring",
    184          version,
    185          "because it's the dummy version"
    186        );
    187        continue;
    188      }
    189      // We don't know this broadcastID. This is probably a bug?
    190      if (!this.jsonFile.data.listeners.hasOwnProperty(broadcastId)) {
    191        lazy.console.warn(
    192          "receivedBroadcastMessage: unknown broadcastId",
    193          broadcastId
    194        );
    195        continue;
    196      }
    197 
    198      const { sourceInfo } = this.jsonFile.data.listeners[broadcastId];
    199      try {
    200        this._validateSourceInfo(sourceInfo);
    201      } catch (e) {
    202        lazy.console.error(
    203          "receivedBroadcastMessage: malformed sourceInfo",
    204          sourceInfo,
    205          e
    206        );
    207        continue;
    208      }
    209 
    210      const { moduleURI, symbolName } = sourceInfo;
    211 
    212      let module;
    213      try {
    214        module = ChromeUtils.importESModule(moduleURI);
    215      } catch (e) {
    216        lazy.console.error(
    217          "receivedBroadcastMessage: couldn't invoke",
    218          broadcastId,
    219          "because import of module",
    220          moduleURI,
    221          "failed",
    222          e
    223        );
    224        continue;
    225      }
    226 
    227      if (!module[symbolName]) {
    228        lazy.console.error(
    229          "receivedBroadcastMessage: couldn't invoke",
    230          broadcastId,
    231          "because module",
    232          moduleURI,
    233          "missing attribute",
    234          symbolName
    235        );
    236        continue;
    237      }
    238 
    239      const handler = module[symbolName];
    240 
    241      if (!handler.receivedBroadcastMessage) {
    242        lazy.console.error(
    243          "receivedBroadcastMessage: couldn't invoke",
    244          broadcastId,
    245          "because handler returned by",
    246          `${moduleURI}.${symbolName}`,
    247          "has no receivedBroadcastMessage method"
    248        );
    249        continue;
    250      }
    251 
    252      try {
    253        await handler.receivedBroadcastMessage(version, broadcastId, context);
    254      } catch (e) {
    255        lazy.console.error(
    256          "receivedBroadcastMessage: handler for",
    257          broadcastId,
    258          "threw error:",
    259          e
    260        );
    261        continue;
    262      }
    263 
    264      // Broadcast message applied successfully. Update the version we
    265      // received if it's different than the one we had.  We don't
    266      // enforce an ordering here (i.e. we use != instead of <)
    267      // because we don't know what the ordering of the service's
    268      // versions is going to be.
    269      if (this.jsonFile.data.listeners[broadcastId].version != version) {
    270        this.jsonFile.data.listeners[broadcastId].version = version;
    271        this.jsonFile.saveSoon();
    272      }
    273    }
    274  }
    275 
    276  // For test only.
    277  _saveImmediately() {
    278    return this.jsonFile._save();
    279  }
    280 };
    281 
    282 function initializeBroadcastService() {
    283  // Fallback path for xpcshell tests.
    284  let path = "broadcast-listeners.json";
    285  try {
    286    if (PathUtils.profileDir) {
    287      // Real path for use in a real profile.
    288      path = PathUtils.join(PathUtils.profileDir, path);
    289    }
    290  } catch (e) {}
    291  return new BroadcastService(lazy.PushService, path);
    292 }
    293 
    294 export var pushBroadcastService = initializeBroadcastService();