tor-browser

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

RemoteSettings.worker.mjs (6333B)


      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 /**
      6 * A worker dedicated to Remote Settings.
      7 */
      8 
      9 // These files are imported into the worker scope, and are not shared singletons
     10 // with the main thread.
     11 /* eslint-disable mozilla/reject-import-system-module-from-non-system */
     12 import { CanonicalJSON } from "resource://gre/modules/CanonicalJSON.sys.mjs";
     13 import { IDBHelpers } from "resource://services-settings/IDBHelpers.sys.mjs";
     14 import { SharedUtils } from "resource://services-settings/SharedUtils.sys.mjs";
     15 import { jsesc } from "resource://gre/modules/third_party/jsesc/jsesc.mjs";
     16 /* eslint-enable mozilla/reject-import-system-module-from-non-system */
     17 
     18 const IDB_RECORDS_STORE = "records";
     19 const IDB_TIMESTAMPS_STORE = "timestamps";
     20 
     21 let gShutdown = false;
     22 
     23 const Agent = {
     24  /**
     25   * Return the canonical JSON serialization of the specified records.
     26   * It has to match what is done on the server (See Kinto/kinto-signer).
     27   *
     28   * @param {Array<object>} records
     29   * @param {string} timestamp
     30   * @returns {string}
     31   */
     32  async canonicalStringify(records, timestamp) {
     33    // Sort list by record id.
     34    let allRecords = records.sort((a, b) => {
     35      if (a.id < b.id) {
     36        return -1;
     37      }
     38      return a.id > b.id ? 1 : 0;
     39    });
     40    // All existing records are replaced by the version from the server
     41    // and deleted records are removed.
     42    for (let i = 0; i < allRecords.length /* no increment! */; ) {
     43      const rec = allRecords[i];
     44      const next = allRecords[i + 1];
     45      if ((next && rec.id == next.id) || rec.deleted) {
     46        allRecords.splice(i, 1); // remove local record
     47      } else {
     48        i++;
     49      }
     50    }
     51    const toSerialize = {
     52      last_modified: "" + timestamp,
     53      data: allRecords,
     54    };
     55    return CanonicalJSON.stringify(toSerialize, jsesc);
     56  },
     57 
     58  /**
     59   * If present, import the JSON file into the Remote Settings IndexedDB
     60   * for the specified bucket and collection.
     61   * (eg. blocklists/certificates, main/onboarding)
     62   *
     63   * @param {string} bucket
     64   * @param {string} collection
     65   * @returns {int} Number of records loaded from dump or -1 if no dump found.
     66   */
     67  async importJSONDump(bucket, collection) {
     68    const { data: records, timestamp } = await SharedUtils.loadJSONDump(
     69      bucket,
     70      collection
     71    );
     72    if (records === null) {
     73      // Return -1 if file is missing.
     74      return -1;
     75    }
     76    if (gShutdown) {
     77      throw new Error("Can't import when we've started shutting down.");
     78    }
     79    await importDumpIDB(bucket, collection, records, timestamp);
     80    return records.length;
     81  },
     82 
     83  /**
     84   * Check that the specified file matches the expected size and SHA-256 hash.
     85   *
     86   * @param {string} fileUrl file URL to read from
     87   * @param {number} size expected file size
     88   * @param {string} size expected file SHA-256 as hex string
     89   * @returns {boolean}
     90   */
     91  async checkFileHash(fileUrl, size, hash) {
     92    let resp;
     93    try {
     94      resp = await fetch(fileUrl);
     95    } catch (e) {
     96      // File does not exist.
     97      return false;
     98    }
     99    const buffer = await resp.arrayBuffer();
    100    return SharedUtils.checkContentHash(buffer, size, hash);
    101  },
    102 
    103  async prepareShutdown() {
    104    gShutdown = true;
    105    // Ensure we can iterate and abort (which may delete items) by cloning
    106    // the list.
    107    let transactions = Array.from(gPendingTransactions);
    108    for (let transaction of transactions) {
    109      try {
    110        transaction.abort();
    111      } catch (ex) {
    112        // We can hit this case if the transaction has finished but
    113        // we haven't heard about it yet.
    114      }
    115    }
    116  },
    117 
    118  _test_only_import(bucket, collection, records, timestamp) {
    119    return importDumpIDB(bucket, collection, records, timestamp);
    120  },
    121 };
    122 
    123 /**
    124 * Wrap worker invocations in order to return the `callbackId` along
    125 * the result. This will allow to transform the worker invocations
    126 * into promises in `RemoteSettingsWorker.sys.mjs`.
    127 */
    128 self.onmessage = event => {
    129  const { callbackId, method, args = [] } = event.data;
    130  Agent[method](...args)
    131    .then(result => {
    132      self.postMessage({ callbackId, result });
    133    })
    134    .catch(error => {
    135      console.log(`RemoteSettingsWorker error: ${error}`);
    136      self.postMessage({ callbackId, error: "" + error });
    137    });
    138 };
    139 
    140 let gPendingTransactions = new Set();
    141 
    142 /**
    143 * Import the records into the Remote Settings Chrome IndexedDB.
    144 *
    145 * Note: This duplicates some logics from `kinto-offline-client.sys.mjs`.
    146 *
    147 * @param {string} bucket
    148 * @param {string} collection
    149 * @param {Array<object>} records
    150 * @param {number} timestamp
    151 */
    152 async function importDumpIDB(bucket, collection, records, timestamp) {
    153  // Open the DB. It will exist since if we are running this, it means
    154  // we already tried to read the timestamp in `remote-settings.sys.mjs`
    155  const db = await IDBHelpers.openIDB(false /* do not allow upgrades */);
    156 
    157  // try...finally to ensure we always close the db.
    158  try {
    159    if (gShutdown) {
    160      throw new Error("Can't import when we've started shutting down.");
    161    }
    162 
    163    // Each entry of the dump will be stored in the records store.
    164    // They are indexed by `_cid`.
    165    const cid = bucket + "/" + collection;
    166    // We can just modify the items in-place, as we got them from SharedUtils.loadJSONDump().
    167    records.forEach(item => {
    168      item._cid = cid;
    169    });
    170    // Store the collection timestamp.
    171    let { transaction, promise } = IDBHelpers.executeIDB(
    172      db,
    173      [IDB_RECORDS_STORE, IDB_TIMESTAMPS_STORE],
    174      "readwrite",
    175      ([recordsStore, timestampStore], rejectTransaction) => {
    176        // Wipe before loading
    177        recordsStore.delete(IDBKeyRange.bound([cid], [cid, []], false, true));
    178        IDBHelpers.bulkOperationHelper(
    179          recordsStore,
    180          {
    181            reject: rejectTransaction,
    182            completion() {
    183              timestampStore.put({ cid, value: timestamp });
    184            },
    185          },
    186          "put",
    187          records
    188        );
    189      }
    190    );
    191    gPendingTransactions.add(transaction);
    192    promise = promise.finally(() => gPendingTransactions.delete(transaction));
    193    await promise;
    194  } finally {
    195    // Close now that we're done.
    196    db.close();
    197  }
    198 }