--- a/services/common/blocklist-clients.js
+++ b/services/common/blocklist-clients.js
@@ -44,20 +44,20 @@ const PREF_BLOCKLIST_PINNING_BUCKET
const PREF_BLOCKLIST_PINNING_COLLECTION = "services.blocklist.pinning.collection";
const PREF_BLOCKLIST_PINNING_CHECKED_SECONDS = "services.blocklist.pinning.checked";
const PREF_BLOCKLIST_GFX_COLLECTION = "services.blocklist.gfx.collection";
const PREF_BLOCKLIST_GFX_CHECKED_SECONDS = "services.blocklist.gfx.checked";
const PREF_BLOCKLIST_ENFORCE_SIGNING = "services.blocklist.signing.enforced";
const INVALID_SIGNATURE = "Invalid content/signature";
-// FIXME: this was the default path in earlier versions of
+// This was the default path in earlier versions of
// FirefoxAdapter, so for backwards compatibility we maintain this
// filename, even though it isn't descriptive of who is using it.
-this.KINTO_STORAGE_PATH = "kinto.sqlite";
+const KINTO_STORAGE_PATH = "kinto.sqlite";
function mergeChanges(collection, localRecords, changes) {
const records = {};
// Local records by id.
localRecords.forEach((record) => records[record.id] = collection.cleanLocalFields(record));
// All existing records are replaced by the version from the server.
@@ -110,16 +110,47 @@ class BlocklistClient {
get filename() {
// Replace slash by OS specific path separator (eg. Windows)
const identifier = OS.Path.join(...this.identifier.split("/"));
return `${identifier}.json`;
}
/**
+ * Open the underlying Kinto collection, using the appropriate adapter and
+ * options. This acts as a context manager where the connection is closed
+ * once the specified `callback` has finished.
+ *
+ * @param {callback} function the async function to execute with the open SQlite connection.
+ * @param {Object} options additional advanced options.
+ * @param {string} options.bucket override bucket name of client (default: this.bucketName)
+ * @param {string} options.collection override collection name of client (default: this.collectionName)
+ * @param {string} options.path override default Sqlite path (default: kinto.sqlite)
+ * @param {string} options.hooks hooks to execute on synchronization (see Kinto.js docs)
+ */
+ async openCollection(callback, options = {}) {
+ const { bucket = this.bucketName, path = KINTO_STORAGE_PATH } = options;
+ if (!this._kinto) {
+ this._kinto = new Kinto({bucket, adapter: FirefoxAdapter});
+ }
+ let sqliteHandle;
+ try {
+ sqliteHandle = await FirefoxAdapter.openConnection({path});
+ const colOptions = Object.assign({adapterOptions: {sqliteHandle}}, options);
+ const {collection: collectionName = this.collectionName} = options;
+ const collection = this._kinto.collection(collectionName, colOptions);
+ return await callback(collection);
+ } finally {
+ if (sqliteHandle) {
+ await sqliteHandle.close();
+ }
+ }
+ }
+
+ /**
* Load the the JSON file distributed with the release for this blocklist.
*
* For Bug 1257565 this method will have to try to load the file from the profile,
* in order to leverage the updateJSONBlocklist() below, which writes a new
* dump each time the collection changes.
*/
async loadDumpFile() {
// Replace OS specific path separator by / for URI.
@@ -181,138 +212,123 @@ class BlocklistClient {
* @return {Promise} which rejects on sync or process failure.
*/
async maybeSync(lastModified, serverTime, options = {loadDump: true}) {
const {loadDump} = options;
const remote = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
const enforceCollectionSigning =
Services.prefs.getBoolPref(PREF_BLOCKLIST_ENFORCE_SIGNING);
- if (!this._kinto) {
- this._kinto = new Kinto({
- bucket: this.bucketName,
- adapter: FirefoxAdapter,
- });
- }
-
// if there is a signerName and collection signing is enforced, add a
// hook for incoming changes that validates the signature
- let hooks;
+ const colOptions = {};
if (this.signerName && enforceCollectionSigning) {
- hooks = {
+ colOptions.hooks = {
"incoming-changes": [(payload, collection) => {
return this.validateCollectionSignature(remote, payload, collection);
}]
}
}
- let sqliteHandle;
let reportStatus = null;
try {
- // Synchronize remote data into a local Sqlite DB.
- sqliteHandle = await FirefoxAdapter.openConnection({path: KINTO_STORAGE_PATH});
- const options = {
- hooks,
- adapterOptions: {sqliteHandle},
- };
- const collection = this._kinto.collection(this.collectionName, options);
-
- let collectionLastModified = await collection.db.getLastModified();
+ return await this.openCollection(async (collection) => {
+ // Synchronize remote data into a local Sqlite DB.
+ let collectionLastModified = await collection.db.getLastModified();
- // If there is no data currently in the collection, attempt to import
- // initial data from the application defaults.
- // This allows to avoid synchronizing the whole collection content on
- // cold start.
- if (!collectionLastModified && loadDump) {
- try {
- const initialData = await this.loadDumpFile();
- await collection.loadDump(initialData.data);
- collectionLastModified = await collection.db.getLastModified();
- } catch (e) {
- // Report but go-on.
- Cu.reportError(e);
+ // If there is no data currently in the collection, attempt to import
+ // initial data from the application defaults.
+ // This allows to avoid synchronizing the whole collection content on
+ // cold start.
+ if (!collectionLastModified && loadDump) {
+ try {
+ const initialData = await this.loadDumpFile();
+ await collection.loadDump(initialData.data);
+ collectionLastModified = await collection.db.getLastModified();
+ } catch (e) {
+ // Report but go-on.
+ Cu.reportError(e);
+ }
}
- }
+
+ // If the data is up to date, there's no need to sync. We still need
+ // to record the fact that a check happened.
+ if (lastModified <= collectionLastModified) {
+ this.updateLastCheck(serverTime);
+ reportStatus = UptakeTelemetry.STATUS.UP_TO_DATE;
+ return;
+ }
- // If the data is up to date, there's no need to sync. We still need
- // to record the fact that a check happened.
- if (lastModified <= collectionLastModified) {
- this.updateLastCheck(serverTime);
- reportStatus = UptakeTelemetry.STATUS.UP_TO_DATE;
- return;
- }
-
- // Fetch changes from server.
- try {
- // Server changes have priority during synchronization.
- const strategy = Kinto.syncStrategy.SERVER_WINS;
- const {ok} = await collection.sync({remote, strategy});
- if (!ok) {
- // Some synchronization conflicts occured.
- reportStatus = UptakeTelemetry.STATUS.CONFLICT_ERROR;
- throw new Error("Sync failed");
- }
- } catch (e) {
- if (e.message == INVALID_SIGNATURE) {
- // Signature verification failed during synchronzation.
- reportStatus = UptakeTelemetry.STATUS.SIGNATURE_ERROR;
- // if sync fails with a signature error, it's likely that our
- // local data has been modified in some way.
- // We will attempt to fix this by retrieving the whole
- // remote collection.
- const payload = await fetchRemoteCollection(remote, collection);
- try {
- await this.validateCollectionSignature(remote, payload, collection, {ignoreLocal: true});
- } catch (e) {
- reportStatus = UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR;
+ // Fetch changes from server.
+ try {
+ // Server changes have priority during synchronization.
+ const strategy = Kinto.syncStrategy.SERVER_WINS;
+ const {ok} = await collection.sync({remote, strategy});
+ if (!ok) {
+ // Some synchronization conflicts occured.
+ reportStatus = UptakeTelemetry.STATUS.CONFLICT_ERROR;
+ throw new Error("Sync failed");
+ }
+ } catch (e) {
+ if (e.message == INVALID_SIGNATURE) {
+ // Signature verification failed during synchronzation.
+ reportStatus = UptakeTelemetry.STATUS.SIGNATURE_ERROR;
+ // if sync fails with a signature error, it's likely that our
+ // local data has been modified in some way.
+ // We will attempt to fix this by retrieving the whole
+ // remote collection.
+ const payload = await fetchRemoteCollection(remote, collection);
+ try {
+ await this.validateCollectionSignature(remote, payload, collection, {ignoreLocal: true});
+ } catch (e) {
+ reportStatus = UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR;
+ throw e;
+ }
+ // if the signature is good (we haven't thrown), and the remote
+ // last_modified is newer than the local last_modified, replace the
+ // local data
+ const localLastModified = await collection.db.getLastModified();
+ if (payload.last_modified >= localLastModified) {
+ await collection.clear();
+ await collection.loadDump(payload.data);
+ }
+ } else {
+ // The sync has thrown, it can be a network or a general error.
+ if (/NetworkError/.test(e.message)) {
+ reportStatus = UptakeTelemetry.STATUS.NETWORK_ERROR;
+ } else if (/Backoff/.test(e.message)) {
+ reportStatus = UptakeTelemetry.STATUS.BACKOFF;
+ } else {
+ reportStatus = UptakeTelemetry.STATUS.SYNC_ERROR;
+ }
throw e;
}
- // if the signature is good (we haven't thrown), and the remote
- // last_modified is newer than the local last_modified, replace the
- // local data
- const localLastModified = await collection.db.getLastModified();
- if (payload.last_modified >= localLastModified) {
- await collection.clear();
- await collection.loadDump(payload.data);
- }
- } else {
- // The sync has thrown, it can be a network or a general error.
- if (/NetworkError/.test(e.message)) {
- reportStatus = UptakeTelemetry.STATUS.NETWORK_ERROR;
- } else if (/Backoff/.test(e.message)) {
- reportStatus = UptakeTelemetry.STATUS.BACKOFF;
- } else {
- reportStatus = UptakeTelemetry.STATUS.SYNC_ERROR;
- }
+ }
+ // Read local collection of records.
+ const {data} = await collection.list();
+
+ // Handle the obtained records (ie. apply locally).
+ try {
+ await this.processCallback(data);
+ } catch (e) {
+ reportStatus = UptakeTelemetry.STATUS.APPLY_ERROR;
throw e;
}
- }
- // Read local collection of records.
- const {data} = await collection.list();
- // Handle the obtained records (ie. apply locally).
- try {
- await this.processCallback(data);
- } catch (e) {
- reportStatus = UptakeTelemetry.STATUS.APPLY_ERROR;
- throw e;
- }
+ // Track last update.
+ this.updateLastCheck(serverTime);
- // Track last update.
- this.updateLastCheck(serverTime);
+ }, colOptions);
} catch (e) {
// No specific error was tracked, mark it as unknown.
if (reportStatus === null) {
reportStatus = UptakeTelemetry.STATUS.UNKNOWN_ERROR;
}
throw e;
} finally {
- if (sqliteHandle) {
- await sqliteHandle.close();
- }
// No error was reported, this is a success!
if (reportStatus === null) {
reportStatus = UptakeTelemetry.STATUS.SUCCESS;
}
// Report success/error status to Telemetry.
UptakeTelemetry.report(this.identifier, reportStatus);
}
}
--- a/services/common/tests/unit/test_blocklist_certificates.js
+++ b/services/common/tests/unit/test_blocklist_certificates.js
@@ -1,38 +1,19 @@
const { Constructor: CC } = Components;
Cu.import("resource://testing-common/httpd.js");
const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js", {});
-const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js", {});
-const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js", {});
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
let server;
-// set up what we need to make storage adapters
-let sqliteHandle;
-const KINTO_FILENAME = "kinto.sqlite";
-
-function do_get_kinto_collection(collectionName) {
- let config = {
- // Set the remote to be some server that will cause test failure when
- // hit since we should never hit the server directly, only via maybeSync()
- remote: "https://firefox.settings.services.mozilla.com/v1/",
- // Set up the adapter and bucket as normal
- adapter: FirefoxAdapter,
- adapterOptions: {sqliteHandle},
- bucket: "blocklists"
- };
- return new Kinto(config).collection(collectionName);
-}
-
// Some simple tests to demonstrate that the logic inside maybeSync works
// correctly and that simple kinto operations are working as expected. There
// are more tests for core Kinto.js (and its storage adapter) in the
// xpcshell tests under /services/common
add_task(async function test_something() {
const configPath = "/v1/";
const recordsPath = "/v1/buckets/blocklists/collections/certificates/records";
@@ -62,55 +43,62 @@ add_task(async function test_something()
}
}
server.registerPathHandler(configPath, handleResponse);
server.registerPathHandler(recordsPath, handleResponse);
// Test an empty db populates
await OneCRLBlocklistClient.maybeSync(2000, Date.now());
- sqliteHandle = await FirefoxAdapter.openConnection({path: KINTO_FILENAME});
- const collection = do_get_kinto_collection("certificates");
-
- // Open the collection, verify it's been populated:
- let list = await collection.list();
- // We know there will be initial values from the JSON dump.
- // (at least as many as in the dump shipped when this test was written).
- do_check_true(list.data.length >= 363);
+ await OneCRLBlocklistClient.openCollection(async (collection) => {
+ // Open the collection, verify it's been populated:
+ const list = await collection.list();
+ // We know there will be initial values from the JSON dump.
+ // (at least as many as in the dump shipped when this test was written).
+ do_check_true(list.data.length >= 363);
+ });
// No sync will be intented if maybeSync() is up-to-date.
Services.prefs.clearUserPref("services.settings.server");
Services.prefs.setIntPref("services.blocklist.onecrl.checked", 0);
// Use any last_modified older than highest shipped in JSON dump.
await OneCRLBlocklistClient.maybeSync(123456, Date.now());
// Last check value was updated.
do_check_neq(0, Services.prefs.getIntPref("services.blocklist.onecrl.checked"));
// Restore server pref.
Services.prefs.setCharPref("services.settings.server", dummyServerURL);
- // clear the collection, save a non-zero lastModified so we don't do
- // import of initial data when we sync again.
- await collection.clear();
- // a lastModified value of 1000 means we get a remote collection with a
- // single record
- await collection.db.saveLastModified(1000);
+
+ await OneCRLBlocklistClient.openCollection(async (collection) => {
+ // clear the collection, save a non-zero lastModified so we don't do
+ // import of initial data when we sync again.
+ await collection.clear();
+ // a lastModified value of 1000 means we get a remote collection with a
+ // single record
+ await collection.db.saveLastModified(1000);
+ });
+
await OneCRLBlocklistClient.maybeSync(2000, Date.now());
- // Open the collection, verify it's been updated:
- // Our test data now has two records; both should be in the local collection
- list = await collection.list();
- do_check_eq(list.data.length, 1);
+ await OneCRLBlocklistClient.openCollection(async (collection) => {
+ // Open the collection, verify it's been updated:
+ // Our test data now has two records; both should be in the local collection
+ const list = await collection.list();
+ do_check_eq(list.data.length, 1);
+ });
// Test the db is updated when we call again with a later lastModified value
await OneCRLBlocklistClient.maybeSync(4000, Date.now());
- // Open the collection, verify it's been updated:
- // Our test data now has two records; both should be in the local collection
- list = await collection.list();
- do_check_eq(list.data.length, 3);
+ await OneCRLBlocklistClient.openCollection(async (collection) => {
+ // Open the collection, verify it's been updated:
+ // Our test data now has two records; both should be in the local collection
+ const list = await collection.list();
+ do_check_eq(list.data.length, 3);
+ });
// Try to maybeSync with the current lastModified value - no connection
// should be attempted.
// Clear the kinto base pref so any connections will cause a test failure
Services.prefs.clearUserPref("services.settings.server");
await OneCRLBlocklistClient.maybeSync(4000, Date.now());
// Try again with a lastModified value at some point in the past
@@ -138,17 +126,16 @@ function run_test() {
// Set up an HTTP Server
server = new HttpServer();
server.start(-1);
run_next_test();
do_register_cleanup(function() {
server.stop(() => { });
- return sqliteHandle.close();
});
}
// get a response for a given request from sample data
function getSampleResponse(req, port) {
const responses = {
"OPTIONS": {
"sampleHeaders": [
--- a/services/common/tests/unit/test_blocklist_clients.js
+++ b/services/common/tests/unit/test_blocklist_clients.js
@@ -1,66 +1,46 @@
const { Constructor: CC } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://testing-common/httpd.js");
Cu.import("resource://gre/modules/Timer.jsm");
const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
-const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js", {});
-const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js", {});
const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js", {});
const { UptakeTelemetry } = Cu.import("resource://services-common/uptake-telemetry.js", {});
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
-const kintoFilename = "kinto.sqlite";
const gBlocklistClients = [
{client: BlocklistClients.AddonBlocklistClient, testData: ["i808", "i720", "i539"]},
{client: BlocklistClients.PluginBlocklistClient, testData: ["p1044", "p32", "p28"]},
{client: BlocklistClients.GfxBlocklistClient, testData: ["g204", "g200", "g36"]},
];
let server;
-function kintoCollection(collectionName, sqliteHandle) {
- const config = {
- // Set the remote to be some server that will cause test failure when
- // hit since we should never hit the server directly, only via maybeSync()
- remote: "https://firefox.settings.services.mozilla.com/v1/",
- adapter: FirefoxAdapter,
- adapterOptions: {sqliteHandle},
- bucket: "blocklists"
- };
- return new Kinto(config).collection(collectionName);
-}
-
async function readJSON(filepath) {
const binaryData = await OS.File.read(filepath);
const textData = (new TextDecoder()).decode(binaryData);
return Promise.resolve(JSON.parse(textData));
}
async function clear_state() {
for (let {client} of gBlocklistClients) {
// Remove last server times.
Services.prefs.clearUserPref(client.lastCheckTimePref);
// Clear local DB.
- let sqliteHandle;
- try {
- sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
- const collection = kintoCollection(client.collectionName, sqliteHandle);
+ await client.openCollection(async (collection) => {
await collection.clear();
- } finally {
- await sqliteHandle.close();
- }
+ });
// Remove JSON dumps folders in profile dir.
const dumpFile = OS.Path.join(OS.Constants.Path.profileDir, client.filename);
const folder = OS.Path.dirname(dumpFile);
await OS.File.removeDir(folder, { ignoreAbsent: true });
}
}
@@ -115,53 +95,49 @@ function run_test() {
}
add_task(async function test_initial_dump_is_loaded_as_synced_when_collection_is_empty() {
for (let {client} of gBlocklistClients) {
// Test an empty db populates, but don't reach server (specified timestamp <= dump).
await client.maybeSync(1, Date.now());
// Open the collection, verify the loaded data has status to synced:
- const sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
- const collection = kintoCollection(client.collectionName, sqliteHandle);
- const list = await collection.list();
- equal(list.data[0]._status, "synced");
- await sqliteHandle.close();
+ await client.openCollection(async (collection) => {
+ const list = await collection.list();
+ equal(list.data[0]._status, "synced");
+ });
}
});
add_task(clear_state);
add_task(async function test_records_obtained_from_server_are_stored_in_db() {
for (let {client} of gBlocklistClients) {
// Test an empty db populates
await client.maybeSync(2000, Date.now(), {loadDump: false});
// Open the collection, verify it's been populated:
// Our test data has a single record; it should be in the local collection
- const sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
- let collection = kintoCollection(client.collectionName, sqliteHandle);
- let list = await collection.list();
- equal(list.data.length, 1);
- await sqliteHandle.close();
+ await client.openCollection(async (collection) => {
+ const list = await collection.list();
+ equal(list.data.length, 1);
+ });
}
});
add_task(clear_state);
add_task(async function test_records_changes_are_overwritten_by_server_changes() {
const {client} = gBlocklistClients[0];
// Create some local conflicting data, and make sure it syncs without error.
- const sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
- const collection = kintoCollection(client.collectionName, sqliteHandle);
- await collection.create({
- "versionRange": [],
- "id": "9d500963-d80e-3a91-6e74-66f3811b99cc"
- }, { useRecordId: true });
- await sqliteHandle.close();
-
+ await client.openCollection(async (collection) => {
+ await collection.create({
+ "versionRange": [],
+ "id": "9d500963-d80e-3a91-6e74-66f3811b99cc"
+ }, { useRecordId: true });
+ });
await client.maybeSync(2000, Date.now(), {loadDump: false});
});
add_task(clear_state);
add_task(async function test_list_is_written_to_file_in_profile() {
for (let {client, testData} of gBlocklistClients) {
const filePath = OS.Path.join(OS.Constants.Path.profileDir, client.filename);
const profFile = new FileUtils.File(filePath);
@@ -279,45 +255,44 @@ add_task(async function test_telemetry_r
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
});
add_task(clear_state);
add_task(async function test_telemetry_reports_if_sync_fails() {
const {client} = gBlocklistClients[0];
const serverTime = Date.now();
- const sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
- const collection = kintoCollection(client.collectionName, sqliteHandle);
- await collection.db.saveLastModified(9999);
- await sqliteHandle.close();
+ await client.openCollection(async (collection) => {
+ await collection.db.saveLastModified(9999);
+ });
const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
try {
await client.maybeSync(10000, serverTime);
} catch (e) {}
const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
const expectedIncrements = {[UptakeTelemetry.STATUS.SYNC_ERROR]: 1};
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
});
add_task(clear_state);
add_task(async function test_telemetry_reports_unknown_errors() {
const {client} = gBlocklistClients[0];
const serverTime = Date.now();
- const backup = FirefoxAdapter.openConnection;
- FirefoxAdapter.openConnection = () => { throw new Error("Internal"); };
+ const backup = client.openCollection;
+ client.openCollection = () => { throw new Error("Internal"); };
const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
try {
await client.maybeSync(2000, serverTime);
} catch (e) {}
- FirefoxAdapter.openConnection = backup;
+ client.openCollection = backup;
const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
const expectedIncrements = {[UptakeTelemetry.STATUS.UNKNOWN_ERROR]: 1};
checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
});
add_task(clear_state);
// get a response for a given request from sample data
function getSampleResponse(req, port) {
--- a/services/common/tests/unit/test_blocklist_pinning.js
+++ b/services/common/tests/unit/test_blocklist_pinning.js
@@ -1,24 +1,17 @@
"use strict"
const { Constructor: CC } = Components;
Cu.import("resource://testing-common/httpd.js");
-const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js", {});
-const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js", {});
-
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream", "setInputStream");
-const PREF_BLOCKLIST_PINNING_COLLECTION = "services.blocklist.pinning.collection";
-const COLLECTION_NAME = "pins";
-const KINTO_STORAGE_PATH = "kinto.sqlite";
-
// First, we need to setup appInfo or we can't do version checks
var id = "xpcshell@tests.mozilla.org";
var appName = "XPCShell";
var version = "1";
var platformVersion = "1.9.2";
Cu.import("resource://testing-common/AppInfo.jsm", this);
updateAppInfo({
@@ -26,40 +19,19 @@ updateAppInfo({
ID: id,
version,
platformVersion: platformVersion ? platformVersion : "1.0",
crashReporter: true,
});
let server;
-
-function do_get_kinto_collection(connection, collectionName) {
- let config = {
- // Set the remote to be some server that will cause test failure when
- // hit since we should never hit the server directly (any non-local
- // request causes failure in tests), only via maybeSync()
- remote: "https://firefox.settings.services.mozilla.com/v1/",
- // Set up the adapter and bucket as normal
- adapter: FirefoxAdapter,
- adapterOptions: {sqliteHandle: connection},
- bucket: "pinning"
- };
- let kintoClient = new Kinto(config);
- return kintoClient.collection(collectionName);
-}
-
// Some simple tests to demonstrate that the core preload sync operations work
// correctly and that simple kinto operations are working as expected.
add_task(async function test_something() {
- // set the collection name explicitly - since there will be version
- // specific collection names in prefs
- Services.prefs.setCharPref(PREF_BLOCKLIST_PINNING_COLLECTION,
- COLLECTION_NAME);
-
const { PinningPreloadClient } = Cu.import("resource://services-common/blocklist-clients.js", {});
const configPath = "/v1/";
const recordsPath = "/v1/buckets/pinning/collections/pins/records";
Services.prefs.setCharPref("services.settings.server",
`http://localhost:${server.identity.primaryPort}/v1`);
@@ -101,37 +73,36 @@ add_task(async function test_something()
ok(!sss.isSecureURI(sss.HEADER_HSTS,
Services.io.newURI("https://four.example.com"), 0));
ok(!sss.isSecureURI(sss.HEADER_HSTS,
Services.io.newURI("https://five.example.com"), 0));
// Test an empty db populates
await PinningPreloadClient.maybeSync(2000, Date.now());
- let connection = await FirefoxAdapter.openConnection({path: KINTO_STORAGE_PATH});
-
// Open the collection, verify it's been populated:
// Our test data has a single record; it should be in the local collection
- let collection = do_get_kinto_collection(connection, COLLECTION_NAME);
- let list = await collection.list();
- do_check_eq(list.data.length, 1);
+ await PinningPreloadClient.openCollection(async (collection) => {
+ const list = await collection.list();
+ do_check_eq(list.data.length, 1);
+ });
// check that a pin exists for one.example.com
ok(sss.isSecureURI(sss.HEADER_HPKP,
Services.io.newURI("https://one.example.com"), 0));
// Test the db is updated when we call again with a later lastModified value
await PinningPreloadClient.maybeSync(4000, Date.now());
// Open the collection, verify it's been updated:
// Our data now has four new records; all should be in the local collection
- collection = do_get_kinto_collection(connection, COLLECTION_NAME);
- list = await collection.list();
- do_check_eq(list.data.length, 5);
- await connection.close();
+ await PinningPreloadClient.openCollection(async (collection) => {
+ const list = await collection.list();
+ do_check_eq(list.data.length, 5);
+ });
// check that a pin exists for two.example.com and three.example.com
ok(sss.isSecureURI(sss.HEADER_HPKP,
Services.io.newURI("https://two.example.com"), 0));
ok(sss.isSecureURI(sss.HEADER_HPKP,
Services.io.newURI("https://three.example.com"), 0));
// check that a pin does not exist for four.example.com - it's in the
--- a/services/common/tests/unit/test_blocklist_signatures.js
+++ b/services/common/tests/unit/test_blocklist_signatures.js
@@ -1,32 +1,26 @@
"use strict";
Cu.import("resource://services-common/blocklist-updater.js");
Cu.import("resource://testing-common/httpd.js");
-const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js", {});
-const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js", {});
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js", {});
const { UptakeTelemetry } = Cu.import("resource://services-common/uptake-telemetry.js", {});
let server;
-const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket";
const PREF_BLOCKLIST_ENFORCE_SIGNING = "services.blocklist.signing.enforced";
-const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection";
const PREF_SETTINGS_SERVER = "services.settings.server";
const PREF_SIGNATURE_ROOT = "security.content.signature.root_hash";
// Telemetry reports.
const TELEMETRY_HISTOGRAM_KEY = OneCRLBlocklistClient.identifier;
-const kintoFilename = "kinto.sqlite";
-
const CERT_DIR = "test_blocklist_signatures/";
const CHAIN_FILES =
["collection_signing_ee.pem",
"collection_signing_int.pem",
"collection_signing_root.pem"];
function getFileData(file) {
const stream = Cc["@mozilla.org/network/file-input-stream;1"]
@@ -55,39 +49,21 @@ function getCertChain() {
const chain = [];
for (let file of CHAIN_FILES) {
chain.push(getFileData(do_get_file(CERT_DIR + file)));
}
return chain.join("\n");
}
async function checkRecordCount(count) {
- // open the collection manually
- const base = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
- const bucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET);
- const collectionName =
- Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_COLLECTION);
-
- const sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
- const config = {
- remote: base,
- bucket,
- adapter: FirefoxAdapter,
- adapterOptions: {sqliteHandle},
- };
-
- const db = new Kinto(config);
- const collection = db.collection(collectionName);
-
- // Check we have the expected number of records
- let records = await collection.list();
- do_check_eq(count, records.data.length);
-
- // Close the collection so the test can exit cleanly
- await sqliteHandle.close();
+ await OneCRLBlocklistClient.openCollection(async (collection) => {
+ // Check we have the expected number of records
+ const records = await collection.list();
+ do_check_eq(count, records.data.length);
+ });
}
// Check to ensure maybeSync is called with correct values when a changes
// document contains information on when a collection was last modified
add_task(async function test_check_signatures() {
const port = server.identity.primaryPort;
// a response to give the client when the cert chain is expected
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -52,17 +52,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
CollectionKeyManager: "resource://services-sync/record.js",
CommonUtils: "resource://services-common/utils.js",
CryptoUtils: "resource://services-crypto/utils.js",
fxAccounts: "resource://gre/modules/FxAccounts.jsm",
KintoHttpClient: "resource://services-common/kinto-http-client.js",
Kinto: "resource://services-common/kinto-offline-client.js",
FirefoxAdapter: "resource://services-common/kinto-storage-adapter.js",
Observers: "resource://services-common/observers.js",
- Sqlite: "resource://gre/modules/Sqlite.jsm",
Utils: "resource://services-sync/util.js",
});
XPCOMUtils.defineLazyPreferenceGetter(this, "prefPermitsStorageSync",
STORAGE_SYNC_ENABLED_PREF, true);
XPCOMUtils.defineLazyPreferenceGetter(this, "prefStorageSyncServerURL",
STORAGE_SYNC_SERVER_URL_PREF,
KINTO_DEFAULT_SERVER_URL);
@@ -329,40 +328,35 @@ global.KeyRingEncryptionRemoteTransforme
* This centralizes the use of the Sqlite database, to which there is
* only one connection which is shared by all threads.
*
* Fields in the object returned by this Promise:
*
* - connection: a Sqlite connection. Meant for internal use only.
* - kinto: a KintoBase object, suitable for using in Firefox. All
* collections in this database will use the same Sqlite connection.
+ * @returns {Promise<Object>}
*/
-const storageSyncInit = (async function() {
- const path = "storage-sync.sqlite";
- const opts = {path, sharedMemoryCache: false};
- const connection = await Sqlite.openConnection(opts);
- await FirefoxAdapter._init(connection);
- return {
- connection,
- kinto: new Kinto({
- adapter: FirefoxAdapter,
- adapterOptions: {sqliteHandle: connection},
- timeout: KINTO_REQUEST_TIMEOUT,
- }),
- };
-})();
+async function storageSyncInit() {
+ // Memoize the result to share the connection.
+ if (storageSyncInit.result === undefined) {
+ const path = "storage-sync.sqlite";
+ const connection = await FirefoxAdapter.openConnection({path});
+ storageSyncInit.result = {
+ connection,
+ kinto: new Kinto({
+ adapter: FirefoxAdapter,
+ adapterOptions: {sqliteHandle: connection},
+ timeout: KINTO_REQUEST_TIMEOUT,
+ }),
+ };
+ }
+ return storageSyncInit.result;
+}
-AsyncShutdown.profileBeforeChange.addBlocker(
- "ExtensionStorageSync: close Sqlite handle",
- async function() {
- const ret = await storageSyncInit;
- const {connection} = ret;
- await connection.close();
- }
-);
// Kinto record IDs have two condtions:
//
// - They must contain only ASCII alphanumerics plus - and _. To fix
// this, we encode all non-letters using _C_, where C is the
// percent-encoded character, so space becomes _20_
// and underscore becomes _5F_.
//
// - They must start with an ASCII letter. To ensure this, we prefix
@@ -427,17 +421,17 @@ const cryptoCollectionIdSchema = {
*/
class CryptoCollection {
constructor(fxaService) {
this._fxaService = fxaService;
}
async getCollection() {
throwIfNoFxA(this._fxaService, "tried to access cryptoCollection");
- const {kinto} = await storageSyncInit;
+ const {kinto} = await storageSyncInit();
return kinto.collection(STORAGE_SYNC_CRYPTO_COLLECTION_NAME, {
idSchema: cryptoCollectionIdSchema,
remoteTransformers: [new KeyRingEncryptionRemoteTransformer(this._fxaService)],
});
}
/**
* Generate a new salt for use in hashing extension and record
@@ -690,17 +684,17 @@ function cleanUpForContext(extension, co
* @param {Context} context
* The context for this extension. The Collection
* will shut down automatically when all contexts
* close.
* @returns {Promise<Collection>}
*/
const openCollection = async function(cryptoCollection, extension, context) {
let collectionId = extension.id;
- const {kinto} = await storageSyncInit;
+ const {kinto} = await storageSyncInit();
const remoteTransformers = [new CollectionKeyEncryptionRemoteTransformer(cryptoCollection, extension.id)];
const coll = kinto.collection(collectionId, {
idSchema: storageSyncIdSchema,
remoteTransformers,
});
return coll;
};
@@ -1225,17 +1219,27 @@ class ExtensionStorageSync {
}
addOnChangedListener(extension, listener, context) {
let listeners = this.listeners.get(extension) || new Set();
listeners.add(listener);
this.listeners.set(extension, listeners);
// Force opening the collection so that we will sync for this extension.
- return this.getCollection(extension, context);
+ // This happens asynchronously, even though the surface API is synchronous.
+ return this.getCollection(extension, context)
+ .catch((e) => {
+ // We can ignore failures that happen during shutdown here. First, we
+ // can't report in any way. And second, a failure to open the collection
+ // does not matter, because there won't be any message to listen to.
+ // See Bug 1395215.
+ if (!(/Kinto storage adapter connection closing/.test(e.message))) {
+ throw e;
+ }
+ });
}
removeOnChangedListener(extension, listener) {
let listeners = this.listeners.get(extension);
listeners.delete(listener);
if (listeners.size == 0) {
this.listeners.delete(extension);
}