summaryrefslogtreecommitdiffstats
path: root/services/settings/RemoteSettingsWorker.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--services/settings/RemoteSettingsWorker.js204
1 files changed, 204 insertions, 0 deletions
diff --git a/services/settings/RemoteSettingsWorker.js b/services/settings/RemoteSettingsWorker.js
new file mode 100644
index 0000000000..96ca530112
--- /dev/null
+++ b/services/settings/RemoteSettingsWorker.js
@@ -0,0 +1,204 @@
+/* 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/. */
+
+/* eslint-env mozilla/chrome-worker */
+
+"use strict";
+
+/**
+ * A worker dedicated to Remote Settings.
+ */
+
+/* import-globals-from /toolkit/components/workerloader/require.js */
+/* import-globals-from /toolkit/modules/CanonicalJSON.jsm */
+/* import-globals-from IDBHelpers.jsm */
+/* import-globals-from SharedUtils.jsm */
+/* import-globals-from /toolkit/modules/third_party/jsesc/jsesc.js */
+importScripts(
+ "resource://gre/modules/workers/require.js",
+ "resource://gre/modules/CanonicalJSON.jsm",
+ "resource://services-settings/IDBHelpers.jsm",
+ "resource://services-settings/SharedUtils.jsm",
+ "resource://gre/modules/third_party/jsesc/jsesc.js"
+);
+
+const IDB_RECORDS_STORE = "records";
+const IDB_TIMESTAMPS_STORE = "timestamps";
+
+let gShutdown = false;
+
+const Agent = {
+ /**
+ * Return the canonical JSON serialization of the specified records.
+ * It has to match what is done on the server (See Kinto/kinto-signer).
+ *
+ * @param {Array<Object>} records
+ * @param {String} timestamp
+ * @returns {String}
+ */
+ async canonicalStringify(records, timestamp) {
+ // Sort list by record id.
+ let allRecords = records.sort((a, b) => {
+ if (a.id < b.id) {
+ return -1;
+ }
+ return a.id > b.id ? 1 : 0;
+ });
+ // All existing records are replaced by the version from the server
+ // and deleted records are removed.
+ for (let i = 0; i < allRecords.length /* no increment! */; ) {
+ const rec = allRecords[i];
+ const next = allRecords[i + 1];
+ if ((next && rec.id == next.id) || rec.deleted) {
+ allRecords.splice(i, 1); // remove local record
+ } else {
+ i++;
+ }
+ }
+ const toSerialize = {
+ last_modified: "" + timestamp,
+ data: allRecords,
+ };
+ return CanonicalJSON.stringify(toSerialize, jsesc);
+ },
+
+ /**
+ * If present, import the JSON file into the Remote Settings IndexedDB
+ * for the specified bucket and collection.
+ * (eg. blocklists/certificates, main/onboarding)
+ * @param {String} bucket
+ * @param {String} collection
+ * @returns {int} Number of records loaded from dump or -1 if no dump found.
+ */
+ async importJSONDump(bucket, collection) {
+ const { data: records, timestamp } = await SharedUtils.loadJSONDump(
+ bucket,
+ collection
+ );
+ if (records === null) {
+ // Return -1 if file is missing.
+ return -1;
+ }
+ if (gShutdown) {
+ throw new Error("Can't import when we've started shutting down.");
+ }
+ await importDumpIDB(bucket, collection, records, timestamp);
+ return records.length;
+ },
+
+ /**
+ * Check that the specified file matches the expected size and SHA-256 hash.
+ * @param {String} fileUrl file URL to read from
+ * @param {Number} size expected file size
+ * @param {String} size expected file SHA-256 as hex string
+ * @returns {boolean}
+ */
+ async checkFileHash(fileUrl, size, hash) {
+ let resp;
+ try {
+ resp = await fetch(fileUrl);
+ } catch (e) {
+ // File does not exist.
+ return false;
+ }
+ const buffer = await resp.arrayBuffer();
+ return SharedUtils.checkContentHash(buffer, size, hash);
+ },
+
+ async prepareShutdown() {
+ gShutdown = true;
+ // Ensure we can iterate and abort (which may delete items) by cloning
+ // the list.
+ let transactions = Array.from(gPendingTransactions);
+ for (let transaction of transactions) {
+ try {
+ transaction.abort();
+ } catch (ex) {
+ // We can hit this case if the transaction has finished but
+ // we haven't heard about it yet.
+ }
+ }
+ },
+
+ _test_only_import(bucket, collection, records, timestamp) {
+ return importDumpIDB(bucket, collection, records, timestamp);
+ },
+};
+
+/**
+ * Wrap worker invocations in order to return the `callbackId` along
+ * the result. This will allow to transform the worker invocations
+ * into promises in `RemoteSettingsWorker.sys.mjs`.
+ */
+self.onmessage = event => {
+ const { callbackId, method, args = [] } = event.data;
+ Agent[method](...args)
+ .then(result => {
+ self.postMessage({ callbackId, result });
+ })
+ .catch(error => {
+ console.log(`RemoteSettingsWorker error: ${error}`);
+ self.postMessage({ callbackId, error: "" + error });
+ });
+};
+
+let gPendingTransactions = new Set();
+
+/**
+ * Import the records into the Remote Settings Chrome IndexedDB.
+ *
+ * Note: This duplicates some logics from `kinto-offline-client.js`.
+ *
+ * @param {String} bucket
+ * @param {String} collection
+ * @param {Array<Object>} records
+ * @param {Number} timestamp
+ */
+async function importDumpIDB(bucket, collection, records, timestamp) {
+ // Open the DB. It will exist since if we are running this, it means
+ // we already tried to read the timestamp in `remote-settings.sys.mjs`
+ const db = await IDBHelpers.openIDB(false /* do not allow upgrades */);
+
+ // try...finally to ensure we always close the db.
+ try {
+ if (gShutdown) {
+ throw new Error("Can't import when we've started shutting down.");
+ }
+
+ // Each entry of the dump will be stored in the records store.
+ // They are indexed by `_cid`.
+ const cid = bucket + "/" + collection;
+ // We can just modify the items in-place, as we got them from SharedUtils.loadJSONDump().
+ records.forEach(item => {
+ item._cid = cid;
+ });
+ // Store the collection timestamp.
+ let { transaction, promise } = IDBHelpers.executeIDB(
+ db,
+ [IDB_RECORDS_STORE, IDB_TIMESTAMPS_STORE],
+ "readwrite",
+ ([recordsStore, timestampStore], rejectTransaction) => {
+ // Wipe before loading
+ recordsStore.delete(IDBKeyRange.bound([cid], [cid, []], false, true));
+ IDBHelpers.bulkOperationHelper(
+ recordsStore,
+ {
+ reject: rejectTransaction,
+ completion() {
+ timestampStore.put({ cid, value: timestamp });
+ },
+ },
+ "put",
+ records
+ );
+ }
+ );
+ gPendingTransactions.add(transaction);
+ promise = promise.finally(() => gPendingTransactions.delete(transaction));
+ await promise;
+ } finally {
+ // Close now that we're done.
+ db.close();
+ }
+}