tor-browser

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

util.sys.mjs (21639B)


      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 { Observers } from "resource://services-common/observers.sys.mjs";
      6 
      7 import { CommonUtils } from "resource://services-common/utils.sys.mjs";
      8 import { CryptoUtils } from "moz-src:///services/crypto/modules/utils.sys.mjs";
      9 
     10 import {
     11  DEVICE_TYPE_DESKTOP,
     12  MAXIMUM_BACKOFF_INTERVAL,
     13  PREFS_BRANCH,
     14  SYNC_KEY_DECODED_LENGTH,
     15  SYNC_KEY_ENCODED_LENGTH,
     16  WEAVE_VERSION,
     17 } from "resource://services-sync/constants.sys.mjs";
     18 
     19 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     20 
     21 const lazy = {};
     22 import * as FxAccountsCommon from "resource://gre/modules/FxAccountsCommon.sys.mjs";
     23 
     24 XPCOMUtils.defineLazyServiceGetter(
     25  lazy,
     26  "cryptoSDR",
     27  "@mozilla.org/login-manager/crypto/SDR;1",
     28  Ci.nsILoginManagerCrypto
     29 );
     30 
     31 XPCOMUtils.defineLazyPreferenceGetter(
     32  lazy,
     33  "localDeviceType",
     34  "services.sync.client.type",
     35  DEVICE_TYPE_DESKTOP
     36 );
     37 
     38 /*
     39 * Custom exception types.
     40 */
     41 class LockException extends Error {
     42  constructor(message) {
     43    super(message);
     44    this.name = "LockException";
     45  }
     46 }
     47 
     48 class HMACMismatch extends Error {
     49  constructor(message) {
     50    super(message);
     51    this.name = "HMACMismatch";
     52  }
     53 }
     54 
     55 /*
     56 * Utility functions
     57 */
     58 export var Utils = {
     59  // Aliases from CryptoUtils.
     60  generateRandomBytesLegacy: CryptoUtils.generateRandomBytesLegacy,
     61  computeHTTPMACSHA1: CryptoUtils.computeHTTPMACSHA1,
     62  digestUTF8: CryptoUtils.digestUTF8,
     63  digestBytes: CryptoUtils.digestBytes,
     64  sha256: CryptoUtils.sha256,
     65  hkdfExpand: CryptoUtils.hkdfExpand,
     66  pbkdf2Generate: CryptoUtils.pbkdf2Generate,
     67  getHTTPMACSHA1Header: CryptoUtils.getHTTPMACSHA1Header,
     68 
     69  /**
     70   * The string to use as the base User-Agent in Sync requests.
     71   * This string will look something like
     72   *
     73   *   Firefox/49.0a1 (Windows NT 6.1; WOW64; rv:46.0) FxSync/1.51.0.20160516142357.desktop
     74   */
     75  _userAgent: null,
     76  get userAgent() {
     77    if (!this._userAgent) {
     78      let hph = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
     79        Ci.nsIHttpProtocolHandler
     80      );
     81      /* eslint-disable no-multi-spaces */
     82      this._userAgent =
     83        Services.appinfo.name +
     84        "/" +
     85        Services.appinfo.version + // Product.
     86        " (" +
     87        hph.oscpu +
     88        ")" + // (oscpu)
     89        " FxSync/" +
     90        WEAVE_VERSION +
     91        "." + // Sync.
     92        Services.appinfo.appBuildID +
     93        "."; // Build.
     94      /* eslint-enable no-multi-spaces */
     95    }
     96    return this._userAgent + lazy.localDeviceType;
     97  },
     98 
     99  /**
    100   * Wrap a [promise-returning] function to catch all exceptions and log them.
    101   *
    102   * Optionally pass a function which will be called if an
    103   * exception occurs.
    104   *
    105   * @example
    106   * MyObj._catch = Utils.catch;
    107   * MyObj.foo = function() { this._catch(func)(); }
    108   */
    109  catch(func, exceptionCallback) {
    110    let thisArg = this;
    111    return async function WrappedCatch() {
    112      try {
    113        return await func.call(thisArg);
    114      } catch (ex) {
    115        thisArg._log.debug(
    116          "Exception calling " + (func.name || "anonymous function"),
    117          ex
    118        );
    119        if (exceptionCallback) {
    120          return exceptionCallback.call(thisArg, ex);
    121        }
    122        return null;
    123      }
    124    };
    125  },
    126 
    127  throwLockException(label) {
    128    throw new LockException(`Could not acquire lock. Label: "${label}".`);
    129  },
    130 
    131  /**
    132   * Wrap a [promise-returning] function to call lock before calling the function
    133   * then unlock when it finishes executing or if it threw an error.
    134   *
    135   * @example
    136   * MyObj._lock = Utils.lock;
    137   * MyObj.foo = async function() { await this._lock(func)(); }
    138   */
    139  lock(label, func) {
    140    let thisArg = this;
    141    return async function WrappedLock() {
    142      if (!thisArg.lock()) {
    143        Utils.throwLockException(label);
    144      }
    145 
    146      try {
    147        return await func.call(thisArg);
    148      } finally {
    149        thisArg.unlock();
    150      }
    151    };
    152  },
    153 
    154  isLockException: function isLockException(ex) {
    155    return ex instanceof LockException;
    156  },
    157 
    158  /**
    159   * Wrap [promise-returning] functions to notify when it starts and
    160   * finishes executing or if it threw an error.
    161   *
    162   * The message is a combination of a provided prefix, the local name, and
    163   * the event. Possible events are: "start", "finish", "error". The subject
    164   * is the function's return value on "finish" or the caught exception on
    165   * "error". The data argument is the predefined data value.
    166   *
    167   * @example
    168   * function MyObj(name) {
    169   *   this.name = name;
    170   *   this._notify = Utils.notify("obj:");
    171   * }
    172   * MyObj.prototype = {
    173   *   foo: function() this._notify("func", "data-arg", async function () {
    174   *     //...
    175   *   }(),
    176   * };
    177   */
    178  notify(prefix) {
    179    return function NotifyMaker(name, data, func) {
    180      let thisArg = this;
    181      let notify = function (state, subject) {
    182        let mesg = prefix + name + ":" + state;
    183        thisArg._log.trace("Event: " + mesg);
    184        Observers.notify(mesg, subject, data);
    185      };
    186 
    187      return async function WrappedNotify() {
    188        notify("start", null);
    189        try {
    190          let ret = await func.call(thisArg);
    191          notify("finish", ret);
    192          return ret;
    193        } catch (ex) {
    194          notify("error", ex);
    195          throw ex;
    196        }
    197      };
    198    };
    199  },
    200 
    201  /**
    202   * GUIDs are 9 random bytes encoded with base64url (RFC 4648).
    203   * That makes them 12 characters long with 72 bits of entropy.
    204   */
    205  makeGUID: function makeGUID() {
    206    return CommonUtils.encodeBase64URL(Utils.generateRandomBytesLegacy(9));
    207  },
    208 
    209  _base64url_regex: /^[-abcdefghijklmnopqrstuvwxyz0123456789_]{12}$/i,
    210  checkGUID: function checkGUID(guid) {
    211    return !!guid && this._base64url_regex.test(guid);
    212  },
    213 
    214  /**
    215   * Add a simple getter/setter to an object that defers access of a property
    216   * to an inner property.
    217   *
    218   * @param obj
    219   *        Object to add properties to defer in its prototype
    220   * @param defer
    221   *        Property of obj to defer to
    222   * @param prop
    223   *        Property name to defer (or an array of property names)
    224   */
    225  deferGetSet: function Utils_deferGetSet(obj, defer, prop) {
    226    if (Array.isArray(prop)) {
    227      return prop.map(prop => Utils.deferGetSet(obj, defer, prop));
    228    }
    229 
    230    let prot = obj.prototype;
    231 
    232    // Create a getter if it doesn't exist yet
    233    if (!prot.__lookupGetter__(prop)) {
    234      prot.__defineGetter__(prop, function () {
    235        return this[defer][prop];
    236      });
    237    }
    238 
    239    // Create a setter if it doesn't exist yet
    240    if (!prot.__lookupSetter__(prop)) {
    241      prot.__defineSetter__(prop, function (val) {
    242        this[defer][prop] = val;
    243      });
    244    }
    245  },
    246 
    247  deepEquals: function eq(a, b) {
    248    // If they're triple equals, then it must be equals!
    249    if (a === b) {
    250      return true;
    251    }
    252 
    253    // If they weren't equal, they must be objects to be different
    254    if (typeof a != "object" || typeof b != "object") {
    255      return false;
    256    }
    257 
    258    // But null objects won't have properties to compare
    259    if (a === null || b === null) {
    260      return false;
    261    }
    262 
    263    // Make sure all of a's keys have a matching value in b
    264    for (let k in a) {
    265      if (!eq(a[k], b[k])) {
    266        return false;
    267      }
    268    }
    269 
    270    // Do the same for b's keys but skip those that we already checked
    271    for (let k in b) {
    272      if (!(k in a) && !eq(a[k], b[k])) {
    273        return false;
    274      }
    275    }
    276 
    277    return true;
    278  },
    279 
    280  // Generator and discriminator for HMAC exceptions.
    281  // Split these out in case we want to make them richer in future, and to
    282  // avoid inevitable confusion if the message changes.
    283  throwHMACMismatch: function throwHMACMismatch(shouldBe, is) {
    284    throw new HMACMismatch(
    285      `Record SHA256 HMAC mismatch: should be ${shouldBe}, is ${is}`
    286    );
    287  },
    288 
    289  isHMACMismatch: function isHMACMismatch(ex) {
    290    return ex instanceof HMACMismatch;
    291  },
    292 
    293  /**
    294   * Turn RFC 4648 base32 into our own user-friendly version.
    295   *   ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
    296   * becomes
    297   *   abcdefghijk8mn9pqrstuvwxyz234567
    298   */
    299  base32ToFriendly: function base32ToFriendly(input) {
    300    return input.toLowerCase().replace(/l/g, "8").replace(/o/g, "9");
    301  },
    302 
    303  base32FromFriendly: function base32FromFriendly(input) {
    304    return input.toUpperCase().replace(/8/g, "L").replace(/9/g, "O");
    305  },
    306 
    307  /**
    308   * Key manipulation.
    309   */
    310 
    311  // Return an octet string in friendly base32 *with no trailing =*.
    312  encodeKeyBase32: function encodeKeyBase32(keyData) {
    313    return Utils.base32ToFriendly(CommonUtils.encodeBase32(keyData)).slice(
    314      0,
    315      SYNC_KEY_ENCODED_LENGTH
    316    );
    317  },
    318 
    319  decodeKeyBase32: function decodeKeyBase32(encoded) {
    320    return CommonUtils.decodeBase32(
    321      Utils.base32FromFriendly(Utils.normalizePassphrase(encoded))
    322    ).slice(0, SYNC_KEY_DECODED_LENGTH);
    323  },
    324 
    325  jsonFilePath(...args) {
    326    let [fileName] = args.splice(-1);
    327 
    328    return PathUtils.join(
    329      PathUtils.profileDir,
    330      "weave",
    331      ...args,
    332      `${fileName}.json`
    333    );
    334  },
    335 
    336  /**
    337   * Load a JSON file from disk in the profile directory.
    338   *
    339   * @param filePath
    340   *        JSON file path load from profile. Loaded file will be
    341   *        extension.
    342   * @param that
    343   *        Object to use for logging.
    344   *
    345   * @return Promise<>
    346   *        Promise resolved when the write has been performed.
    347   */
    348  async jsonLoad(filePath, that) {
    349    let path;
    350    if (Array.isArray(filePath)) {
    351      path = Utils.jsonFilePath(...filePath);
    352    } else {
    353      path = Utils.jsonFilePath(filePath);
    354    }
    355 
    356    if (that._log && that._log.trace) {
    357      that._log.trace("Loading json from disk: " + path);
    358    }
    359 
    360    try {
    361      return await IOUtils.readJSON(path);
    362    } catch (e) {
    363      if (!DOMException.isInstance(e) || e.name !== "NotFoundError") {
    364        if (that._log) {
    365          that._log.debug("Failed to load json", e);
    366        }
    367      }
    368      return null;
    369    }
    370  },
    371 
    372  /**
    373   * Save a json-able object to disk in the profile directory.
    374   *
    375   * @param filePath
    376   *        JSON file path save to <filePath>.json
    377   * @param that
    378   *        Object to use for logging.
    379   * @param obj
    380   *        Function to provide json-able object to save. If this isn't a
    381   *        function, it'll be used as the object to make a json string.*
    382   *        Function called when the write has been performed. Optional.
    383   *
    384   * @return Promise<>
    385   *        Promise resolved when the write has been performed.
    386   */
    387  async jsonSave(filePath, that, obj) {
    388    let path = PathUtils.join(
    389      PathUtils.profileDir,
    390      "weave",
    391      ...(filePath + ".json").split("/")
    392    );
    393    let dir = PathUtils.parent(path);
    394 
    395    await IOUtils.makeDirectory(dir, { createAncestors: true });
    396 
    397    if (that._log) {
    398      that._log.trace("Saving json to disk: " + path);
    399    }
    400 
    401    let json = typeof obj == "function" ? obj.call(that) : obj;
    402 
    403    return IOUtils.writeJSON(path, json);
    404  },
    405 
    406  /**
    407   * Helper utility function to fit an array of records so that when serialized,
    408   * they will be within payloadSizeMaxBytes. Returns a new array without the
    409   * items.
    410   *
    411   * Note: This shouldn't be used for extremely large record sizes as
    412   * it uses JSON.stringify, which could lead to a heavy performance hit.
    413   * See Bug 1815151 for more details.
    414   *
    415   */
    416  tryFitItems(records, payloadSizeMaxBytes) {
    417    // Copy this so that callers don't have to do it in advance.
    418    records = records.slice();
    419    let encoder = Utils.utf8Encoder;
    420    const computeSerializedSize = () =>
    421      encoder.encode(JSON.stringify(records)).byteLength;
    422    // Figure out how many records we can pack into a payload.
    423    // We use byteLength here because the data is not encrypted in ascii yet.
    424    let size = computeSerializedSize();
    425    // See bug 535326 comment 8 for an explanation of the estimation
    426    const maxSerializedSize = (payloadSizeMaxBytes / 4) * 3 - 1500;
    427    if (maxSerializedSize < 0) {
    428      // This is probably due to a test, but it causes very bad behavior if a
    429      // test causes this accidentally. We could throw, but there's an obvious/
    430      // natural way to handle it, so we do that instead (otherwise we'd have a
    431      // weird lower bound of ~1125b on the max record payload size).
    432      return [];
    433    }
    434    if (size > maxSerializedSize) {
    435      // Estimate a little more than the direct fraction to maximize packing
    436      let cutoff = Math.ceil((records.length * maxSerializedSize) / size);
    437      records = records.slice(0, cutoff + 1);
    438 
    439      // Keep dropping off the last entry until the data fits.
    440      while (computeSerializedSize() > maxSerializedSize) {
    441        records.pop();
    442      }
    443    }
    444    return records;
    445  },
    446 
    447  /**
    448   * Move a json file in the profile directory. Will fail if a file exists at the
    449   * destination.
    450   *
    451   * @returns a promise that resolves to undefined on success, or rejects on failure
    452   *
    453   * @param aFrom
    454   *        Current path to the JSON file saved on disk, relative to profileDir/weave
    455   *        .json will be appended to the file name.
    456   * @param aTo
    457   *        New path to the JSON file saved on disk, relative to profileDir/weave
    458   *        .json will be appended to the file name.
    459   * @param that
    460   *        Object to use for logging
    461   */
    462  jsonMove(aFrom, aTo, that) {
    463    let pathFrom = PathUtils.join(
    464      PathUtils.profileDir,
    465      "weave",
    466      ...(aFrom + ".json").split("/")
    467    );
    468    let pathTo = PathUtils.join(
    469      PathUtils.profileDir,
    470      "weave",
    471      ...(aTo + ".json").split("/")
    472    );
    473    if (that._log) {
    474      that._log.trace("Moving " + pathFrom + " to " + pathTo);
    475    }
    476    return IOUtils.move(pathFrom, pathTo, { noOverwrite: true });
    477  },
    478 
    479  /**
    480   * Removes a json file in the profile directory.
    481   *
    482   * @returns a promise that resolves to undefined on success, or rejects on failure
    483   *
    484   * @param filePath
    485   *        Current path to the JSON file saved on disk, relative to profileDir/weave
    486   *        .json will be appended to the file name.
    487   * @param that
    488   *        Object to use for logging
    489   */
    490  jsonRemove(filePath, that) {
    491    let path = PathUtils.join(
    492      PathUtils.profileDir,
    493      "weave",
    494      ...(filePath + ".json").split("/")
    495    );
    496    if (that._log) {
    497      that._log.trace("Deleting " + path);
    498    }
    499    return IOUtils.remove(path, { ignoreAbsent: true });
    500  },
    501 
    502  /**
    503   * The following are the methods supported for UI use:
    504   *
    505   * * isPassphrase:
    506   *     determines whether a string is either a normalized or presentable
    507   *     passphrase.
    508   * * normalizePassphrase:
    509   *     take a presentable passphrase and reduce it to a normalized
    510   *     representation for storage. normalizePassphrase can safely be called
    511   *     on normalized input.
    512   */
    513 
    514  isPassphrase(s) {
    515    if (s) {
    516      return /^[abcdefghijkmnpqrstuvwxyz23456789]{26}$/.test(
    517        Utils.normalizePassphrase(s)
    518      );
    519    }
    520    return false;
    521  },
    522 
    523  normalizePassphrase: function normalizePassphrase(pp) {
    524    // Short var name... have you seen the lines below?!
    525    // Allow leading and trailing whitespace.
    526    pp = pp.trim().toLowerCase();
    527 
    528    // 20-char sync key.
    529    if (pp.length == 23 && [5, 11, 17].every(i => pp[i] == "-")) {
    530      return (
    531        pp.slice(0, 5) + pp.slice(6, 11) + pp.slice(12, 17) + pp.slice(18, 23)
    532      );
    533    }
    534 
    535    // "Modern" 26-char key.
    536    if (pp.length == 31 && [1, 7, 13, 19, 25].every(i => pp[i] == "-")) {
    537      return (
    538        pp.slice(0, 1) +
    539        pp.slice(2, 7) +
    540        pp.slice(8, 13) +
    541        pp.slice(14, 19) +
    542        pp.slice(20, 25) +
    543        pp.slice(26, 31)
    544      );
    545    }
    546 
    547    // Something else -- just return.
    548    return pp;
    549  },
    550 
    551  /**
    552   * Create an array like the first but without elements of the second. Reuse
    553   * arrays if possible.
    554   */
    555  arraySub: function arraySub(minuend, subtrahend) {
    556    if (!minuend.length || !subtrahend.length) {
    557      return minuend;
    558    }
    559    let setSubtrahend = new Set(subtrahend);
    560    return minuend.filter(i => !setSubtrahend.has(i));
    561  },
    562 
    563  /**
    564   * Build the union of two arrays. Reuse arrays if possible.
    565   */
    566  arrayUnion: function arrayUnion(foo, bar) {
    567    if (!foo.length) {
    568      return bar;
    569    }
    570    if (!bar.length) {
    571      return foo;
    572    }
    573    return foo.concat(Utils.arraySub(bar, foo));
    574  },
    575 
    576  /**
    577   * Add all the items in `items` to the provided Set in-place.
    578   *
    579   * @return The provided set.
    580   */
    581  setAddAll(set, items) {
    582    for (let item of items) {
    583      set.add(item);
    584    }
    585    return set;
    586  },
    587 
    588  /**
    589   * Delete every items in `items` to the provided Set in-place.
    590   *
    591   * @return The provided set.
    592   */
    593  setDeleteAll(set, items) {
    594    for (let item of items) {
    595      set.delete(item);
    596    }
    597    return set;
    598  },
    599 
    600  /**
    601   * Take the first `size` items from the Set `items`.
    602   *
    603   * @return A Set of size at most `size`
    604   */
    605  subsetOfSize(items, size) {
    606    let result = new Set();
    607    let count = 0;
    608    for (let item of items) {
    609      if (count++ == size) {
    610        return result;
    611      }
    612      result.add(item);
    613    }
    614    return result;
    615  },
    616 
    617  bind2: function Async_bind2(object, method) {
    618    return function innerBind() {
    619      return method.apply(object, arguments);
    620    };
    621  },
    622 
    623  /**
    624   * Is there a master password configured and currently locked?
    625   */
    626  mpLocked() {
    627    return !lazy.cryptoSDR.isLoggedIn;
    628  },
    629 
    630  // If Master Password is enabled and locked, present a dialog to unlock it.
    631  // Return whether the system is unlocked.
    632  ensureMPUnlocked() {
    633    if (lazy.cryptoSDR.uiBusy) {
    634      return false;
    635    }
    636    try {
    637      lazy.cryptoSDR.encrypt("bacon");
    638      return true;
    639    } catch (e) {}
    640    return false;
    641  },
    642 
    643  /**
    644   * Return a value for a backoff interval.  Maximum is eight hours, unless
    645   * Status.backoffInterval is higher.
    646   *
    647   */
    648  calculateBackoff: function calculateBackoff(
    649    attempts,
    650    baseInterval,
    651    statusInterval
    652  ) {
    653    let backoffInterval =
    654      attempts * (Math.floor(Math.random() * baseInterval) + baseInterval);
    655    return Math.max(
    656      Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL),
    657      statusInterval
    658    );
    659  },
    660 
    661  /**
    662   * Return a set of hostnames (including the protocol) which may have
    663   * credentials for sync itself stored in the login manager.
    664   *
    665   * In general, these hosts will not have their passwords synced, will be
    666   * reset when we drop sync credentials, etc.
    667   */
    668  getSyncCredentialsHosts() {
    669    let result = new Set();
    670    // the FxA host
    671    result.add(FxAccountsCommon.FXA_PWDMGR_HOST);
    672    // We used to include the FxA hosts (hence the Set() result) but we now
    673    // don't give them special treatment (hence the Set() with exactly 1 item)
    674    return result;
    675  },
    676 
    677  /**
    678   * Helper to implement a more efficient version of fairly common pattern:
    679   *
    680   * Utils.defineLazyIDProperty(this, "syncID", "services.sync.client.syncID")
    681   *
    682   * is equivalent to (but more efficient than) the following:
    683   *
    684   * Foo.prototype = {
    685   *   ...
    686   *   get syncID() {
    687   *     let syncID = Svc.PrefBranch.getStringPref("client.syncID", "");
    688   *     return syncID == "" ? this.syncID = Utils.makeGUID() : syncID;
    689   *   },
    690   *   set syncID(value) {
    691   *     Svc.PrefBranch.setStringPref("client.syncID", value);
    692   *   },
    693   *   ...
    694   * };
    695   */
    696  defineLazyIDProperty(object, propName, prefName) {
    697    // An object that exists to be the target of the lazy pref getter.
    698    // We can't use `object` (at least, not using `propName`) since XPCOMUtils
    699    // will stomp on any setter we define.
    700    const storage = {};
    701    XPCOMUtils.defineLazyPreferenceGetter(storage, "value", prefName, "");
    702    Object.defineProperty(object, propName, {
    703      configurable: true,
    704      enumerable: true,
    705      get() {
    706        let value = storage.value;
    707        if (!value) {
    708          value = Utils.makeGUID();
    709          Services.prefs.setStringPref(prefName, value);
    710        }
    711        return value;
    712      },
    713      set(value) {
    714        Services.prefs.setStringPref(prefName, value);
    715      },
    716    });
    717  },
    718 
    719  getDeviceType() {
    720    return lazy.localDeviceType;
    721  },
    722 
    723  formatTimestamp(date) {
    724    // Format timestamp as: "%Y-%m-%d %H:%M:%S"
    725    let year = String(date.getFullYear());
    726    let month = String(date.getMonth() + 1).padStart(2, "0");
    727    let day = String(date.getDate()).padStart(2, "0");
    728    let hours = String(date.getHours()).padStart(2, "0");
    729    let minutes = String(date.getMinutes()).padStart(2, "0");
    730    let seconds = String(date.getSeconds()).padStart(2, "0");
    731 
    732    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    733  },
    734 
    735  *walkTree(tree, parent = null) {
    736    if (tree) {
    737      // Skip root node
    738      if (parent) {
    739        yield [tree, parent];
    740      }
    741      if (tree.children) {
    742        for (let child of tree.children) {
    743          yield* Utils.walkTree(child, tree);
    744        }
    745      }
    746    }
    747  },
    748 };
    749 
    750 /**
    751 * A subclass of Set that serializes as an Array when passed to JSON.stringify.
    752 */
    753 export class SerializableSet extends Set {
    754  toJSON() {
    755    return Array.from(this);
    756  }
    757 }
    758 
    759 ChromeUtils.defineLazyGetter(Utils, "_utf8Converter", function () {
    760  let converter = Cc[
    761    "@mozilla.org/intl/scriptableunicodeconverter"
    762  ].createInstance(Ci.nsIScriptableUnicodeConverter);
    763  converter.charset = "UTF-8";
    764  return converter;
    765 });
    766 
    767 ChromeUtils.defineLazyGetter(Utils, "utf8Encoder", () => new TextEncoder());
    768 
    769 /*
    770 * Commonly-used services
    771 */
    772 export var Svc = {};
    773 
    774 Svc.PrefBranch = Services.prefs.getBranch(PREFS_BRANCH);
    775 Svc.Obs = Observers;
    776 
    777 Svc.Obs.add("xpcom-shutdown", function () {
    778  for (let name in Svc) {
    779    delete Svc[name];
    780  }
    781 });