diff options
Diffstat (limited to 'services/settings/RemoteSettingsWorker.js')
-rw-r--r-- | services/settings/RemoteSettingsWorker.js | 204 |
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(); + } +} |