mirror of
https://github.com/roytam1/UXP.git
synced 2026-05-26 14:54:25 +00:00
Remove kinto client, Firefox kinto storage adapter, blocklist update client and integration with sync, OneCRL and the custom time check for derives system time.
This commit is contained in:
@@ -315,29 +315,26 @@ var AboutNetAndCertErrorListener = {
|
||||
case MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE:
|
||||
case MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE:
|
||||
|
||||
// use blocklist stats if available
|
||||
if (Services.prefs.getPrefType(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS)) {
|
||||
let difference = Services.prefs.getIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS);
|
||||
let appBuildId = Services.appinfo.appBuildID;
|
||||
let year = parseInt(appBuildID.substr(0, 4), 10);
|
||||
let month = parseInt(appBuildID.substr(4, 2), 10) - 1;
|
||||
let day = parseInt(appBuildID.substr(6, 2), 10);
|
||||
let buildDate = new Date(year, month, day);
|
||||
let systemDate = new Date();
|
||||
|
||||
// if the difference is more than a day
|
||||
if (buildDate > systemDate) {
|
||||
let formatter = new Intl.DateTimeFormat();
|
||||
|
||||
// if the difference is more than a day
|
||||
if (Math.abs(difference) > 60 * 60 * 24) {
|
||||
let formatter = new Intl.DateTimeFormat();
|
||||
let systemDate = formatter.format(new Date());
|
||||
// negative difference means local time is behind server time
|
||||
let actualDate = formatter.format(new Date(Date.now() - difference * 1000));
|
||||
content.document.getElementById("wrongSystemTime_URL")
|
||||
.textContent = content.document.location.hostname;
|
||||
content.document.getElementById("wrongSystemTime_systemDate")
|
||||
.textContent = formatter.format(systemDate);
|
||||
|
||||
content.document.getElementById("wrongSystemTime_URL")
|
||||
.textContent = content.document.location.hostname;
|
||||
content.document.getElementById("wrongSystemTime_systemDate")
|
||||
.textContent = systemDate;
|
||||
content.document.getElementById("wrongSystemTime_actualDate")
|
||||
.textContent = actualDate;
|
||||
|
||||
content.document.getElementById("errorShortDesc")
|
||||
.style.display = "none";
|
||||
content.document.getElementById("wrongSystemTimePanel")
|
||||
.style.display = "block";
|
||||
}
|
||||
content.document.getElementById("errorShortDesc")
|
||||
.style.display = "none";
|
||||
content.document.getElementById("wrongSystemTimePanel")
|
||||
.style.display = "block";
|
||||
}
|
||||
learnMoreLink.href = baseURL + "time-errors";
|
||||
break;
|
||||
|
||||
@@ -199,7 +199,7 @@ was trying to connect. -->
|
||||
|
||||
<!-- LOCALIZATION NOTE (certerror.wrongSystemTime) - The <span id='..' /> tags will be injected with actual values,
|
||||
please leave them unchanged. -->
|
||||
<!ENTITY certerror.wrongSystemTime "<p>A secure connection to <span id='wrongSystemTime_URL'/> isn’t possible because your clock appears to show the wrong time.</p> <p>Your computer thinks it is <span id='wrongSystemTime_systemDate'/>, when it should be <span id='wrongSystemTime_actualDate'/>. To fix this problem, change your date and time settings to match the correct time.</p>">
|
||||
<!ENTITY certerror.wrongSystemTime "<p>&brandShortName; did not connect to <span id='wrongSystemTimeWithoutReference_URL'/> because your computer’s clock appears to show the wrong time and this is preventing a secure connection.</p> <p>Your computer is set to <span id='wrongSystemTimeWithoutReference_systemDate'/>. To fix this problem, change your date and time settings to match the correct time.</p>">
|
||||
|
||||
<!ENTITY certerror.pagetitle1 "Insecure Connection">
|
||||
<!ENTITY certerror.whatShouldIDo.badStsCertExplanation "This site uses HTTP
|
||||
|
||||
@@ -2175,9 +2175,6 @@ pref("security.cert_pinning.process_headers_from_non_builtin_roots", false);
|
||||
// their protocol with the inner URI of the view-source URI
|
||||
pref("security.view-source.reachable-from-inner-protocol", false);
|
||||
|
||||
// Services security settings
|
||||
pref("services.settings.server", "https://firefox.settings.services.mozilla.com/v1");
|
||||
|
||||
// Blocklist preferences
|
||||
pref("extensions.blocklist.enabled", true);
|
||||
// OneCRL freshness checking depends on this value, so if you change it,
|
||||
@@ -2192,28 +2189,6 @@ pref("extensions.blocklist.itemURL", "https://blocklist.addons.mozilla.org/%LOCA
|
||||
// Controls what level the blocklist switches from warning about items to forcibly
|
||||
// blocking them.
|
||||
pref("extensions.blocklist.level", 2);
|
||||
// Blocklist via settings server (Kinto)
|
||||
pref("services.blocklist.changes.path", "/buckets/monitor/collections/changes/records");
|
||||
pref("services.blocklist.bucket", "blocklists");
|
||||
pref("services.blocklist.onecrl.collection", "certificates");
|
||||
pref("services.blocklist.onecrl.checked", 0);
|
||||
pref("services.blocklist.addons.collection", "addons");
|
||||
pref("services.blocklist.addons.checked", 0);
|
||||
pref("services.blocklist.plugins.collection", "plugins");
|
||||
pref("services.blocklist.plugins.checked", 0);
|
||||
pref("services.blocklist.gfx.collection", "gfx");
|
||||
pref("services.blocklist.gfx.checked", 0);
|
||||
|
||||
// Controls whether signing should be enforced on signature-capable blocklist
|
||||
// collections.
|
||||
pref("services.blocklist.signing.enforced", true);
|
||||
|
||||
// Enable blocklists via the services settings mechanism
|
||||
pref("services.blocklist.update_enabled", true);
|
||||
|
||||
// Enable certificate blocklist updates via services settings
|
||||
pref("security.onecrl.via.amo", false);
|
||||
|
||||
|
||||
// Modifier key prefs: default to Windows settings,
|
||||
// menu access key = alt, accelerator key = control.
|
||||
|
||||
@@ -34,14 +34,11 @@ using namespace mozilla::pkix;
|
||||
#define PREF_BACKGROUND_UPDATE_TIMER "app.update.lastUpdateTime.blocklist-background-update-timer"
|
||||
#define PREF_BLOCKLIST_ONECRL_CHECKED "services.blocklist.onecrl.checked"
|
||||
#define PREF_MAX_STALENESS_IN_SECONDS "security.onecrl.maximum_staleness_in_seconds"
|
||||
#define PREF_ONECRL_VIA_AMO "security.onecrl.via.amo"
|
||||
|
||||
static LazyLogModule gCertBlockPRLog("CertBlock");
|
||||
|
||||
uint32_t CertBlocklist::sLastBlocklistUpdate = 0U;
|
||||
uint32_t CertBlocklist::sLastKintoUpdate = 0U;
|
||||
uint32_t CertBlocklist::sMaxStaleness = 0U;
|
||||
bool CertBlocklist::sUseAMO = true;
|
||||
|
||||
CertBlocklistItem::CertBlocklistItem(const uint8_t* DNData,
|
||||
size_t DNLength,
|
||||
@@ -142,9 +139,6 @@ CertBlocklist::~CertBlocklist()
|
||||
Preferences::UnregisterCallback(CertBlocklist::PreferenceChanged,
|
||||
PREF_MAX_STALENESS_IN_SECONDS,
|
||||
this);
|
||||
Preferences::UnregisterCallback(CertBlocklist::PreferenceChanged,
|
||||
PREF_ONECRL_VIA_AMO,
|
||||
this);
|
||||
Preferences::UnregisterCallback(CertBlocklist::PreferenceChanged,
|
||||
PREF_BLOCKLIST_ONECRL_CHECKED,
|
||||
this);
|
||||
@@ -176,12 +170,6 @@ CertBlocklist::Init()
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
rv = Preferences::RegisterCallbackAndCall(CertBlocklist::PreferenceChanged,
|
||||
PREF_ONECRL_VIA_AMO,
|
||||
this);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
rv = Preferences::RegisterCallbackAndCall(CertBlocklist::PreferenceChanged,
|
||||
PREF_BLOCKLIST_ONECRL_CHECKED,
|
||||
this);
|
||||
@@ -628,10 +616,10 @@ CertBlocklist::IsBlocklistFresh(bool* _retval)
|
||||
*_retval = false;
|
||||
|
||||
uint32_t now = uint32_t(PR_Now() / PR_USEC_PER_SEC);
|
||||
uint32_t lastUpdate = sUseAMO ? sLastBlocklistUpdate : sLastKintoUpdate;
|
||||
uint32_t lastUpdate = sLastBlocklistUpdate;
|
||||
MOZ_LOG(gCertBlockPRLog, LogLevel::Warning,
|
||||
("CertBlocklist::IsBlocklistFresh using AMO? %i lastUpdate is %i",
|
||||
sUseAMO, lastUpdate));
|
||||
("CertBlocklist::IsBlocklistFresh lastUpdate is %i",
|
||||
lastUpdate));
|
||||
|
||||
if (now > lastUpdate) {
|
||||
int64_t interval = now - lastUpdate;
|
||||
@@ -659,13 +647,8 @@ CertBlocklist::PreferenceChanged(const char* aPref, void* aClosure)
|
||||
if (strcmp(aPref, PREF_BACKGROUND_UPDATE_TIMER) == 0) {
|
||||
sLastBlocklistUpdate = Preferences::GetUint(PREF_BACKGROUND_UPDATE_TIMER,
|
||||
uint32_t(0));
|
||||
} else if (strcmp(aPref, PREF_BLOCKLIST_ONECRL_CHECKED) == 0) {
|
||||
sLastKintoUpdate = Preferences::GetUint(PREF_BLOCKLIST_ONECRL_CHECKED,
|
||||
uint32_t(0));
|
||||
} else if (strcmp(aPref, PREF_MAX_STALENESS_IN_SECONDS) == 0) {
|
||||
sMaxStaleness = Preferences::GetUint(PREF_MAX_STALENESS_IN_SECONDS,
|
||||
uint32_t(0));
|
||||
} else if (strcmp(aPref, PREF_ONECRL_VIA_AMO) == 0) {
|
||||
sUseAMO = Preferences::GetBool(PREF_ONECRL_VIA_AMO, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,9 +80,7 @@ private:
|
||||
protected:
|
||||
static void PreferenceChanged(const char* aPref, void* aClosure);
|
||||
static uint32_t sLastBlocklistUpdate;
|
||||
static uint32_t sLastKintoUpdate;
|
||||
static uint32_t sMaxStaleness;
|
||||
static bool sUseAMO;
|
||||
virtual ~CertBlocklist();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["AddonBlocklistClient",
|
||||
"GfxBlocklistClient",
|
||||
"OneCRLBlocklistClient",
|
||||
"PluginBlocklistClient",
|
||||
"FILENAME_ADDONS_JSON",
|
||||
"FILENAME_GFX_JSON",
|
||||
"FILENAME_PLUGINS_JSON"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
const { Task } = Cu.import("resource://gre/modules/Task.jsm");
|
||||
const { OS } = Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.importGlobalProperties(["fetch"]);
|
||||
|
||||
const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
|
||||
const { KintoHttpClient } = Cu.import("resource://services-common/kinto-http-client.js");
|
||||
const { CanonicalJSON } = Components.utils.import("resource://gre/modules/CanonicalJSON.jsm");
|
||||
|
||||
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||
const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket";
|
||||
const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection";
|
||||
const PREF_BLOCKLIST_ONECRL_CHECKED_SECONDS = "services.blocklist.onecrl.checked";
|
||||
const PREF_BLOCKLIST_ADDONS_COLLECTION = "services.blocklist.addons.collection";
|
||||
const PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS = "services.blocklist.addons.checked";
|
||||
const PREF_BLOCKLIST_PLUGINS_COLLECTION = "services.blocklist.plugins.collection";
|
||||
const PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS = "services.blocklist.plugins.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";
|
||||
|
||||
this.FILENAME_ADDONS_JSON = "blocklist-addons.json";
|
||||
this.FILENAME_GFX_JSON = "blocklist-gfx.json";
|
||||
this.FILENAME_PLUGINS_JSON = "blocklist-plugins.json";
|
||||
|
||||
function mergeChanges(localRecords, changes) {
|
||||
// Kinto.js adds attributes to local records that aren't present on server.
|
||||
// (e.g. _status)
|
||||
const stripPrivateProps = (obj) => {
|
||||
return Object.keys(obj).reduce((current, key) => {
|
||||
if (!key.startsWith("_")) {
|
||||
current[key] = obj[key];
|
||||
}
|
||||
return current;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const records = {};
|
||||
// Local records by id.
|
||||
localRecords.forEach((record) => records[record.id] = stripPrivateProps(record));
|
||||
// All existing records are replaced by the version from the server.
|
||||
changes.forEach((record) => records[record.id] = record);
|
||||
|
||||
return Object.values(records)
|
||||
// Filter out deleted records.
|
||||
.filter((record) => record.deleted != true)
|
||||
// Sort list by record id.
|
||||
.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
||||
}
|
||||
|
||||
|
||||
function fetchCollectionMetadata(collection) {
|
||||
const client = new KintoHttpClient(collection.api.remote);
|
||||
return client.bucket(collection.bucket).collection(collection.name).getData()
|
||||
.then(result => {
|
||||
return result.signature;
|
||||
});
|
||||
}
|
||||
|
||||
function fetchRemoteCollection(collection) {
|
||||
const client = new KintoHttpClient(collection.api.remote);
|
||||
return client.bucket(collection.bucket)
|
||||
.collection(collection.name)
|
||||
.listRecords({sort: "id"});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to instantiate a Kinto client based on preferences for remote server
|
||||
* URL and bucket name. It uses the `FirefoxAdapter` which relies on SQLite to
|
||||
* persist the local DB.
|
||||
*/
|
||||
function kintoClient() {
|
||||
let base = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
|
||||
let bucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET);
|
||||
|
||||
let Kinto = loadKinto();
|
||||
|
||||
let FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
|
||||
|
||||
let config = {
|
||||
remote: base,
|
||||
bucket: bucket,
|
||||
adapter: FirefoxAdapter,
|
||||
};
|
||||
|
||||
return new Kinto(config);
|
||||
}
|
||||
|
||||
|
||||
class BlocklistClient {
|
||||
|
||||
constructor(collectionName, lastCheckTimePref, processCallback, signerName) {
|
||||
this.collectionName = collectionName;
|
||||
this.lastCheckTimePref = lastCheckTimePref;
|
||||
this.processCallback = processCallback;
|
||||
this.signerName = signerName;
|
||||
}
|
||||
|
||||
validateCollectionSignature(payload, collection, ignoreLocal) {
|
||||
return Task.spawn((function* () {
|
||||
// this is a content-signature field from an autograph response.
|
||||
const {x5u, signature} = yield fetchCollectionMetadata(collection);
|
||||
const certChain = yield fetch(x5u).then((res) => res.text());
|
||||
|
||||
const verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
|
||||
.createInstance(Ci.nsIContentSignatureVerifier);
|
||||
|
||||
let toSerialize;
|
||||
if (ignoreLocal) {
|
||||
toSerialize = {
|
||||
last_modified: `${payload.last_modified}`,
|
||||
data: payload.data
|
||||
};
|
||||
} else {
|
||||
const localRecords = (yield collection.list()).data;
|
||||
const records = mergeChanges(localRecords, payload.changes);
|
||||
toSerialize = {
|
||||
last_modified: `${payload.lastModified}`,
|
||||
data: records
|
||||
};
|
||||
}
|
||||
|
||||
const serialized = CanonicalJSON.stringify(toSerialize);
|
||||
|
||||
if (verifier.verifyContentSignature(serialized, "p384ecdsa=" + signature,
|
||||
certChain,
|
||||
this.signerName)) {
|
||||
// In case the hash is valid, apply the changes locally.
|
||||
return payload;
|
||||
}
|
||||
throw new Error(INVALID_SIGNATURE);
|
||||
}).bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize from Kinto server, if necessary.
|
||||
*
|
||||
* @param {int} lastModified the lastModified date (on the server) for
|
||||
the remote collection.
|
||||
* @param {Date} serverTime the current date return by the server.
|
||||
* @return {Promise} which rejects on sync or process failure.
|
||||
*/
|
||||
maybeSync(lastModified, serverTime) {
|
||||
let db = kintoClient();
|
||||
let opts = {};
|
||||
let enforceCollectionSigning =
|
||||
Services.prefs.getBoolPref(PREF_BLOCKLIST_ENFORCE_SIGNING);
|
||||
|
||||
// if there is a signerName and collection signing is enforced, add a
|
||||
// hook for incoming changes that validates the signature
|
||||
if (this.signerName && enforceCollectionSigning) {
|
||||
opts.hooks = {
|
||||
"incoming-changes": [this.validateCollectionSignature.bind(this)]
|
||||
}
|
||||
}
|
||||
|
||||
let collection = db.collection(this.collectionName, opts);
|
||||
|
||||
return Task.spawn((function* syncCollection() {
|
||||
try {
|
||||
yield collection.db.open();
|
||||
|
||||
let collectionLastModified = yield collection.db.getLastModified();
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
// Fetch changes from server.
|
||||
try {
|
||||
let syncResult = yield collection.sync();
|
||||
if (!syncResult.ok) {
|
||||
throw new Error("Sync failed");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message == INVALID_SIGNATURE) {
|
||||
// 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.
|
||||
let payload = yield fetchRemoteCollection(collection);
|
||||
yield this.validateCollectionSignature(payload, collection, true);
|
||||
// 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 = yield collection.db.getLastModified();
|
||||
if (payload.last_modified >= localLastModified) {
|
||||
yield collection.clear();
|
||||
yield collection.loadDump(payload.data);
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
// Read local collection of records.
|
||||
let list = yield collection.list();
|
||||
|
||||
yield this.processCallback(list.data);
|
||||
|
||||
// Track last update.
|
||||
this.updateLastCheck(serverTime);
|
||||
} finally {
|
||||
collection.db.close();
|
||||
}
|
||||
}).bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save last time server was checked in users prefs.
|
||||
*
|
||||
* @param {Date} serverTime the current date return by server.
|
||||
*/
|
||||
updateLastCheck(serverTime) {
|
||||
let checkedServerTimeInSeconds = Math.round(serverTime / 1000);
|
||||
Services.prefs.setIntPref(this.lastCheckTimePref, checkedServerTimeInSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke the appropriate certificates based on the records from the blocklist.
|
||||
*
|
||||
* @param {Object} records current records in the local db.
|
||||
*/
|
||||
function* updateCertBlocklist(records) {
|
||||
let certList = Cc["@mozilla.org/security/certblocklist;1"]
|
||||
.getService(Ci.nsICertBlocklist);
|
||||
for (let item of records) {
|
||||
try {
|
||||
if (item.issuerName && item.serialNumber) {
|
||||
certList.revokeCertByIssuerAndSerial(item.issuerName,
|
||||
item.serialNumber);
|
||||
} else if (item.subject && item.pubKeyHash) {
|
||||
certList.revokeCertBySubjectAndPubKey(item.subject,
|
||||
item.pubKeyHash);
|
||||
}
|
||||
} catch (e) {
|
||||
// prevent errors relating to individual blocklist entries from
|
||||
// causing sync to fail. At some point in the future, we may want to
|
||||
// accumulate telemetry on these failures.
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
certList.saveEntries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write list of records into JSON file, and notify nsBlocklistService.
|
||||
*
|
||||
* @param {String} filename path relative to profile dir.
|
||||
* @param {Object} records current records in the local db.
|
||||
*/
|
||||
function* updateJSONBlocklist(filename, records) {
|
||||
// Write JSON dump for synchronous load at startup.
|
||||
const path = OS.Path.join(OS.Constants.Path.profileDir, filename);
|
||||
const serialized = JSON.stringify({data: records}, null, 2);
|
||||
try {
|
||||
yield OS.File.writeAtomic(path, serialized, {tmpPath: path + ".tmp"});
|
||||
|
||||
// Notify change to `nsBlocklistService`
|
||||
const eventData = {filename: filename};
|
||||
Services.cpmm.sendAsyncMessage("Blocklist:reload-from-disk", eventData);
|
||||
} catch(e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.OneCRLBlocklistClient = new BlocklistClient(
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_COLLECTION),
|
||||
PREF_BLOCKLIST_ONECRL_CHECKED_SECONDS,
|
||||
updateCertBlocklist,
|
||||
"onecrl.content-signature.mozilla.org"
|
||||
);
|
||||
|
||||
this.AddonBlocklistClient = new BlocklistClient(
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS_COLLECTION),
|
||||
PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS,
|
||||
updateJSONBlocklist.bind(undefined, FILENAME_ADDONS_JSON)
|
||||
);
|
||||
|
||||
this.GfxBlocklistClient = new BlocklistClient(
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_GFX_COLLECTION),
|
||||
PREF_BLOCKLIST_GFX_CHECKED_SECONDS,
|
||||
updateJSONBlocklist.bind(undefined, FILENAME_GFX_JSON)
|
||||
);
|
||||
|
||||
this.PluginBlocklistClient = new BlocklistClient(
|
||||
Services.prefs.getCharPref(PREF_BLOCKLIST_PLUGINS_COLLECTION),
|
||||
PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS,
|
||||
updateJSONBlocklist.bind(undefined, FILENAME_PLUGINS_JSON)
|
||||
);
|
||||
@@ -1,117 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["checkVersions", "addTestBlocklistClient"];
|
||||
|
||||
const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.importGlobalProperties(['fetch']);
|
||||
const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js", {});
|
||||
|
||||
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||
const PREF_BLOCKLIST_CHANGES_PATH = "services.blocklist.changes.path";
|
||||
const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket";
|
||||
const PREF_BLOCKLIST_LAST_UPDATE = "services.blocklist.last_update_seconds";
|
||||
const PREF_BLOCKLIST_LAST_ETAG = "services.blocklist.last_etag";
|
||||
const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
|
||||
|
||||
|
||||
const gBlocklistClients = {
|
||||
[BlocklistClients.OneCRLBlocklistClient.collectionName]: BlocklistClients.OneCRLBlocklistClient,
|
||||
[BlocklistClients.AddonBlocklistClient.collectionName]: BlocklistClients.AddonBlocklistClient,
|
||||
[BlocklistClients.GfxBlocklistClient.collectionName]: BlocklistClients.GfxBlocklistClient,
|
||||
[BlocklistClients.PluginBlocklistClient.collectionName]: BlocklistClients.PluginBlocklistClient
|
||||
};
|
||||
|
||||
// Add a blocklist client for testing purposes. Do not use for any other purpose
|
||||
this.addTestBlocklistClient = (name, client) => { gBlocklistClients[name] = client; }
|
||||
|
||||
// This is called by the ping mechanism.
|
||||
// returns a promise that rejects if something goes wrong
|
||||
this.checkVersions = function() {
|
||||
return Task.spawn(function* syncClients() {
|
||||
// Fetch a versionInfo object that looks like:
|
||||
// {"data":[
|
||||
// {
|
||||
// "host":"kinto-ota.dev.mozaws.net",
|
||||
// "last_modified":1450717104423,
|
||||
// "bucket":"blocklists",
|
||||
// "collection":"certificates"
|
||||
// }]}
|
||||
// Right now, we only use the collection name and the last modified info
|
||||
let kintoBase = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
|
||||
let changesEndpoint = kintoBase + Services.prefs.getCharPref(PREF_BLOCKLIST_CHANGES_PATH);
|
||||
let blocklistsBucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET);
|
||||
|
||||
// Use ETag to obtain a `304 Not modified` when no change occurred.
|
||||
const headers = {};
|
||||
if (Services.prefs.prefHasUserValue(PREF_BLOCKLIST_LAST_ETAG)) {
|
||||
const lastEtag = Services.prefs.getCharPref(PREF_BLOCKLIST_LAST_ETAG);
|
||||
if (lastEtag) {
|
||||
headers["If-None-Match"] = lastEtag;
|
||||
}
|
||||
}
|
||||
|
||||
let response = yield fetch(changesEndpoint, {headers});
|
||||
|
||||
let versionInfo;
|
||||
// No changes since last time. Go on with empty list of changes.
|
||||
if (response.status == 304) {
|
||||
versionInfo = {data: []};
|
||||
} else {
|
||||
versionInfo = yield response.json();
|
||||
}
|
||||
|
||||
// If the server is failing, the JSON response might not contain the
|
||||
// expected data (e.g. error response - Bug 1259145)
|
||||
if (!versionInfo.hasOwnProperty("data")) {
|
||||
throw new Error("Polling for changes failed.");
|
||||
}
|
||||
|
||||
// Record new update time and the difference between local and server time
|
||||
let serverTimeMillis = Date.parse(response.headers.get("Date"));
|
||||
|
||||
// negative clockDifference means local time is behind server time
|
||||
// by the absolute of that value in seconds (positive means it's ahead)
|
||||
let clockDifference = Math.floor((Date.now() - serverTimeMillis) / 1000);
|
||||
Services.prefs.setIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, clockDifference);
|
||||
Services.prefs.setIntPref(PREF_BLOCKLIST_LAST_UPDATE, serverTimeMillis / 1000);
|
||||
|
||||
let firstError;
|
||||
for (let collectionInfo of versionInfo.data) {
|
||||
// Skip changes that don't concern configured blocklist bucket.
|
||||
if (collectionInfo.bucket != blocklistsBucket) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let collection = collectionInfo.collection;
|
||||
let client = gBlocklistClients[collection];
|
||||
if (client && client.maybeSync) {
|
||||
let lastModified = 0;
|
||||
if (collectionInfo.last_modified) {
|
||||
lastModified = collectionInfo.last_modified;
|
||||
}
|
||||
try {
|
||||
yield client.maybeSync(lastModified, serverTimeMillis);
|
||||
} catch (e) {
|
||||
if (!firstError) {
|
||||
firstError = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (firstError) {
|
||||
// cause the promise to reject by throwing the first observed error
|
||||
throw firstError;
|
||||
}
|
||||
|
||||
// Save current Etag for next poll.
|
||||
if (response.headers.has("ETag")) {
|
||||
const currentEtag = response.headers.get("ETag");
|
||||
Services.prefs.setCharPref(PREF_BLOCKLIST_LAST_ETAG, currentEtag);
|
||||
}
|
||||
});
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -15,10 +15,6 @@ EXTRA_COMPONENTS += [
|
||||
|
||||
EXTRA_JS_MODULES['services-common'] += [
|
||||
'async.js',
|
||||
'blocklist-clients.js',
|
||||
'blocklist-updater.js',
|
||||
'kinto-http-client.js',
|
||||
'kinto-offline-client.js',
|
||||
'logmanager.js',
|
||||
'observers.js',
|
||||
'rest.js',
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
const { Constructor: CC } = Components;
|
||||
|
||||
Cu.import("resource://testing-common/httpd.js");
|
||||
|
||||
const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js");
|
||||
const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
|
||||
|
||||
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
|
||||
"nsIBinaryInputStream", "setInputStream");
|
||||
|
||||
let server;
|
||||
|
||||
// set up what we need to make storage adapters
|
||||
const Kinto = loadKinto();
|
||||
const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
|
||||
const kintoFilename = "kinto.sqlite";
|
||||
|
||||
let kintoClient;
|
||||
|
||||
function do_get_kinto_collection(collectionName) {
|
||||
if (!kintoClient) {
|
||||
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,
|
||||
bucket: "blocklists"
|
||||
};
|
||||
kintoClient = new Kinto(config);
|
||||
}
|
||||
return kintoClient.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(function* test_something(){
|
||||
const configPath = "/v1/";
|
||||
const recordsPath = "/v1/buckets/blocklists/collections/certificates/records";
|
||||
|
||||
Services.prefs.setCharPref("services.settings.server",
|
||||
`http://localhost:${server.identity.primaryPort}/v1`);
|
||||
|
||||
// register a handler
|
||||
function handleResponse (request, response) {
|
||||
try {
|
||||
const sample = getSampleResponse(request, server.identity.primaryPort);
|
||||
if (!sample) {
|
||||
do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
|
||||
}
|
||||
|
||||
response.setStatusLine(null, sample.status.status,
|
||||
sample.status.statusText);
|
||||
// send the headers
|
||||
for (let headerLine of sample.sampleHeaders) {
|
||||
let headerElements = headerLine.split(':');
|
||||
response.setHeader(headerElements[0], headerElements[1].trimLeft());
|
||||
}
|
||||
response.setHeader("Date", (new Date()).toUTCString());
|
||||
|
||||
response.write(sample.responseBody);
|
||||
} catch (e) {
|
||||
do_print(e);
|
||||
}
|
||||
}
|
||||
server.registerPathHandler(configPath, handleResponse);
|
||||
server.registerPathHandler(recordsPath, handleResponse);
|
||||
|
||||
// Test an empty db populates
|
||||
let result = yield OneCRLBlocklistClient.maybeSync(2000, Date.now());
|
||||
|
||||
// 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("certificates");
|
||||
yield collection.db.open();
|
||||
let list = yield collection.list();
|
||||
do_check_eq(list.data.length, 1);
|
||||
yield collection.db.close();
|
||||
|
||||
// Test the db is updated when we call again with a later lastModified value
|
||||
result = yield 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
|
||||
collection = do_get_kinto_collection("certificates");
|
||||
yield collection.db.open();
|
||||
list = yield collection.list();
|
||||
do_check_eq(list.data.length, 3);
|
||||
yield collection.db.close();
|
||||
|
||||
// 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");
|
||||
yield OneCRLBlocklistClient.maybeSync(4000, Date.now());
|
||||
|
||||
// Try again with a lastModified value at some point in the past
|
||||
yield OneCRLBlocklistClient.maybeSync(3000, Date.now());
|
||||
|
||||
// Check the OneCRL check time pref is modified, even if the collection
|
||||
// hasn't changed
|
||||
Services.prefs.setIntPref("services.blocklist.onecrl.checked", 0);
|
||||
yield OneCRLBlocklistClient.maybeSync(3000, Date.now());
|
||||
let newValue = Services.prefs.getIntPref("services.blocklist.onecrl.checked");
|
||||
do_check_neq(newValue, 0);
|
||||
|
||||
// Check that a sync completes even when there's bad data in the
|
||||
// collection. This will throw on fail, so just calling maybeSync is an
|
||||
// acceptible test.
|
||||
Services.prefs.setCharPref("services.settings.server",
|
||||
`http://localhost:${server.identity.primaryPort}/v1`);
|
||||
yield OneCRLBlocklistClient.maybeSync(5000, Date.now());
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
// Ensure that signature verification is disabled to prevent interference
|
||||
// with basic certificate sync tests
|
||||
Services.prefs.setBoolPref("services.blocklist.signing.enforced", false);
|
||||
|
||||
// Set up an HTTP Server
|
||||
server = new HttpServer();
|
||||
server.start(-1);
|
||||
|
||||
run_next_test();
|
||||
|
||||
do_register_cleanup(function() {
|
||||
server.stop(() => { });
|
||||
});
|
||||
}
|
||||
|
||||
// get a response for a given request from sample data
|
||||
function getSampleResponse(req, port) {
|
||||
const responses = {
|
||||
"OPTIONS": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
|
||||
"Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress"
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": "null"
|
||||
},
|
||||
"GET:/v1/?": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress"
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
|
||||
},
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"3000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"issuerName": "MEQxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwx0aGF3dGUsIEluYy4xHjAcBgNVBAMTFXRoYXd0ZSBFViBTU0wgQ0EgLSBHMw==",
|
||||
"serialNumber":"CrTHPEE6AZSfI3jysin2bA==",
|
||||
"id":"78cf8900-fdea-4ce5-f8fb-b78710617718",
|
||||
"last_modified":3000
|
||||
}]})
|
||||
},
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=3000": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"4000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"issuerName":"MFkxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKjAoBgNVBAMTIVN0YWF0IGRlciBOZWRlcmxhbmRlbiBPdmVyaGVpZCBDQQ",
|
||||
"serialNumber":"ATFpsA==",
|
||||
"id":"dabafde9-df4a-ddba-2548-748da04cc02c",
|
||||
"last_modified":4000
|
||||
},{
|
||||
"subject":"MCIxIDAeBgNVBAMMF0Fub3RoZXIgVGVzdCBFbmQtZW50aXR5",
|
||||
"pubKeyHash":"VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=",
|
||||
"id":"dabafde9-df4a-ddba-2548-748da04cc02d",
|
||||
"last_modified":4000
|
||||
}]})
|
||||
},
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"5000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"issuerName":"not a base64 encoded issuer",
|
||||
"serialNumber":"not a base64 encoded serial",
|
||||
"id":"dabafde9-df4a-ddba-2548-748da04cc02e",
|
||||
"last_modified":5000
|
||||
},{
|
||||
"subject":"not a base64 encoded subject",
|
||||
"pubKeyHash":"not a base64 encoded pubKeyHash",
|
||||
"id":"dabafde9-df4a-ddba-2548-748da04cc02f",
|
||||
"last_modified":5000
|
||||
},{
|
||||
"subject":"MCIxIDAeBgNVBAMMF0Fub3RoZXIgVGVzdCBFbmQtZW50aXR5",
|
||||
"pubKeyHash":"VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=",
|
||||
"id":"dabafde9-df4a-ddba-2548-748da04cc02g",
|
||||
"last_modified":5000
|
||||
}]})
|
||||
}
|
||||
};
|
||||
return responses[`${req.method}:${req.path}?${req.queryString}`] ||
|
||||
responses[req.method];
|
||||
|
||||
}
|
||||
@@ -1,412 +0,0 @@
|
||||
const { Constructor: CC } = Components;
|
||||
|
||||
const KEY_PROFILEDIR = "ProfD";
|
||||
|
||||
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 { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
|
||||
const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js");
|
||||
|
||||
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
|
||||
"nsIBinaryInputStream", "setInputStream");
|
||||
|
||||
const gBlocklistClients = [
|
||||
{client: BlocklistClients.AddonBlocklistClient, filename: BlocklistClients.FILENAME_ADDONS_JSON, testData: ["i808","i720", "i539"]},
|
||||
{client: BlocklistClients.PluginBlocklistClient, filename: BlocklistClients.FILENAME_PLUGINS_JSON, testData: ["p1044","p32","p28"]},
|
||||
{client: BlocklistClients.GfxBlocklistClient, filename: BlocklistClients.FILENAME_GFX_JSON, testData: ["g204","g200","g36"]},
|
||||
];
|
||||
|
||||
|
||||
let server;
|
||||
let kintoClient;
|
||||
|
||||
function kintoCollection(collectionName) {
|
||||
if (!kintoClient) {
|
||||
const Kinto = loadKinto();
|
||||
const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
|
||||
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,
|
||||
bucket: "blocklists"
|
||||
};
|
||||
kintoClient = new Kinto(config);
|
||||
}
|
||||
return kintoClient.collection(collectionName);
|
||||
}
|
||||
|
||||
function* readJSON(filepath) {
|
||||
const binaryData = yield OS.File.read(filepath);
|
||||
const textData = (new TextDecoder()).decode(binaryData);
|
||||
return Promise.resolve(JSON.parse(textData));
|
||||
}
|
||||
|
||||
function* clear_state() {
|
||||
for (let {client} of gBlocklistClients) {
|
||||
// Remove last server times.
|
||||
Services.prefs.clearUserPref(client.lastCheckTimePref);
|
||||
|
||||
// Clear local DB.
|
||||
const collection = kintoCollection(client.collectionName);
|
||||
try {
|
||||
yield collection.db.open();
|
||||
yield collection.clear();
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove profile data.
|
||||
for (let {filename} of gBlocklistClients) {
|
||||
const blocklist = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
|
||||
if (blocklist.exists()) {
|
||||
blocklist.remove(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function run_test() {
|
||||
// Set up an HTTP Server
|
||||
server = new HttpServer();
|
||||
server.start(-1);
|
||||
|
||||
// Point the blocklist clients to use this local HTTP server.
|
||||
Services.prefs.setCharPref("services.settings.server",
|
||||
`http://localhost:${server.identity.primaryPort}/v1`);
|
||||
|
||||
// Setup server fake responses.
|
||||
function handleResponse(request, response) {
|
||||
try {
|
||||
const sample = getSampleResponse(request, server.identity.primaryPort);
|
||||
if (!sample) {
|
||||
do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
|
||||
}
|
||||
|
||||
response.setStatusLine(null, sample.status.status,
|
||||
sample.status.statusText);
|
||||
// send the headers
|
||||
for (let headerLine of sample.sampleHeaders) {
|
||||
let headerElements = headerLine.split(':');
|
||||
response.setHeader(headerElements[0], headerElements[1].trimLeft());
|
||||
}
|
||||
response.setHeader("Date", (new Date()).toUTCString());
|
||||
|
||||
response.write(sample.responseBody);
|
||||
response.finish();
|
||||
} catch (e) {
|
||||
do_print(e);
|
||||
}
|
||||
}
|
||||
const configPath = "/v1/";
|
||||
const addonsRecordsPath = "/v1/buckets/blocklists/collections/addons/records";
|
||||
const gfxRecordsPath = "/v1/buckets/blocklists/collections/gfx/records";
|
||||
const pluginsRecordsPath = "/v1/buckets/blocklists/collections/plugins/records";
|
||||
server.registerPathHandler(configPath, handleResponse);
|
||||
server.registerPathHandler(addonsRecordsPath, handleResponse);
|
||||
server.registerPathHandler(gfxRecordsPath, handleResponse);
|
||||
server.registerPathHandler(pluginsRecordsPath, handleResponse);
|
||||
|
||||
|
||||
run_next_test();
|
||||
|
||||
do_register_cleanup(function() {
|
||||
server.stop(() => { });
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* test_records_obtained_from_server_are_stored_in_db(){
|
||||
for (let {client} of gBlocklistClients) {
|
||||
// Test an empty db populates
|
||||
let result = yield client.maybeSync(2000, Date.now());
|
||||
|
||||
// Open the collection, verify it's been populated:
|
||||
// Our test data has a single record; it should be in the local collection
|
||||
let collection = kintoCollection(client.collectionName);
|
||||
yield collection.db.open();
|
||||
let list = yield collection.list();
|
||||
equal(list.data.length, 1);
|
||||
yield collection.db.close();
|
||||
}
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(function* test_list_is_written_to_file_in_profile(){
|
||||
for (let {client, filename, testData} of gBlocklistClients) {
|
||||
const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
|
||||
strictEqual(profFile.exists(), false);
|
||||
|
||||
let result = yield client.maybeSync(2000, Date.now());
|
||||
|
||||
strictEqual(profFile.exists(), true);
|
||||
const content = yield readJSON(profFile.path);
|
||||
equal(content.data[0].blockID, testData[testData.length - 1]);
|
||||
}
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(function* test_current_server_time_is_saved_in_pref(){
|
||||
for (let {client} of gBlocklistClients) {
|
||||
const before = Services.prefs.getIntPref(client.lastCheckTimePref);
|
||||
const serverTime = Date.now();
|
||||
yield client.maybeSync(2000, serverTime);
|
||||
const after = Services.prefs.getIntPref(client.lastCheckTimePref);
|
||||
equal(after, Math.round(serverTime / 1000));
|
||||
}
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(function* test_update_json_file_when_addons_has_changes(){
|
||||
for (let {client, filename, testData} of gBlocklistClients) {
|
||||
yield client.maybeSync(2000, Date.now() - 1000);
|
||||
const before = Services.prefs.getIntPref(client.lastCheckTimePref);
|
||||
const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
|
||||
const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000;
|
||||
const serverTime = Date.now();
|
||||
|
||||
yield client.maybeSync(3001, serverTime);
|
||||
|
||||
// File was updated.
|
||||
notEqual(fileLastModified, profFile.lastModifiedTime);
|
||||
const content = yield readJSON(profFile.path);
|
||||
deepEqual(content.data.map((r) => r.blockID), testData);
|
||||
// Server time was updated.
|
||||
const after = Services.prefs.getIntPref(client.lastCheckTimePref);
|
||||
equal(after, Math.round(serverTime / 1000));
|
||||
}
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(function* test_sends_reload_message_when_blocklist_has_changes(){
|
||||
for (let {client, filename} of gBlocklistClients) {
|
||||
let received = yield new Promise((resolve, reject) => {
|
||||
Services.ppmm.addMessageListener("Blocklist:reload-from-disk", {
|
||||
receiveMessage(aMsg) { resolve(aMsg) }
|
||||
});
|
||||
|
||||
client.maybeSync(2000, Date.now() - 1000);
|
||||
});
|
||||
|
||||
equal(received.data.filename, filename);
|
||||
}
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(function* test_do_nothing_when_blocklist_is_up_to_date(){
|
||||
for (let {client, filename} of gBlocklistClients) {
|
||||
yield client.maybeSync(2000, Date.now() - 1000);
|
||||
const before = Services.prefs.getIntPref(client.lastCheckTimePref);
|
||||
const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
|
||||
const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000;
|
||||
const serverTime = Date.now();
|
||||
|
||||
yield client.maybeSync(3000, serverTime);
|
||||
|
||||
// File was not updated.
|
||||
equal(fileLastModified, profFile.lastModifiedTime);
|
||||
// Server time was updated.
|
||||
const after = Services.prefs.getIntPref(client.lastCheckTimePref);
|
||||
equal(after, Math.round(serverTime / 1000));
|
||||
}
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
|
||||
|
||||
// get a response for a given request from sample data
|
||||
function getSampleResponse(req, port) {
|
||||
const responses = {
|
||||
"OPTIONS": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
|
||||
"Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress"
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": "null"
|
||||
},
|
||||
"GET:/v1/?": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress"
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
|
||||
},
|
||||
"GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"3000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"prefs": [],
|
||||
"blockID": "i539",
|
||||
"last_modified": 3000,
|
||||
"versionRange": [{
|
||||
"targetApplication": [],
|
||||
"maxVersion": "*",
|
||||
"minVersion": "0",
|
||||
"severity": "1"
|
||||
}],
|
||||
"guid": "ScorpionSaver@jetpack",
|
||||
"id": "9d500963-d80e-3a91-6e74-66f3811b99cc"
|
||||
}]})
|
||||
},
|
||||
"GET:/v1/buckets/blocklists/collections/plugins/records?_sort=-last_modified": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"3000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"matchFilename": "NPFFAddOn.dll",
|
||||
"blockID": "p28",
|
||||
"id": "7b1e0b3c-e390-a817-11b6-a6887f65f56e",
|
||||
"last_modified": 3000,
|
||||
"versionRange": []
|
||||
}]})
|
||||
},
|
||||
"GET:/v1/buckets/blocklists/collections/gfx/records?_sort=-last_modified": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"3000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"driverVersionComparator": "LESS_THAN_OR_EQUAL",
|
||||
"driverVersion": "8.17.12.5896",
|
||||
"vendor": "0x10de",
|
||||
"blockID": "g36",
|
||||
"feature": "DIRECT3D_9_LAYERS",
|
||||
"devices": ["0x0a6c"],
|
||||
"featureStatus": "BLOCKED_DRIVER_VERSION",
|
||||
"last_modified": 3000,
|
||||
"os": "WINNT 6.1",
|
||||
"id": "3f947f16-37c2-4e96-d356-78b26363729b"
|
||||
}]})
|
||||
},
|
||||
"GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified&_since=3000": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"4000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"prefs": [],
|
||||
"blockID": "i808",
|
||||
"last_modified": 4000,
|
||||
"versionRange": [{
|
||||
"targetApplication": [],
|
||||
"maxVersion": "*",
|
||||
"minVersion": "0",
|
||||
"severity": "3"
|
||||
}],
|
||||
"guid": "{c96d1ae6-c4cf-4984-b110-f5f561b33b5a}",
|
||||
"id": "9ccfac91-e463-c30c-f0bd-14143794a8dd"
|
||||
}, {
|
||||
"prefs": ["browser.startup.homepage"],
|
||||
"blockID": "i720",
|
||||
"last_modified": 3500,
|
||||
"versionRange": [{
|
||||
"targetApplication": [],
|
||||
"maxVersion": "*",
|
||||
"minVersion": "0",
|
||||
"severity": "1"
|
||||
}],
|
||||
"guid": "FXqG@xeeR.net",
|
||||
"id": "cf9b3129-a97e-dbd7-9525-a8575ac03c25"
|
||||
}]})
|
||||
},
|
||||
"GET:/v1/buckets/blocklists/collections/plugins/records?_sort=-last_modified&_since=3000": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"4000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"infoURL": "https://get.adobe.com/flashplayer/",
|
||||
"blockID": "p1044",
|
||||
"matchFilename": "libflashplayer\\.so",
|
||||
"last_modified": 4000,
|
||||
"versionRange": [{
|
||||
"targetApplication": [],
|
||||
"minVersion": "11.2.202.509",
|
||||
"maxVersion": "11.2.202.539",
|
||||
"severity": "0",
|
||||
"vulnerabilityStatus": "1"
|
||||
}],
|
||||
"os": "Linux",
|
||||
"id": "aabad965-e556-ffe7-4191-074f5dee3df3"
|
||||
}, {
|
||||
"matchFilename": "npViewpoint.dll",
|
||||
"blockID": "p32",
|
||||
"id": "1f48af42-c508-b8ef-b8d5-609d48e4f6c9",
|
||||
"last_modified": 3500,
|
||||
"versionRange": [{
|
||||
"targetApplication": [{
|
||||
"minVersion": "3.0",
|
||||
"guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
|
||||
"maxVersion": "*"
|
||||
}]
|
||||
}]
|
||||
}]})
|
||||
},
|
||||
"GET:/v1/buckets/blocklists/collections/gfx/records?_sort=-last_modified&_since=3000": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"4000\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{
|
||||
"vendor": "0x8086",
|
||||
"blockID": "g204",
|
||||
"feature": "WEBGL_MSAA",
|
||||
"devices": [],
|
||||
"id": "c96bca82-e6bd-044d-14c4-9c1d67e9283a",
|
||||
"last_modified": 4000,
|
||||
"os": "Darwin 10",
|
||||
"featureStatus": "BLOCKED_DEVICE"
|
||||
}, {
|
||||
"vendor": "0x10de",
|
||||
"blockID": "g200",
|
||||
"feature": "WEBGL_MSAA",
|
||||
"devices": [],
|
||||
"id": "c3a15ba9-e0e2-421f-e399-c995e5b8d14e",
|
||||
"last_modified": 3500,
|
||||
"os": "Darwin 11",
|
||||
"featureStatus": "BLOCKED_DEVICE"
|
||||
}]})
|
||||
}
|
||||
};
|
||||
return responses[`${req.method}:${req.path}?${req.queryString}`] ||
|
||||
responses[req.method];
|
||||
|
||||
}
|
||||
@@ -1,510 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://services-common/blocklist-updater.js");
|
||||
Cu.import("resource://testing-common/httpd.js");
|
||||
|
||||
const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
|
||||
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
|
||||
const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.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";
|
||||
|
||||
|
||||
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"]
|
||||
.createInstance(Ci.nsIFileInputStream);
|
||||
stream.init(file, -1, 0, 0);
|
||||
const data = NetUtil.readInputStreamToString(stream, stream.available());
|
||||
stream.close();
|
||||
return data;
|
||||
}
|
||||
|
||||
function setRoot() {
|
||||
const filename = CERT_DIR + CHAIN_FILES[0];
|
||||
|
||||
const certFile = do_get_file(filename, false);
|
||||
const b64cert = getFileData(certFile)
|
||||
.replace(/-----BEGIN CERTIFICATE-----/, "")
|
||||
.replace(/-----END CERTIFICATE-----/, "")
|
||||
.replace(/[\r\n]/g, "");
|
||||
const certdb = Cc["@mozilla.org/security/x509certdb;1"]
|
||||
.getService(Ci.nsIX509CertDB);
|
||||
const cert = certdb.constructX509FromBase64(b64cert);
|
||||
Services.prefs.setCharPref(PREF_SIGNATURE_ROOT, cert.sha256Fingerprint);
|
||||
}
|
||||
|
||||
function getCertChain() {
|
||||
const chain = [];
|
||||
for (let file of CHAIN_FILES) {
|
||||
chain.push(getFileData(do_get_file(CERT_DIR + file)));
|
||||
}
|
||||
return chain.join("\n");
|
||||
}
|
||||
|
||||
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 Kinto = loadKinto();
|
||||
|
||||
const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
|
||||
|
||||
const config = {
|
||||
remote: base,
|
||||
bucket: bucket,
|
||||
adapter: FirefoxAdapter,
|
||||
};
|
||||
|
||||
const db = new Kinto(config);
|
||||
const collection = db.collection(collectionName);
|
||||
|
||||
yield collection.db.open();
|
||||
|
||||
// Check we have the expected number of records
|
||||
let records = yield collection.list();
|
||||
do_check_eq(count, records.data.length);
|
||||
|
||||
// Close the collection so the test can exit cleanly
|
||||
yield collection.db.close();
|
||||
}
|
||||
|
||||
// Check to ensure maybeSync is called with correct values when a changes
|
||||
// document contains information on when a collection was last modified
|
||||
add_task(function* test_check_signatures(){
|
||||
const port = server.identity.primaryPort;
|
||||
|
||||
// a response to give the client when the cert chain is expected
|
||||
function makeMetaResponseBody(lastModified, signature) {
|
||||
return {
|
||||
data: {
|
||||
id: "certificates",
|
||||
last_modified: lastModified,
|
||||
signature: {
|
||||
x5u: `http://localhost:${port}/test_blocklist_signatures/test_cert_chain.pem`,
|
||||
public_key: "fake",
|
||||
"content-signature": `x5u=http://localhost:${port}/test_blocklist_signatures/test_cert_chain.pem;p384ecdsa=${signature}`,
|
||||
signature_encoding: "rs_base64url",
|
||||
signature: signature,
|
||||
hash_algorithm: "sha384",
|
||||
ref: "1yryrnmzou5rf31ou80znpnq8n"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function makeMetaResponse(eTag, body, comment) {
|
||||
return {
|
||||
comment: comment,
|
||||
sampleHeaders: [
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
`ETag: \"${eTag}\"`
|
||||
],
|
||||
status: {status: 200, statusText: "OK"},
|
||||
responseBody: JSON.stringify(body)
|
||||
};
|
||||
}
|
||||
|
||||
function registerHandlers(responses){
|
||||
function handleResponse (serverTimeMillis, request, response) {
|
||||
const key = `${request.method}:${request.path}?${request.queryString}`;
|
||||
const available = responses[key];
|
||||
const sampled = available.length > 1 ? available.shift() : available[0];
|
||||
|
||||
if (!sampled) {
|
||||
do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
|
||||
}
|
||||
|
||||
response.setStatusLine(null, sampled.status.status,
|
||||
sampled.status.statusText);
|
||||
// send the headers
|
||||
for (let headerLine of sampled.sampleHeaders) {
|
||||
let headerElements = headerLine.split(':');
|
||||
response.setHeader(headerElements[0], headerElements[1].trimLeft());
|
||||
}
|
||||
|
||||
// set the server date
|
||||
response.setHeader("Date", (new Date(serverTimeMillis)).toUTCString());
|
||||
|
||||
response.write(sampled.responseBody);
|
||||
}
|
||||
|
||||
for (let key of Object.keys(responses)) {
|
||||
const keyParts = key.split(":");
|
||||
const method = keyParts[0];
|
||||
const valueParts = keyParts[1].split("?");
|
||||
const path = valueParts[0];
|
||||
|
||||
server.registerPathHandler(path, handleResponse.bind(null, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
// First, perform a signature verification with known data and signature
|
||||
// to ensure things are working correctly
|
||||
let verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
|
||||
.createInstance(Ci.nsIContentSignatureVerifier);
|
||||
|
||||
const emptyData = '[]';
|
||||
const emptySignature = "p384ecdsa=zbugm2FDitsHwk5-IWsas1PpWwY29f0Fg5ZHeqD8fzep7AVl2vfcaHA7LdmCZ28qZLOioGKvco3qT117Q4-HlqFTJM7COHzxGyU2MMJ0ZTnhJrPOC1fP3cVQjU1PTWi9";
|
||||
const name = "onecrl.content-signature.mozilla.org";
|
||||
ok(verifier.verifyContentSignature(emptyData, emptySignature,
|
||||
getCertChain(), name));
|
||||
|
||||
verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
|
||||
.createInstance(Ci.nsIContentSignatureVerifier);
|
||||
|
||||
const collectionData = '[{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:43:37Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"97fbf7c4-3ef2-f54f-0029-1ba6540c63ea","issuerName":"MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==","last_modified":2000,"serialNumber":"BAAAAAABA/A35EU="},{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:48:11Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc","issuerName":"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB","last_modified":3000,"serialNumber":"BAAAAAABI54PryQ="}]';
|
||||
const collectionSignature = "p384ecdsa=f4pA2tYM5jQgWY6YUmhUwQiBLj6QO5sHLD_5MqLePz95qv-7cNCuQoZnPQwxoptDtW8hcWH3kLb0quR7SB-r82gkpR9POVofsnWJRA-ETb0BcIz6VvI3pDT49ZLlNg3p";
|
||||
|
||||
ok(verifier.verifyContentSignature(collectionData, collectionSignature, getCertChain(), name));
|
||||
|
||||
// set up prefs so the kinto updater talks to the test server
|
||||
Services.prefs.setCharPref(PREF_SETTINGS_SERVER,
|
||||
`http://localhost:${server.identity.primaryPort}/v1`);
|
||||
|
||||
// Set up some data we need for our test
|
||||
let startTime = Date.now();
|
||||
|
||||
// These are records we'll use in the test collections
|
||||
const RECORD1 = {
|
||||
details: {
|
||||
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
|
||||
created: "2016-01-18T14:43:37Z",
|
||||
name: "GlobalSign certs",
|
||||
who: ".",
|
||||
why: "."
|
||||
},
|
||||
enabled: true,
|
||||
id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
|
||||
issuerName: "MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==",
|
||||
last_modified: 2000,
|
||||
serialNumber: "BAAAAAABA/A35EU="
|
||||
};
|
||||
|
||||
const RECORD2 = {
|
||||
details: {
|
||||
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
|
||||
created: "2016-01-18T14:48:11Z",
|
||||
name: "GlobalSign certs",
|
||||
who: ".",
|
||||
why: "."
|
||||
},
|
||||
enabled: true,
|
||||
id: "e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc",
|
||||
issuerName: "MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
|
||||
last_modified: 3000,
|
||||
serialNumber: "BAAAAAABI54PryQ="
|
||||
};
|
||||
|
||||
const RECORD3 = {
|
||||
details: {
|
||||
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
|
||||
created: "2016-01-18T14:48:11Z",
|
||||
name: "GlobalSign certs",
|
||||
who: ".",
|
||||
why: "."
|
||||
},
|
||||
enabled: true,
|
||||
id: "c7c49b69-a4ab-418e-92a9-e1961459aa7f",
|
||||
issuerName: "MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
|
||||
last_modified: 4000,
|
||||
serialNumber: "BAAAAAABI54PryQ="
|
||||
};
|
||||
|
||||
const RECORD1_DELETION = {
|
||||
deleted: true,
|
||||
enabled: true,
|
||||
id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
|
||||
last_modified: 3500,
|
||||
};
|
||||
|
||||
// Check that a signature on an empty collection is OK
|
||||
// We need to set up paths on the HTTP server to return specific data from
|
||||
// specific paths for each test. Here we prepare data for each response.
|
||||
|
||||
// A cert chain response (this the cert chain that contains the signing
|
||||
// cert, the root and any intermediates in between). This is used in each
|
||||
// sync.
|
||||
const RESPONSE_CERT_CHAIN = {
|
||||
comment: "RESPONSE_CERT_CHAIN",
|
||||
sampleHeaders: [
|
||||
"Content-Type: text/plain; charset=UTF-8"
|
||||
],
|
||||
status: {status: 200, statusText: "OK"},
|
||||
responseBody: getCertChain()
|
||||
};
|
||||
|
||||
// A server settings response. This is used in each sync.
|
||||
const RESPONSE_SERVER_SETTINGS = {
|
||||
comment: "RESPONSE_SERVER_SETTINGS",
|
||||
sampleHeaders: [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress"
|
||||
],
|
||||
status: {status: 200, statusText: "OK"},
|
||||
responseBody: JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
|
||||
};
|
||||
|
||||
// This is the initial, empty state of the collection. This is only used
|
||||
// for the first sync.
|
||||
const RESPONSE_EMPTY_INITIAL = {
|
||||
comment: "RESPONSE_EMPTY_INITIAL",
|
||||
sampleHeaders: [
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"ETag: \"1000\""
|
||||
],
|
||||
status: {status: 200, statusText: "OK"},
|
||||
responseBody: JSON.stringify({"data": []})
|
||||
};
|
||||
|
||||
const RESPONSE_BODY_META_EMPTY_SIG = makeMetaResponseBody(1000,
|
||||
"vxuAg5rDCB-1pul4a91vqSBQRXJG_j7WOYUTswxRSMltdYmbhLRH8R8brQ9YKuNDF56F-w6pn4HWxb076qgKPwgcEBtUeZAO_RtaHXRkRUUgVzAr86yQL4-aJTbv3D6u");
|
||||
|
||||
// The collection metadata containing the signature for the empty
|
||||
// collection.
|
||||
const RESPONSE_META_EMPTY_SIG =
|
||||
makeMetaResponse(1000, RESPONSE_BODY_META_EMPTY_SIG,
|
||||
"RESPONSE_META_EMPTY_SIG");
|
||||
|
||||
// Here, we map request method and path to the available responses
|
||||
const emptyCollectionResponses = {
|
||||
"GET:/test_blocklist_signatures/test_cert_chain.pem?":[RESPONSE_CERT_CHAIN],
|
||||
"GET:/v1/?": [RESPONSE_SERVER_SETTINGS],
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified":
|
||||
[RESPONSE_EMPTY_INITIAL],
|
||||
"GET:/v1/buckets/blocklists/collections/certificates?":
|
||||
[RESPONSE_META_EMPTY_SIG]
|
||||
};
|
||||
|
||||
// .. and use this map to register handlers for each path
|
||||
registerHandlers(emptyCollectionResponses);
|
||||
|
||||
// With all of this set up, we attempt a sync. This will resolve if all is
|
||||
// well and throw if something goes wrong.
|
||||
yield OneCRLBlocklistClient.maybeSync(1000, startTime);
|
||||
|
||||
// Check that some additions (2 records) to the collection have a valid
|
||||
// signature.
|
||||
|
||||
// This response adds two entries (RECORD1 and RECORD2) to the collection
|
||||
const RESPONSE_TWO_ADDED = {
|
||||
comment: "RESPONSE_TWO_ADDED",
|
||||
sampleHeaders: [
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"ETag: \"3000\""
|
||||
],
|
||||
status: {status: 200, statusText: "OK"},
|
||||
responseBody: JSON.stringify({"data": [RECORD2, RECORD1]})
|
||||
};
|
||||
|
||||
const RESPONSE_BODY_META_TWO_ITEMS_SIG = makeMetaResponseBody(3000,
|
||||
"dwhJeypadNIyzGj3QdI0KMRTPnHhFPF_j73mNrsPAHKMW46S2Ftf4BzsPMvPMB8h0TjDus13wo_R4l432DHe7tYyMIWXY0PBeMcoe5BREhFIxMxTsh9eGVXBD1e3UwRy");
|
||||
|
||||
// A signature response for the collection containg RECORD1 and RECORD2
|
||||
const RESPONSE_META_TWO_ITEMS_SIG =
|
||||
makeMetaResponse(3000, RESPONSE_BODY_META_TWO_ITEMS_SIG,
|
||||
"RESPONSE_META_TWO_ITEMS_SIG");
|
||||
|
||||
const twoItemsResponses = {
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=1000":
|
||||
[RESPONSE_TWO_ADDED],
|
||||
"GET:/v1/buckets/blocklists/collections/certificates?":
|
||||
[RESPONSE_META_TWO_ITEMS_SIG]
|
||||
};
|
||||
registerHandlers(twoItemsResponses);
|
||||
yield OneCRLBlocklistClient.maybeSync(3000, startTime);
|
||||
|
||||
// Check the collection with one addition and one removal has a valid
|
||||
// signature
|
||||
|
||||
// Remove RECORD1, add RECORD3
|
||||
const RESPONSE_ONE_ADDED_ONE_REMOVED = {
|
||||
comment: "RESPONSE_ONE_ADDED_ONE_REMOVED ",
|
||||
sampleHeaders: [
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"ETag: \"4000\""
|
||||
],
|
||||
status: {status: 200, statusText: "OK"},
|
||||
responseBody: JSON.stringify({"data": [RECORD3, RECORD1_DELETION]})
|
||||
};
|
||||
|
||||
const RESPONSE_BODY_META_THREE_ITEMS_SIG = makeMetaResponseBody(4000,
|
||||
"MIEmNghKnkz12UodAAIc3q_Y4a3IJJ7GhHF4JYNYmm8avAGyPM9fYU7NzVo94pzjotG7vmtiYuHyIX2rTHTbT587w0LdRWxipgFd_PC1mHiwUyjFYNqBBG-kifYk7kEw");
|
||||
|
||||
// signature response for the collection containing RECORD2 and RECORD3
|
||||
const RESPONSE_META_THREE_ITEMS_SIG =
|
||||
makeMetaResponse(4000, RESPONSE_BODY_META_THREE_ITEMS_SIG,
|
||||
"RESPONSE_META_THREE_ITEMS_SIG");
|
||||
|
||||
const oneAddedOneRemovedResponses = {
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=3000":
|
||||
[RESPONSE_ONE_ADDED_ONE_REMOVED],
|
||||
"GET:/v1/buckets/blocklists/collections/certificates?":
|
||||
[RESPONSE_META_THREE_ITEMS_SIG]
|
||||
};
|
||||
registerHandlers(oneAddedOneRemovedResponses);
|
||||
yield OneCRLBlocklistClient.maybeSync(4000, startTime);
|
||||
|
||||
// Check the signature is still valid with no operation (no changes)
|
||||
|
||||
// Leave the collection unchanged
|
||||
const RESPONSE_EMPTY_NO_UPDATE = {
|
||||
comment: "RESPONSE_EMPTY_NO_UPDATE ",
|
||||
sampleHeaders: [
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"ETag: \"4000\""
|
||||
],
|
||||
status: {status: 200, statusText: "OK"},
|
||||
responseBody: JSON.stringify({"data": []})
|
||||
};
|
||||
|
||||
const noOpResponses = {
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
|
||||
[RESPONSE_EMPTY_NO_UPDATE],
|
||||
"GET:/v1/buckets/blocklists/collections/certificates?":
|
||||
[RESPONSE_META_THREE_ITEMS_SIG]
|
||||
};
|
||||
registerHandlers(noOpResponses);
|
||||
yield OneCRLBlocklistClient.maybeSync(4100, startTime);
|
||||
|
||||
// Check the collection is reset when the signature is invalid
|
||||
|
||||
// Prepare a (deliberately) bad signature to check the collection state is
|
||||
// reset if something is inconsistent
|
||||
const RESPONSE_COMPLETE_INITIAL = {
|
||||
comment: "RESPONSE_COMPLETE_INITIAL ",
|
||||
sampleHeaders: [
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"ETag: \"4000\""
|
||||
],
|
||||
status: {status: 200, statusText: "OK"},
|
||||
responseBody: JSON.stringify({"data": [RECORD2, RECORD3]})
|
||||
};
|
||||
|
||||
const RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID = {
|
||||
comment: "RESPONSE_COMPLETE_INITIAL ",
|
||||
sampleHeaders: [
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"ETag: \"4000\""
|
||||
],
|
||||
status: {status: 200, statusText: "OK"},
|
||||
responseBody: JSON.stringify({"data": [RECORD3, RECORD2]})
|
||||
};
|
||||
|
||||
const RESPONSE_BODY_META_BAD_SIG = makeMetaResponseBody(4000,
|
||||
"aW52YWxpZCBzaWduYXR1cmUK");
|
||||
|
||||
const RESPONSE_META_BAD_SIG =
|
||||
makeMetaResponse(4000, RESPONSE_BODY_META_BAD_SIG, "RESPONSE_META_BAD_SIG");
|
||||
|
||||
const badSigGoodSigResponses = {
|
||||
// In this test, we deliberately serve a bad signature initially. The
|
||||
// subsequent signature returned is a valid one for the three item
|
||||
// collection.
|
||||
"GET:/v1/buckets/blocklists/collections/certificates?":
|
||||
[RESPONSE_META_BAD_SIG, RESPONSE_META_THREE_ITEMS_SIG],
|
||||
// The first collection state is the three item collection (since
|
||||
// there's a sync with no updates) - but, since the signature is wrong,
|
||||
// another request will be made...
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
|
||||
[RESPONSE_EMPTY_NO_UPDATE],
|
||||
// The next request is for the full collection. This will be checked
|
||||
// against the valid signature - so the sync should succeed.
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified":
|
||||
[RESPONSE_COMPLETE_INITIAL],
|
||||
// The next request is for the full collection sorted by id. This will be
|
||||
// checked against the valid signature - so the sync should succeed.
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
|
||||
[RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID]
|
||||
};
|
||||
|
||||
registerHandlers(badSigGoodSigResponses);
|
||||
yield OneCRLBlocklistClient.maybeSync(5000, startTime);
|
||||
|
||||
const badSigGoodOldResponses = {
|
||||
// In this test, we deliberately serve a bad signature initially. The
|
||||
// subsequent sitnature returned is a valid one for the three item
|
||||
// collection.
|
||||
"GET:/v1/buckets/blocklists/collections/certificates?":
|
||||
[RESPONSE_META_BAD_SIG, RESPONSE_META_EMPTY_SIG],
|
||||
// The first collection state is the current state (since there's no update
|
||||
// - but, since the signature is wrong, another request will be made)
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
|
||||
[RESPONSE_EMPTY_NO_UPDATE],
|
||||
// The next request is for the full collection sorted by id. This will be
|
||||
// checked against the valid signature and last_modified times will be
|
||||
// compared. Sync should fail, even though the signature is good,
|
||||
// because the local collection is newer.
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
|
||||
[RESPONSE_EMPTY_INITIAL],
|
||||
};
|
||||
|
||||
// ensure our collection hasn't been replaced with an older, empty one
|
||||
yield checkRecordCount(2);
|
||||
|
||||
registerHandlers(badSigGoodOldResponses);
|
||||
yield OneCRLBlocklistClient.maybeSync(5000, startTime);
|
||||
|
||||
const allBadSigResponses = {
|
||||
// In this test, we deliberately serve only a bad signature.
|
||||
"GET:/v1/buckets/blocklists/collections/certificates?":
|
||||
[RESPONSE_META_BAD_SIG],
|
||||
// The first collection state is the three item collection (since
|
||||
// there's a sync with no updates) - but, since the signature is wrong,
|
||||
// another request will be made...
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
|
||||
[RESPONSE_EMPTY_NO_UPDATE],
|
||||
// The next request is for the full collection sorted by id. This will be
|
||||
// checked against the valid signature - so the sync should succeed.
|
||||
"GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
|
||||
[RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID]
|
||||
};
|
||||
|
||||
registerHandlers(allBadSigResponses);
|
||||
try {
|
||||
yield OneCRLBlocklistClient.maybeSync(6000, startTime);
|
||||
do_throw("Sync should fail (the signature is intentionally bad)");
|
||||
} catch (e) {
|
||||
yield checkRecordCount(2);
|
||||
}
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
// ensure signatures are enforced
|
||||
Services.prefs.setBoolPref(PREF_BLOCKLIST_ENFORCE_SIGNING, true);
|
||||
|
||||
// get a signature verifier to ensure nsNSSComponent is initialized
|
||||
Cc["@mozilla.org/security/contentsignatureverifier;1"]
|
||||
.createInstance(Ci.nsIContentSignatureVerifier);
|
||||
|
||||
// set the content signing root to our test root
|
||||
setRoot();
|
||||
|
||||
// Set up an HTTP Server
|
||||
server = new HttpServer();
|
||||
server.start(-1);
|
||||
|
||||
run_next_test();
|
||||
|
||||
do_register_cleanup(function() {
|
||||
server.stop(function() { });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
Cu.import("resource://testing-common/httpd.js");
|
||||
|
||||
var server;
|
||||
|
||||
const PREF_SETTINGS_SERVER = "services.settings.server";
|
||||
const PREF_LAST_UPDATE = "services.blocklist.last_update_seconds";
|
||||
const PREF_LAST_ETAG = "services.blocklist.last_etag";
|
||||
const PREF_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
|
||||
|
||||
// Check to ensure maybeSync is called with correct values when a changes
|
||||
// document contains information on when a collection was last modified
|
||||
add_task(function* test_check_maybeSync(){
|
||||
const changesPath = "/v1/buckets/monitor/collections/changes/records";
|
||||
|
||||
// register a handler
|
||||
function handleResponse (serverTimeMillis, request, response) {
|
||||
try {
|
||||
const sampled = getSampleResponse(request, server.identity.primaryPort);
|
||||
if (!sampled) {
|
||||
do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
|
||||
}
|
||||
|
||||
response.setStatusLine(null, sampled.status.status,
|
||||
sampled.status.statusText);
|
||||
// send the headers
|
||||
for (let headerLine of sampled.sampleHeaders) {
|
||||
let headerElements = headerLine.split(':');
|
||||
response.setHeader(headerElements[0], headerElements[1].trimLeft());
|
||||
}
|
||||
|
||||
// set the server date
|
||||
response.setHeader("Date", (new Date(serverTimeMillis)).toUTCString());
|
||||
|
||||
response.write(sampled.responseBody);
|
||||
} catch (e) {
|
||||
dump(`${e}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
server.registerPathHandler(changesPath, handleResponse.bind(null, 2000));
|
||||
|
||||
// set up prefs so the kinto updater talks to the test server
|
||||
Services.prefs.setCharPref(PREF_SETTINGS_SERVER,
|
||||
`http://localhost:${server.identity.primaryPort}/v1`);
|
||||
|
||||
// set some initial values so we can check these are updated appropriately
|
||||
Services.prefs.setIntPref(PREF_LAST_UPDATE, 0);
|
||||
Services.prefs.setIntPref(PREF_CLOCK_SKEW_SECONDS, 0);
|
||||
Services.prefs.clearUserPref(PREF_LAST_ETAG);
|
||||
|
||||
|
||||
let startTime = Date.now();
|
||||
|
||||
let updater = Cu.import("resource://services-common/blocklist-updater.js");
|
||||
|
||||
let syncPromise = new Promise(function(resolve, reject) {
|
||||
// add a test kinto client that will respond to lastModified information
|
||||
// for a collection called 'test-collection'
|
||||
updater.addTestBlocklistClient("test-collection", {
|
||||
maybeSync(lastModified, serverTime) {
|
||||
do_check_eq(lastModified, 1000);
|
||||
do_check_eq(serverTime, 2000);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
updater.checkVersions();
|
||||
});
|
||||
|
||||
// ensure we get the maybeSync call
|
||||
yield syncPromise;
|
||||
|
||||
// check the last_update is updated
|
||||
do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
|
||||
|
||||
// How does the clock difference look?
|
||||
let endTime = Date.now();
|
||||
let clockDifference = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS);
|
||||
// we previously set the serverTime to 2 (seconds past epoch)
|
||||
do_check_true(clockDifference <= endTime / 1000
|
||||
&& clockDifference >= Math.floor(startTime / 1000) - 2);
|
||||
// Last timestamp was saved. An ETag header value is a quoted string.
|
||||
let lastEtag = Services.prefs.getCharPref(PREF_LAST_ETAG);
|
||||
do_check_eq(lastEtag, "\"1100\"");
|
||||
|
||||
// Simulate a poll with up-to-date collection.
|
||||
Services.prefs.setIntPref(PREF_LAST_UPDATE, 0);
|
||||
// If server has no change, a 304 is received, maybeSync() is not called.
|
||||
updater.addTestBlocklistClient("test-collection", {
|
||||
maybeSync: () => {throw new Error("Should not be called");}
|
||||
});
|
||||
yield updater.checkVersions();
|
||||
// Last update is overwritten
|
||||
do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
|
||||
|
||||
|
||||
// Simulate a server error.
|
||||
function simulateErrorResponse (request, response) {
|
||||
response.setHeader("Date", (new Date(3000)).toUTCString());
|
||||
response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
||||
response.write(JSON.stringify({
|
||||
code: 503,
|
||||
errno: 999,
|
||||
error: "Service Unavailable",
|
||||
}));
|
||||
response.setStatusLine(null, 503, "Service Unavailable");
|
||||
}
|
||||
server.registerPathHandler(changesPath, simulateErrorResponse);
|
||||
// checkVersions() fails with adequate error.
|
||||
let error;
|
||||
try {
|
||||
yield updater.checkVersions();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
do_check_eq(error.message, "Polling for changes failed.");
|
||||
// When an error occurs, last update was not overwritten (see Date header above).
|
||||
do_check_eq(Services.prefs.getIntPref(PREF_LAST_UPDATE), 2);
|
||||
|
||||
// check negative clock skew times
|
||||
|
||||
// set to a time in the future
|
||||
server.registerPathHandler(changesPath, handleResponse.bind(null, Date.now() + 10000));
|
||||
|
||||
yield updater.checkVersions();
|
||||
|
||||
clockDifference = Services.prefs.getIntPref(PREF_CLOCK_SKEW_SECONDS);
|
||||
// we previously set the serverTime to Date.now() + 10000 ms past epoch
|
||||
do_check_true(clockDifference <= 0 && clockDifference >= -10);
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
// Set up an HTTP Server
|
||||
server = new HttpServer();
|
||||
server.start(-1);
|
||||
|
||||
run_next_test();
|
||||
|
||||
do_register_cleanup(function() {
|
||||
server.stop(function() { });
|
||||
});
|
||||
}
|
||||
|
||||
// get a response for a given request from sample data
|
||||
function getSampleResponse(req, port) {
|
||||
const responses = {
|
||||
"GET:/v1/buckets/monitor/collections/changes/records?": {
|
||||
"sampleHeaders": [
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"ETag: \"1100\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data": [{
|
||||
"host": "localhost",
|
||||
"last_modified": 1100,
|
||||
"bucket": "blocklists:aurora",
|
||||
"id": "330a0c5f-fadf-ff0b-40c8-4eb0d924ff6a",
|
||||
"collection": "test-collection"
|
||||
}, {
|
||||
"host": "localhost",
|
||||
"last_modified": 1000,
|
||||
"bucket": "blocklists",
|
||||
"id": "254cbb9e-6888-4d9f-8e60-58b74faa8778",
|
||||
"collection": "test-collection"
|
||||
}]})
|
||||
}
|
||||
};
|
||||
|
||||
if (req.hasHeader("if-none-match") && req.getHeader("if-none-match", "") == "\"1100\"")
|
||||
return {sampleHeaders: [], status: {status: 304, statusText: "Not Modified"}, responseBody: ""};
|
||||
|
||||
return responses[`${req.method}:${req.path}?${req.queryString}`] ||
|
||||
responses[req.method];
|
||||
}
|
||||
@@ -1,412 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://services-common/kinto-offline-client.js");
|
||||
Cu.import("resource://testing-common/httpd.js");
|
||||
|
||||
const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
|
||||
"nsIBinaryInputStream", "setInputStream");
|
||||
|
||||
var server;
|
||||
|
||||
// set up what we need to make storage adapters
|
||||
const Kinto = loadKinto();
|
||||
const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
|
||||
const kintoFilename = "kinto.sqlite";
|
||||
|
||||
let kintoClient;
|
||||
|
||||
function do_get_kinto_collection() {
|
||||
if (!kintoClient) {
|
||||
let config = {
|
||||
remote:`http://localhost:${server.identity.primaryPort}/v1/`,
|
||||
headers: {Authorization: "Basic " + btoa("user:pass")},
|
||||
adapter: FirefoxAdapter
|
||||
};
|
||||
kintoClient = new Kinto(config);
|
||||
}
|
||||
return kintoClient.collection("test_collection");
|
||||
}
|
||||
|
||||
function* clear_collection() {
|
||||
const collection = do_get_kinto_collection();
|
||||
try {
|
||||
yield collection.db.open();
|
||||
yield collection.clear();
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// test some operations on a local collection
|
||||
add_task(function* test_kinto_add_get() {
|
||||
const collection = do_get_kinto_collection();
|
||||
try {
|
||||
yield collection.db.open();
|
||||
|
||||
let newRecord = { foo: "bar" };
|
||||
// check a record is created
|
||||
let createResult = yield collection.create(newRecord);
|
||||
do_check_eq(createResult.data.foo, newRecord.foo);
|
||||
// check getting the record gets the same info
|
||||
let getResult = yield collection.get(createResult.data.id);
|
||||
deepEqual(createResult.data, getResult.data);
|
||||
// check what happens if we create the same item again (it should throw
|
||||
// since you can't create with id)
|
||||
try {
|
||||
yield collection.create(createResult.data);
|
||||
do_throw("Creation of a record with an id should fail");
|
||||
} catch (err) { }
|
||||
// try a few creates without waiting for the first few to resolve
|
||||
let promises = [];
|
||||
promises.push(collection.create(newRecord));
|
||||
promises.push(collection.create(newRecord));
|
||||
promises.push(collection.create(newRecord));
|
||||
yield collection.create(newRecord);
|
||||
yield Promise.all(promises);
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(clear_collection);
|
||||
|
||||
// test some operations on multiple connections
|
||||
add_task(function* test_kinto_add_get() {
|
||||
const collection1 = do_get_kinto_collection();
|
||||
const collection2 = kintoClient.collection("test_collection_2");
|
||||
|
||||
try {
|
||||
yield collection1.db.open();
|
||||
yield collection2.db.open();
|
||||
|
||||
let newRecord = { foo: "bar" };
|
||||
|
||||
// perform several write operations alternately without waiting for promises
|
||||
// to resolve
|
||||
let promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(collection1.create(newRecord));
|
||||
promises.push(collection2.create(newRecord));
|
||||
}
|
||||
|
||||
// ensure subsequent operations still work
|
||||
yield Promise.all([collection1.create(newRecord),
|
||||
collection2.create(newRecord)]);
|
||||
yield Promise.all(promises);
|
||||
} finally {
|
||||
yield collection1.db.close();
|
||||
yield collection2.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(clear_collection);
|
||||
|
||||
add_task(function* test_kinto_update() {
|
||||
const collection = do_get_kinto_collection();
|
||||
try {
|
||||
yield collection.db.open();
|
||||
const newRecord = { foo: "bar" };
|
||||
// check a record is created
|
||||
let createResult = yield collection.create(newRecord);
|
||||
do_check_eq(createResult.data.foo, newRecord.foo);
|
||||
do_check_eq(createResult.data._status, "created");
|
||||
// check we can update this OK
|
||||
let copiedRecord = Object.assign(createResult.data, {});
|
||||
deepEqual(createResult.data, copiedRecord);
|
||||
copiedRecord.foo = "wibble";
|
||||
let updateResult = yield collection.update(copiedRecord);
|
||||
// check the field was updated
|
||||
do_check_eq(updateResult.data.foo, copiedRecord.foo);
|
||||
// check the status is still "created", since we haven't synced
|
||||
// the record
|
||||
do_check_eq(updateResult.data._status, "created");
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(clear_collection);
|
||||
|
||||
add_task(function* test_kinto_clear() {
|
||||
const collection = do_get_kinto_collection();
|
||||
try {
|
||||
yield collection.db.open();
|
||||
|
||||
// create an expected number of records
|
||||
const expected = 10;
|
||||
const newRecord = { foo: "bar" };
|
||||
for (let i = 0; i < expected; i++) {
|
||||
yield collection.create(newRecord);
|
||||
}
|
||||
// check the collection contains the correct number
|
||||
let list = yield collection.list();
|
||||
do_check_eq(list.data.length, expected);
|
||||
// clear the collection and check again - should be 0
|
||||
yield collection.clear();
|
||||
list = yield collection.list();
|
||||
do_check_eq(list.data.length, 0);
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(clear_collection);
|
||||
|
||||
add_task(function* test_kinto_delete(){
|
||||
const collection = do_get_kinto_collection();
|
||||
try {
|
||||
yield collection.db.open();
|
||||
const newRecord = { foo: "bar" };
|
||||
// check a record is created
|
||||
let createResult = yield collection.create(newRecord);
|
||||
do_check_eq(createResult.data.foo, newRecord.foo);
|
||||
// check getting the record gets the same info
|
||||
let getResult = yield collection.get(createResult.data.id);
|
||||
deepEqual(createResult.data, getResult.data);
|
||||
// delete that record
|
||||
let deleteResult = yield collection.delete(createResult.data.id);
|
||||
// check the ID is set on the result
|
||||
do_check_eq(getResult.data.id, deleteResult.data.id);
|
||||
// and check that get no longer returns the record
|
||||
try {
|
||||
getResult = yield collection.get(createResult.data.id);
|
||||
do_throw("there should not be a result");
|
||||
} catch (e) { }
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* test_kinto_list(){
|
||||
const collection = do_get_kinto_collection();
|
||||
try {
|
||||
yield collection.db.open();
|
||||
const expected = 10;
|
||||
const created = [];
|
||||
for (let i = 0; i < expected; i++) {
|
||||
let newRecord = { foo: "test " + i };
|
||||
let createResult = yield collection.create(newRecord);
|
||||
created.push(createResult.data);
|
||||
}
|
||||
// check the collection contains the correct number
|
||||
let list = yield collection.list();
|
||||
do_check_eq(list.data.length, expected);
|
||||
|
||||
// check that all created records exist in the retrieved list
|
||||
for (let createdRecord of created) {
|
||||
let found = false;
|
||||
for (let retrievedRecord of list.data) {
|
||||
if (createdRecord.id == retrievedRecord.id) {
|
||||
deepEqual(createdRecord, retrievedRecord);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
do_check_true(found);
|
||||
}
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(clear_collection);
|
||||
|
||||
add_task(function* test_loadDump_ignores_already_imported_records(){
|
||||
const collection = do_get_kinto_collection();
|
||||
try {
|
||||
yield collection.db.open();
|
||||
const record = {id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", title: "foo", last_modified: 1457896541};
|
||||
yield collection.loadDump([record]);
|
||||
let impactedRecords = yield collection.loadDump([record]);
|
||||
do_check_eq(impactedRecords.length, 0);
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(clear_collection);
|
||||
|
||||
add_task(function* test_loadDump_should_overwrite_old_records(){
|
||||
const collection = do_get_kinto_collection();
|
||||
try {
|
||||
yield collection.db.open();
|
||||
const record = {id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", title: "foo", last_modified: 1457896541};
|
||||
yield collection.loadDump([record]);
|
||||
const updated = Object.assign({}, record, {last_modified: 1457896543});
|
||||
let impactedRecords = yield collection.loadDump([updated]);
|
||||
do_check_eq(impactedRecords.length, 1);
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(clear_collection);
|
||||
|
||||
add_task(function* test_loadDump_should_not_overwrite_unsynced_records(){
|
||||
const collection = do_get_kinto_collection();
|
||||
try {
|
||||
yield collection.db.open();
|
||||
const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
|
||||
yield collection.create({id: recordId, title: "foo"}, {useRecordId: true});
|
||||
const record = {id: recordId, title: "bar", last_modified: 1457896541};
|
||||
let impactedRecords = yield collection.loadDump([record]);
|
||||
do_check_eq(impactedRecords.length, 0);
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(clear_collection);
|
||||
|
||||
add_task(function* test_loadDump_should_not_overwrite_records_without_last_modified(){
|
||||
const collection = do_get_kinto_collection();
|
||||
try {
|
||||
yield collection.db.open();
|
||||
const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
|
||||
yield collection.create({id: recordId, title: "foo"}, {synced: true});
|
||||
const record = {id: recordId, title: "bar", last_modified: 1457896541};
|
||||
let impactedRecords = yield collection.loadDump([record]);
|
||||
do_check_eq(impactedRecords.length, 0);
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(clear_collection);
|
||||
|
||||
// Now do some sanity checks against a server - we're not looking to test
|
||||
// core kinto.js functionality here (there is excellent test coverage in
|
||||
// kinto.js), more making sure things are basically working as expected.
|
||||
add_task(function* test_kinto_sync(){
|
||||
const configPath = "/v1/";
|
||||
const recordsPath = "/v1/buckets/default/collections/test_collection/records";
|
||||
// register a handler
|
||||
function handleResponse (request, response) {
|
||||
try {
|
||||
const sampled = getSampleResponse(request, server.identity.primaryPort);
|
||||
if (!sampled) {
|
||||
do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
|
||||
}
|
||||
|
||||
response.setStatusLine(null, sampled.status.status,
|
||||
sampled.status.statusText);
|
||||
// send the headers
|
||||
for (let headerLine of sampled.sampleHeaders) {
|
||||
let headerElements = headerLine.split(':');
|
||||
response.setHeader(headerElements[0], headerElements[1].trimLeft());
|
||||
}
|
||||
response.setHeader("Date", (new Date()).toUTCString());
|
||||
|
||||
response.write(sampled.responseBody);
|
||||
} catch (e) {
|
||||
dump(`${e}\n`);
|
||||
}
|
||||
}
|
||||
server.registerPathHandler(configPath, handleResponse);
|
||||
server.registerPathHandler(recordsPath, handleResponse);
|
||||
|
||||
// create an empty collection, sync to populate
|
||||
const collection = do_get_kinto_collection();
|
||||
try {
|
||||
let result;
|
||||
|
||||
yield collection.db.open();
|
||||
result = yield collection.sync();
|
||||
do_check_true(result.ok);
|
||||
|
||||
// our test data has a single record; it should be in the local collection
|
||||
let list = yield collection.list();
|
||||
do_check_eq(list.data.length, 1);
|
||||
|
||||
// now sync again; we should now have 2 records
|
||||
result = yield collection.sync();
|
||||
do_check_true(result.ok);
|
||||
list = yield collection.list();
|
||||
do_check_eq(list.data.length, 2);
|
||||
|
||||
// sync again; the second records should have been modified
|
||||
const before = list.data[0].title;
|
||||
result = yield collection.sync();
|
||||
do_check_true(result.ok);
|
||||
list = yield collection.list();
|
||||
const after = list.data[0].title;
|
||||
do_check_neq(before, after);
|
||||
} finally {
|
||||
yield collection.db.close();
|
||||
}
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
// Set up an HTTP Server
|
||||
server = new HttpServer();
|
||||
server.start(-1);
|
||||
|
||||
run_next_test();
|
||||
|
||||
do_register_cleanup(function() {
|
||||
server.stop(function() { });
|
||||
});
|
||||
}
|
||||
|
||||
// get a response for a given request from sample data
|
||||
function getSampleResponse(req, port) {
|
||||
const responses = {
|
||||
"OPTIONS": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
|
||||
"Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress"
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": "null"
|
||||
},
|
||||
"GET:/v1/?": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress"
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"settings":{"batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
|
||||
},
|
||||
"GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"1445606341071\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{"last_modified":1445606341071, "done":false, "id":"68db8313-686e-4fff-835e-07d78ad6f2af", "title":"New test"}]})
|
||||
},
|
||||
"GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445606341071": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"1445607941223\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{"last_modified":1445607941223, "done":false, "id":"901967b0-f729-4b30-8d8d-499cba7f4b1d", "title":"Another new test"}]})
|
||||
},
|
||||
"GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445607941223": {
|
||||
"sampleHeaders": [
|
||||
"Access-Control-Allow-Origin: *",
|
||||
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
||||
"Content-Type: application/json; charset=UTF-8",
|
||||
"Server: waitress",
|
||||
"Etag: \"1445607541265\""
|
||||
],
|
||||
"status": {status: 200, statusText: "OK"},
|
||||
"responseBody": JSON.stringify({"data":[{"last_modified":1445607541265, "done":false, "id":"901967b0-f729-4b30-8d8d-499cba7f4b1d", "title":"Modified title"}]})
|
||||
}
|
||||
};
|
||||
return responses[`${req.method}:${req.path}?${req.queryString}`] ||
|
||||
responses[req.method];
|
||||
|
||||
}
|
||||
@@ -9,14 +9,6 @@ support-files =
|
||||
# Test load modules first so syntax failures are caught early.
|
||||
[test_load_modules.js]
|
||||
|
||||
[test_blocklist_certificates.js]
|
||||
[test_blocklist_clients.js]
|
||||
[test_blocklist_updater.js]
|
||||
|
||||
[test_kinto.js]
|
||||
[test_blocklist_signatures.js]
|
||||
[test_storage_adapter.js]
|
||||
|
||||
[test_utils_atob.js]
|
||||
[test_utils_convert_string.js]
|
||||
[test_utils_dateprefs.js]
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ['ExtensionStorageEngine', 'EncryptionRemoteTransformer',
|
||||
'KeyRingEncryptionRemoteTransformer'];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-sync/constants.js");
|
||||
Cu.import("resource://services-sync/engines.js");
|
||||
Cu.import("resource://services-sync/keys.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
Cu.import("resource://services-common/async.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorageSync",
|
||||
"resource://gre/modules/ExtensionStorageSync.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
|
||||
"resource://gre/modules/FxAccounts.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
/**
|
||||
* The Engine that manages syncing for the web extension "storage"
|
||||
* API, and in particular ext.storage.sync.
|
||||
*
|
||||
* ext.storage.sync is implemented using Kinto, so it has mechanisms
|
||||
* for syncing that we do not need to integrate in the Firefox Sync
|
||||
* framework, so this is something of a stub.
|
||||
*/
|
||||
this.ExtensionStorageEngine = function ExtensionStorageEngine(service) {
|
||||
SyncEngine.call(this, "Extension-Storage", service);
|
||||
};
|
||||
ExtensionStorageEngine.prototype = {
|
||||
__proto__: SyncEngine.prototype,
|
||||
_trackerObj: ExtensionStorageTracker,
|
||||
// we don't need these since we implement our own sync logic
|
||||
_storeObj: undefined,
|
||||
_recordObj: undefined,
|
||||
|
||||
syncPriority: 10,
|
||||
allowSkippedRecord: false,
|
||||
|
||||
_sync: function () {
|
||||
return Async.promiseSpinningly(ExtensionStorageSync.syncAll());
|
||||
},
|
||||
|
||||
get enabled() {
|
||||
// By default, we sync extension storage if we sync addons. This
|
||||
// lets us simplify the UX since users probably don't consider
|
||||
// "extension preferences" a separate category of syncing.
|
||||
// However, we also respect engine.extension-storage.force, which
|
||||
// can be set to true or false, if a power user wants to customize
|
||||
// the behavior despite the lack of UI.
|
||||
const forced = Svc.Prefs.get("engine." + this.prefName + ".force", undefined);
|
||||
if (forced !== undefined) {
|
||||
return forced;
|
||||
}
|
||||
return Svc.Prefs.get("engine.addons", false);
|
||||
},
|
||||
};
|
||||
|
||||
function ExtensionStorageTracker(name, engine) {
|
||||
Tracker.call(this, name, engine);
|
||||
}
|
||||
ExtensionStorageTracker.prototype = {
|
||||
__proto__: Tracker.prototype,
|
||||
|
||||
startTracking: function () {
|
||||
Svc.Obs.add("ext.storage.sync-changed", this);
|
||||
},
|
||||
|
||||
stopTracking: function () {
|
||||
Svc.Obs.remove("ext.storage.sync-changed", this);
|
||||
},
|
||||
|
||||
observe: function (subject, topic, data) {
|
||||
Tracker.prototype.observe.call(this, subject, topic, data);
|
||||
|
||||
if (this.ignoreAll) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (topic !== "ext.storage.sync-changed") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Single adds, removes and changes are not so important on their
|
||||
// own, so let's just increment score a bit.
|
||||
this.score += SCORE_INCREMENT_MEDIUM;
|
||||
},
|
||||
|
||||
// Override a bunch of methods which don't do anything for us.
|
||||
// This is a performance hack.
|
||||
saveChangedIDs: function() {
|
||||
},
|
||||
loadChangedIDs: function() {
|
||||
},
|
||||
ignoreID: function() {
|
||||
},
|
||||
unignoreID: function() {
|
||||
},
|
||||
addChangedID: function() {
|
||||
},
|
||||
removeChangedID: function() {
|
||||
},
|
||||
clearChangedIDs: function() {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to enforce an order of fields when computing an HMAC.
|
||||
*/
|
||||
function ciphertextHMAC(keyBundle, id, IV, ciphertext) {
|
||||
const hasher = keyBundle.sha256HMACHasher;
|
||||
return Utils.bytesAsHex(Utils.digestUTF8(id + IV + ciphertext, hasher));
|
||||
}
|
||||
|
||||
/**
|
||||
* A "remote transformer" that the Kinto library will use to
|
||||
* encrypt/decrypt records when syncing.
|
||||
*
|
||||
* This is an "abstract base class". Subclass this and override
|
||||
* getKeys() to use it.
|
||||
*/
|
||||
class EncryptionRemoteTransformer {
|
||||
encode(record) {
|
||||
const self = this;
|
||||
return Task.spawn(function* () {
|
||||
const keyBundle = yield self.getKeys();
|
||||
if (record.ciphertext) {
|
||||
throw new Error("Attempt to reencrypt??");
|
||||
}
|
||||
let id = record.id;
|
||||
if (!record.id) {
|
||||
throw new Error("Record ID is missing or invalid");
|
||||
}
|
||||
|
||||
let IV = Svc.Crypto.generateRandomIV();
|
||||
let ciphertext = Svc.Crypto.encrypt(JSON.stringify(record),
|
||||
keyBundle.encryptionKeyB64, IV);
|
||||
let hmac = ciphertextHMAC(keyBundle, id, IV, ciphertext);
|
||||
const encryptedResult = {ciphertext, IV, hmac, id};
|
||||
if (record.hasOwnProperty("last_modified")) {
|
||||
encryptedResult.last_modified = record.last_modified;
|
||||
}
|
||||
return encryptedResult;
|
||||
});
|
||||
}
|
||||
|
||||
decode(record) {
|
||||
const self = this;
|
||||
return Task.spawn(function* () {
|
||||
if (!record.ciphertext) {
|
||||
// This can happen for tombstones if a record is deleted.
|
||||
if (record.deleted) {
|
||||
return record;
|
||||
}
|
||||
throw new Error("No ciphertext: nothing to decrypt?");
|
||||
}
|
||||
const keyBundle = yield self.getKeys();
|
||||
// Authenticate the encrypted blob with the expected HMAC
|
||||
let computedHMAC = ciphertextHMAC(keyBundle, record.id, record.IV, record.ciphertext);
|
||||
|
||||
if (computedHMAC != record.hmac) {
|
||||
Utils.throwHMACMismatch(record.hmac, computedHMAC);
|
||||
}
|
||||
|
||||
// Handle invalid data here. Elsewhere we assume that cleartext is an object.
|
||||
let cleartext = Svc.Crypto.decrypt(record.ciphertext,
|
||||
keyBundle.encryptionKeyB64, record.IV);
|
||||
let jsonResult = JSON.parse(cleartext);
|
||||
if (!jsonResult || typeof jsonResult !== "object") {
|
||||
throw new Error("Decryption failed: result is <" + jsonResult + ">, not an object.");
|
||||
}
|
||||
|
||||
// Verify that the encrypted id matches the requested record's id.
|
||||
// This should always be true, because we compute the HMAC over
|
||||
// the original record's ID, and that was verified already (above).
|
||||
if (jsonResult.id != record.id) {
|
||||
throw new Error("Record id mismatch: " + jsonResult.id + " != " + record.id);
|
||||
}
|
||||
|
||||
if (record.hasOwnProperty("last_modified")) {
|
||||
jsonResult.last_modified = record.last_modified;
|
||||
}
|
||||
|
||||
return jsonResult;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve keys to use during encryption.
|
||||
*
|
||||
* Returns a Promise<KeyBundle>.
|
||||
*/
|
||||
getKeys() {
|
||||
throw new Error("override getKeys in a subclass");
|
||||
}
|
||||
}
|
||||
// You can inject this
|
||||
EncryptionRemoteTransformer.prototype._fxaService = fxAccounts;
|
||||
|
||||
/**
|
||||
* An EncryptionRemoteTransformer that provides a keybundle derived
|
||||
* from the user's kB, suitable for encrypting a keyring.
|
||||
*/
|
||||
class KeyRingEncryptionRemoteTransformer extends EncryptionRemoteTransformer {
|
||||
getKeys() {
|
||||
const self = this;
|
||||
return Task.spawn(function* () {
|
||||
const user = yield self._fxaService.getSignedInUser();
|
||||
// FIXME: we should permit this if the user is self-hosting
|
||||
// their storage
|
||||
if (!user) {
|
||||
throw new Error("user isn't signed in to FxA; can't sync");
|
||||
}
|
||||
|
||||
if (!user.kB) {
|
||||
throw new Error("user doesn't have kB");
|
||||
}
|
||||
|
||||
let kB = Utils.hexToBytes(user.kB);
|
||||
|
||||
let keyMaterial = CryptoUtils.hkdf(kB, undefined,
|
||||
"identity.mozilla.com/picl/v1/chrome.storage.sync", 2*32);
|
||||
let bundle = new BulkKeyBundle();
|
||||
// [encryptionKey, hmacKey]
|
||||
bundle.keyPair = [keyMaterial.slice(0, 32), keyMaterial.slice(32, 64)];
|
||||
return bundle;
|
||||
});
|
||||
}
|
||||
// Pass through the kbHash field from the unencrypted record. If
|
||||
// encryption fails, we can use this to try to detect whether we are
|
||||
// being compromised or if the record here was encoded with a
|
||||
// different kB.
|
||||
encode(record) {
|
||||
const encodePromise = super.encode(record);
|
||||
return Task.spawn(function* () {
|
||||
const encoded = yield encodePromise;
|
||||
encoded.kbHash = record.kbHash;
|
||||
return encoded;
|
||||
});
|
||||
}
|
||||
|
||||
decode(record) {
|
||||
const decodePromise = super.decode(record);
|
||||
return Task.spawn(function* () {
|
||||
try {
|
||||
return yield decodePromise;
|
||||
} catch (e) {
|
||||
if (Utils.isHMACMismatch(e)) {
|
||||
const currentKBHash = yield ExtensionStorageSync.getKBHash();
|
||||
if (record.kbHash != currentKBHash) {
|
||||
// Some other client encoded this with a kB that we don't
|
||||
// have access to.
|
||||
KeyRingEncryptionRemoteTransformer.throwOutdatedKB(currentKBHash, record.kbHash);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Generator and discriminator for KB-is-outdated exceptions.
|
||||
static throwOutdatedKB(shouldBe, is) {
|
||||
throw new Error(`kB hash on record is outdated: should be ${shouldBe}, is ${is}`);
|
||||
}
|
||||
|
||||
static isOutdatedKB(exc) {
|
||||
const kbMessage = "kB hash on record is outdated: ";
|
||||
return exc && exc.message && exc.message.indexOf &&
|
||||
(exc.message.indexOf(kbMessage) == 0);
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,6 @@ const ENGINE_MODULES = {
|
||||
Password: "passwords.js",
|
||||
Prefs: "prefs.js",
|
||||
Tab: "tabs.js",
|
||||
ExtensionStorage: "extension-storage.js",
|
||||
};
|
||||
|
||||
const STORAGE_INFO_TYPES = [INFO_COLLECTIONS,
|
||||
|
||||
@@ -52,7 +52,6 @@ EXTRA_JS_MODULES['services-sync'].engines += [
|
||||
'modules/engines/addons.js',
|
||||
'modules/engines/bookmarks.js',
|
||||
'modules/engines/clients.js',
|
||||
'modules/engines/extension-storage.js',
|
||||
'modules/engines/forms.js',
|
||||
'modules/engines/history.js',
|
||||
'modules/engines/passwords.js',
|
||||
|
||||
@@ -119,8 +119,6 @@ user_pref("extensions.getAddons.get.url", "http://%(server)s/extensions-dummy/re
|
||||
user_pref("extensions.getAddons.getWithPerformance.url", "http://%(server)s/extensions-dummy/repositoryGetWithPerformanceURL");
|
||||
user_pref("extensions.getAddons.search.browseURL", "http://%(server)s/extensions-dummy/repositoryBrowseURL");
|
||||
user_pref("extensions.getAddons.search.url", "http://%(server)s/extensions-dummy/repositorySearchURL");
|
||||
// Ensure blocklist updates don't hit the network
|
||||
user_pref("services.settings.server", "http://%(server)s/dummy-kinto/v1");
|
||||
// Make sure SNTP requests don't hit the network
|
||||
user_pref("network.sntp.pools", "%(server)s");
|
||||
// We know the SNTP request will fail, since localhost isn't listening on
|
||||
|
||||
@@ -1,848 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
// TODO:
|
||||
// * find out how the Chrome implementation deals with conflicts
|
||||
|
||||
"use strict";
|
||||
|
||||
/* exported extensionIdToCollectionId */
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["ExtensionStorageSync"];
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
const global = this;
|
||||
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
const KINTO_PROD_SERVER_URL = "https://webextensions.settings.services.mozilla.com/v1";
|
||||
const KINTO_DEV_SERVER_URL = "https://webextensions.dev.mozaws.net/v1";
|
||||
const KINTO_DEFAULT_SERVER_URL = AppConstants.RELEASE_OR_BETA ? KINTO_PROD_SERVER_URL : KINTO_DEV_SERVER_URL;
|
||||
|
||||
const STORAGE_SYNC_ENABLED_PREF = "webextensions.storage.sync.enabled";
|
||||
const STORAGE_SYNC_SERVER_URL_PREF = "webextensions.storage.sync.serverURL";
|
||||
const STORAGE_SYNC_SCOPE = "sync:addon_storage";
|
||||
const STORAGE_SYNC_CRYPTO_COLLECTION_NAME = "storage-sync-crypto";
|
||||
const STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID = "keys";
|
||||
const FXA_OAUTH_OPTIONS = {
|
||||
scope: STORAGE_SYNC_SCOPE,
|
||||
};
|
||||
// Default is 5sec, which seems a bit aggressive on the open internet
|
||||
const KINTO_REQUEST_TIMEOUT = 30000;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
const {
|
||||
runSafeSyncWithoutClone,
|
||||
} = Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AppsUtils",
|
||||
"resource://gre/modules/AppsUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
||||
"resource://gre/modules/AsyncShutdown.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CollectionKeyManager",
|
||||
"resource://services-sync/record.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
|
||||
"resource://services-common/utils.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
|
||||
"resource://services-crypto/utils.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "EncryptionRemoteTransformer",
|
||||
"resource://services-sync/engines/extension-storage.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
|
||||
"resource://gre/modules/ExtensionStorage.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
|
||||
"resource://gre/modules/FxAccounts.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "KintoHttpClient",
|
||||
"resource://services-common/kinto-http-client.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "loadKinto",
|
||||
"resource://services-common/kinto-offline-client.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Log",
|
||||
"resource://gre/modules/Log.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Observers",
|
||||
"resource://services-common/observers.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
||||
"resource://gre/modules/Sqlite.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "KeyRingEncryptionRemoteTransformer",
|
||||
"resource://services-sync/engines/extension-storage.js");
|
||||
XPCOMUtils.defineLazyPreferenceGetter(this, "prefPermitsStorageSync",
|
||||
STORAGE_SYNC_ENABLED_PREF, false);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(this, "prefStorageSyncServerURL",
|
||||
STORAGE_SYNC_SERVER_URL_PREF,
|
||||
KINTO_DEFAULT_SERVER_URL);
|
||||
|
||||
/* globals prefPermitsStorageSync, prefStorageSyncServerURL */
|
||||
|
||||
// Map of Extensions to Set<Contexts> to track contexts that are still
|
||||
// "live" and use storage.sync.
|
||||
const extensionContexts = new Map();
|
||||
// Borrow logger from Sync.
|
||||
const log = Log.repository.getLogger("Sync.Engine.Extension-Storage");
|
||||
|
||||
/**
|
||||
* A Promise that centralizes initialization of ExtensionStorageSync.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
const storageSyncInit = Task.spawn(function* () {
|
||||
const Kinto = loadKinto();
|
||||
const path = "storage-sync.sqlite";
|
||||
const opts = {path, sharedMemoryCache: false};
|
||||
const connection = yield Sqlite.openConnection(opts);
|
||||
yield Kinto.adapters.FirefoxAdapter._init(connection);
|
||||
return {
|
||||
connection,
|
||||
kinto: new Kinto({
|
||||
adapter: Kinto.adapters.FirefoxAdapter,
|
||||
adapterOptions: {sqliteHandle: connection},
|
||||
timeout: KINTO_REQUEST_TIMEOUT,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
AsyncShutdown.profileBeforeChange.addBlocker(
|
||||
"ExtensionStorageSync: close Sqlite handle",
|
||||
Task.async(function* () {
|
||||
const ret = yield storageSyncInit;
|
||||
const {connection} = ret;
|
||||
yield 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
|
||||
// all keys with "key-".
|
||||
function keyToId(key) {
|
||||
function escapeChar(match) {
|
||||
return "_" + match.codePointAt(0).toString(16).toUpperCase() + "_";
|
||||
}
|
||||
return "key-" + key.replace(/[^a-zA-Z0-9]/g, escapeChar);
|
||||
}
|
||||
|
||||
// Convert a Kinto ID back into a chrome.storage key.
|
||||
// Returns null if a key couldn't be parsed.
|
||||
function idToKey(id) {
|
||||
function unescapeNumber(match, group1) {
|
||||
return String.fromCodePoint(parseInt(group1, 16));
|
||||
}
|
||||
// An escaped ID should match this regex.
|
||||
// An escaped ID should consist of only letters and numbers, plus
|
||||
// code points escaped as _[0-9a-f]+_.
|
||||
const ESCAPED_ID_FORMAT = /^(?:[a-zA-Z0-9]|_[0-9A-F]+_)*$/;
|
||||
|
||||
if (!id.startsWith("key-")) {
|
||||
return null;
|
||||
}
|
||||
const unprefixed = id.slice(4);
|
||||
// Verify that the ID is the correct format.
|
||||
if (!ESCAPED_ID_FORMAT.test(unprefixed)) {
|
||||
return null;
|
||||
}
|
||||
return unprefixed.replace(/_([0-9A-F]+)_/g, unescapeNumber);
|
||||
}
|
||||
|
||||
// An "id schema" used to validate Kinto IDs and generate new ones.
|
||||
const storageSyncIdSchema = {
|
||||
// We should never generate IDs; chrome.storage only acts as a
|
||||
// key-value store, so we should always have a key.
|
||||
generate() {
|
||||
throw new Error("cannot generate IDs");
|
||||
},
|
||||
|
||||
// See keyToId and idToKey for more details.
|
||||
validate(id) {
|
||||
return idToKey(id) !== null;
|
||||
},
|
||||
};
|
||||
|
||||
// An "id schema" used for the system collection, which doesn't
|
||||
// require validation or generation of IDs.
|
||||
const cryptoCollectionIdSchema = {
|
||||
generate() {
|
||||
throw new Error("cannot generate IDs for system collection");
|
||||
},
|
||||
|
||||
validate(id) {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
let cryptoCollection, CollectionKeyEncryptionRemoteTransformer;
|
||||
if (AppConstants.platform != "android") {
|
||||
/**
|
||||
* Wrapper around the crypto collection providing some handy utilities.
|
||||
*/
|
||||
cryptoCollection = this.cryptoCollection = {
|
||||
getCollection: Task.async(function* () {
|
||||
const {kinto} = yield storageSyncInit;
|
||||
return kinto.collection(STORAGE_SYNC_CRYPTO_COLLECTION_NAME, {
|
||||
idSchema: cryptoCollectionIdSchema,
|
||||
remoteTransformers: [new KeyRingEncryptionRemoteTransformer()],
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* Retrieve the keyring record from the crypto collection.
|
||||
*
|
||||
* You can use this if you want to check metadata on the keyring
|
||||
* record rather than use the keyring itself.
|
||||
*
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
getKeyRingRecord: Task.async(function* () {
|
||||
const collection = yield this.getCollection();
|
||||
const cryptoKeyRecord = yield collection.getAny(STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID);
|
||||
|
||||
let data = cryptoKeyRecord.data;
|
||||
if (!data) {
|
||||
// This is a new keyring. Invent an ID for this record. If this
|
||||
// changes, it means a client replaced the keyring, so we need to
|
||||
// reupload everything.
|
||||
const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
||||
const uuid = uuidgen.generateUUID().toString();
|
||||
data = {uuid};
|
||||
}
|
||||
return data;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Retrieve the actual keyring from the crypto collection.
|
||||
*
|
||||
* @returns {Promise<CollectionKeyManager>}
|
||||
*/
|
||||
getKeyRing: Task.async(function* () {
|
||||
const cryptoKeyRecord = yield this.getKeyRingRecord();
|
||||
const collectionKeys = new CollectionKeyManager();
|
||||
if (cryptoKeyRecord.keys) {
|
||||
collectionKeys.setContents(cryptoKeyRecord.keys, cryptoKeyRecord.last_modified);
|
||||
} else {
|
||||
// We never actually use the default key, so it's OK if we
|
||||
// generate one multiple times.
|
||||
collectionKeys.generateDefaultKey();
|
||||
}
|
||||
// Pass through uuid field so that we can save it if we need to.
|
||||
collectionKeys.uuid = cryptoKeyRecord.uuid;
|
||||
return collectionKeys;
|
||||
}),
|
||||
|
||||
updateKBHash: Task.async(function* (kbHash) {
|
||||
const coll = yield this.getCollection();
|
||||
yield coll.update({id: STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID,
|
||||
kbHash: kbHash},
|
||||
{patch: true});
|
||||
}),
|
||||
|
||||
upsert: Task.async(function* (record) {
|
||||
const collection = yield this.getCollection();
|
||||
yield collection.upsert(record);
|
||||
}),
|
||||
|
||||
sync: Task.async(function* () {
|
||||
const collection = yield this.getCollection();
|
||||
return yield ExtensionStorageSync._syncCollection(collection, {
|
||||
strategy: "server_wins",
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* Reset sync status for ALL collections by directly
|
||||
* accessing the FirefoxAdapter.
|
||||
*/
|
||||
resetSyncStatus: Task.async(function* () {
|
||||
const coll = yield this.getCollection();
|
||||
yield coll.db.resetSyncStatus();
|
||||
}),
|
||||
|
||||
// Used only for testing.
|
||||
_clear: Task.async(function* () {
|
||||
const collection = yield this.getCollection();
|
||||
yield collection.clear();
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* An EncryptionRemoteTransformer that uses the special "keys" record
|
||||
* to find a key for a given extension.
|
||||
*
|
||||
* @param {string} extensionId The extension ID for which to find a key.
|
||||
*/
|
||||
CollectionKeyEncryptionRemoteTransformer = class extends EncryptionRemoteTransformer {
|
||||
constructor(extensionId) {
|
||||
super();
|
||||
this.extensionId = extensionId;
|
||||
}
|
||||
|
||||
getKeys() {
|
||||
const self = this;
|
||||
return Task.spawn(function* () {
|
||||
// FIXME: cache the crypto record for the duration of a sync cycle?
|
||||
const collectionKeys = yield cryptoCollection.getKeyRing();
|
||||
if (!collectionKeys.hasKeysFor([self.extensionId])) {
|
||||
// This should never happen. Keys should be created (and
|
||||
// synced) at the beginning of the sync cycle.
|
||||
throw new Error(`tried to encrypt records for ${this.extensionId}, but key is not present`);
|
||||
}
|
||||
return collectionKeys.keyForCollection(self.extensionId);
|
||||
});
|
||||
}
|
||||
};
|
||||
global.CollectionKeyEncryptionRemoteTransformer = CollectionKeyEncryptionRemoteTransformer;
|
||||
}
|
||||
/**
|
||||
* Clean up now that one context is no longer using this extension's collection.
|
||||
*
|
||||
* @param {Extension} extension
|
||||
* The extension whose context just ended.
|
||||
* @param {Context} context
|
||||
* The context that just ended.
|
||||
*/
|
||||
function cleanUpForContext(extension, context) {
|
||||
const contexts = extensionContexts.get(extension);
|
||||
if (!contexts) {
|
||||
Cu.reportError(new Error(`Internal error: cannot find any contexts for extension ${extension.id}`));
|
||||
}
|
||||
contexts.delete(context);
|
||||
if (contexts.size === 0) {
|
||||
// Nobody else is using this collection. Clean up.
|
||||
extensionContexts.delete(extension);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a promise that produces the Collection for an extension.
|
||||
*
|
||||
* @param {Extension} extension
|
||||
* The extension whose collection needs to
|
||||
* be opened.
|
||||
* @param {Context} context
|
||||
* The context for this extension. The Collection
|
||||
* will shut down automatically when all contexts
|
||||
* close.
|
||||
* @returns {Promise<Collection>}
|
||||
*/
|
||||
const openCollection = Task.async(function* (extension, context) {
|
||||
let collectionId = extension.id;
|
||||
const {kinto} = yield storageSyncInit;
|
||||
const remoteTransformers = [];
|
||||
if (CollectionKeyEncryptionRemoteTransformer) {
|
||||
remoteTransformers.push(new CollectionKeyEncryptionRemoteTransformer(extension.id));
|
||||
}
|
||||
const coll = kinto.collection(collectionId, {
|
||||
idSchema: storageSyncIdSchema,
|
||||
remoteTransformers,
|
||||
});
|
||||
return coll;
|
||||
});
|
||||
|
||||
/**
|
||||
* Hash an extension ID for a given user so that an attacker can't
|
||||
* identify the extensions a user has installed.
|
||||
*
|
||||
* @param {User} user
|
||||
* The user for whom to choose a collection to sync
|
||||
* an extension to.
|
||||
* @param {string} extensionId The extension ID to obfuscate.
|
||||
* @returns {string} A collection ID suitable for use to sync to.
|
||||
*/
|
||||
function extensionIdToCollectionId(user, extensionId) {
|
||||
const userFingerprint = CryptoUtils.hkdf(user.uid, undefined,
|
||||
"identity.mozilla.com/picl/v1/chrome.storage.sync.collectionIds", 2 * 32);
|
||||
let data = new TextEncoder().encode(userFingerprint + extensionId);
|
||||
let hasher = Cc["@mozilla.org/security/hash;1"]
|
||||
.createInstance(Ci.nsICryptoHash);
|
||||
hasher.init(hasher.SHA256);
|
||||
hasher.update(data, data.length);
|
||||
|
||||
return CommonUtils.bytesAsHex(hasher.finish(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that we were built on not-Android. Call this as a sanity
|
||||
* check before using cryptoCollection.
|
||||
*/
|
||||
function ensureCryptoCollection() {
|
||||
if (!cryptoCollection) {
|
||||
throw new Error("Call to ensureKeysFor, but no sync code; are you on Android?");
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This is kind of ugly. Probably we should have
|
||||
// ExtensionStorageSync not be a singleton, but a constructed object,
|
||||
// and this should be a constructor argument.
|
||||
let _fxaService = null;
|
||||
if (AppConstants.platform != "android") {
|
||||
_fxaService = fxAccounts;
|
||||
}
|
||||
|
||||
this.ExtensionStorageSync = {
|
||||
_fxaService,
|
||||
listeners: new WeakMap(),
|
||||
|
||||
syncAll: Task.async(function* () {
|
||||
const extensions = extensionContexts.keys();
|
||||
const extIds = Array.from(extensions, extension => extension.id);
|
||||
log.debug(`Syncing extension settings for ${JSON.stringify(extIds)}\n`);
|
||||
if (extIds.length == 0) {
|
||||
// No extensions to sync. Get out.
|
||||
return;
|
||||
}
|
||||
yield this.ensureKeysFor(extIds);
|
||||
yield this.checkSyncKeyRing();
|
||||
const promises = Array.from(extensionContexts.keys(), extension => {
|
||||
return openCollection(extension).then(coll => {
|
||||
return this.sync(extension, coll);
|
||||
});
|
||||
});
|
||||
yield Promise.all(promises);
|
||||
}),
|
||||
|
||||
sync: Task.async(function* (extension, collection) {
|
||||
const signedInUser = yield this._fxaService.getSignedInUser();
|
||||
if (!signedInUser) {
|
||||
// FIXME: this should support syncing to self-hosted
|
||||
log.info("User was not signed into FxA; cannot sync");
|
||||
throw new Error("Not signed in to FxA");
|
||||
}
|
||||
const collectionId = extensionIdToCollectionId(signedInUser, extension.id);
|
||||
let syncResults;
|
||||
try {
|
||||
syncResults = yield this._syncCollection(collection, {
|
||||
strategy: "client_wins",
|
||||
collection: collectionId,
|
||||
});
|
||||
} catch (err) {
|
||||
log.warn("Syncing failed", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
let changes = {};
|
||||
for (const record of syncResults.created) {
|
||||
changes[record.key] = {
|
||||
newValue: record.data,
|
||||
};
|
||||
}
|
||||
for (const record of syncResults.updated) {
|
||||
// N.B. It's safe to just pick old.key because it's not
|
||||
// possible to "rename" a record in the storage.sync API.
|
||||
const key = record.old.key;
|
||||
changes[key] = {
|
||||
oldValue: record.old.data,
|
||||
newValue: record.new.data,
|
||||
};
|
||||
}
|
||||
for (const record of syncResults.deleted) {
|
||||
changes[record.key] = {
|
||||
oldValue: record.data,
|
||||
};
|
||||
}
|
||||
for (const conflict of syncResults.resolved) {
|
||||
// FIXME: Should we even send a notification? If so, what
|
||||
// best values for "old" and "new"? This might violate
|
||||
// client code's assumptions, since from their perspective,
|
||||
// we were in state L, but this diff is from R -> L.
|
||||
changes[conflict.remote.key] = {
|
||||
oldValue: conflict.local.data,
|
||||
newValue: conflict.remote.data,
|
||||
};
|
||||
}
|
||||
if (Object.keys(changes).length > 0) {
|
||||
this.notifyListeners(extension, changes);
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Utility function that handles the common stuff about syncing all
|
||||
* Kinto collections (including "meta" collections like the crypto
|
||||
* one).
|
||||
*
|
||||
* @param {Collection} collection
|
||||
* @param {Object} options
|
||||
* Additional options to be passed to sync().
|
||||
* @returns {Promise<SyncResultObject>}
|
||||
*/
|
||||
_syncCollection: Task.async(function* (collection, options) {
|
||||
// FIXME: this should support syncing to self-hosted
|
||||
return yield this._requestWithToken(`Syncing ${collection.name}`, function* (token) {
|
||||
const allOptions = Object.assign({}, {
|
||||
remote: prefStorageSyncServerURL,
|
||||
headers: {
|
||||
Authorization: "Bearer " + token,
|
||||
},
|
||||
}, options);
|
||||
|
||||
return yield collection.sync(allOptions);
|
||||
});
|
||||
}),
|
||||
|
||||
// Make a Kinto request with a current FxA token.
|
||||
// If the response indicates that the token might have expired,
|
||||
// retry the request.
|
||||
_requestWithToken: Task.async(function* (description, f) {
|
||||
const fxaToken = yield this._fxaService.getOAuthToken(FXA_OAUTH_OPTIONS);
|
||||
try {
|
||||
return yield f(fxaToken);
|
||||
} catch (e) {
|
||||
log.error(`${description}: request failed`, e);
|
||||
if (e && e.data && e.data.code == 401) {
|
||||
// Our token might have expired. Refresh and retry.
|
||||
log.info("Token might have expired");
|
||||
yield this._fxaService.removeCachedOAuthToken({token: fxaToken});
|
||||
const newToken = yield this._fxaService.getOAuthToken(FXA_OAUTH_OPTIONS);
|
||||
|
||||
// If this fails too, let it go.
|
||||
return yield f(newToken);
|
||||
}
|
||||
// Otherwise, we don't know how to handle this error, so just reraise.
|
||||
throw e;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Helper similar to _syncCollection, but for deleting the user's bucket.
|
||||
*/
|
||||
_deleteBucket: Task.async(function* () {
|
||||
return yield this._requestWithToken("Clearing server", function* (token) {
|
||||
const headers = {Authorization: "Bearer " + token};
|
||||
const kintoHttp = new KintoHttpClient(prefStorageSyncServerURL, {
|
||||
headers: headers,
|
||||
timeout: KINTO_REQUEST_TIMEOUT,
|
||||
});
|
||||
return yield kintoHttp.deleteBucket("default");
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* Recursive promise that terminates when our local collectionKeys,
|
||||
* as well as that on the server, have keys for all the extensions
|
||||
* in extIds.
|
||||
*
|
||||
* @param {Array<string>} extIds
|
||||
* The IDs of the extensions which need keys.
|
||||
* @returns {Promise<CollectionKeyManager>}
|
||||
*/
|
||||
ensureKeysFor: Task.async(function* (extIds) {
|
||||
ensureCryptoCollection();
|
||||
|
||||
const collectionKeys = yield cryptoCollection.getKeyRing();
|
||||
if (collectionKeys.hasKeysFor(extIds)) {
|
||||
return collectionKeys;
|
||||
}
|
||||
|
||||
const kbHash = yield this.getKBHash();
|
||||
const newKeys = yield collectionKeys.ensureKeysFor(extIds);
|
||||
const newRecord = {
|
||||
id: STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID,
|
||||
keys: newKeys.asWBO().cleartext,
|
||||
uuid: collectionKeys.uuid,
|
||||
// Add a field for the current kB hash.
|
||||
kbHash: kbHash,
|
||||
};
|
||||
yield cryptoCollection.upsert(newRecord);
|
||||
const result = yield this._syncKeyRing(newRecord);
|
||||
if (result.resolved.length != 0) {
|
||||
// We had a conflict which was automatically resolved. We now
|
||||
// have a new keyring which might have keys for the
|
||||
// collections. Recurse.
|
||||
return yield this.ensureKeysFor(extIds);
|
||||
}
|
||||
|
||||
// No conflicts. We're good.
|
||||
return newKeys;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get the current user's hashed kB.
|
||||
*
|
||||
* @returns sha256 of the user's kB as a hex string
|
||||
*/
|
||||
getKBHash: Task.async(function* () {
|
||||
const signedInUser = yield this._fxaService.getSignedInUser();
|
||||
if (!signedInUser) {
|
||||
throw new Error("User isn't signed in!");
|
||||
}
|
||||
|
||||
if (!signedInUser.kB) {
|
||||
throw new Error("User doesn't have kB??");
|
||||
}
|
||||
|
||||
let kBbytes = CommonUtils.hexToBytes(signedInUser.kB);
|
||||
let hasher = Cc["@mozilla.org/security/hash;1"]
|
||||
.createInstance(Ci.nsICryptoHash);
|
||||
hasher.init(hasher.SHA256);
|
||||
return CommonUtils.bytesAsHex(CryptoUtils.digestBytes(signedInUser.uid + kBbytes, hasher));
|
||||
}),
|
||||
|
||||
/**
|
||||
* Update the kB in the crypto record.
|
||||
*/
|
||||
updateKeyRingKB: Task.async(function* () {
|
||||
ensureCryptoCollection();
|
||||
|
||||
const signedInUser = yield this._fxaService.getSignedInUser();
|
||||
if (!signedInUser) {
|
||||
// Although this function is meant to be called on login,
|
||||
// it's not unreasonable to check any time, even if we aren't
|
||||
// logged in.
|
||||
//
|
||||
// If we aren't logged in, we don't have any information about
|
||||
// the user's kB, so we can't be sure that the user changed
|
||||
// their kB, so just return.
|
||||
return;
|
||||
}
|
||||
|
||||
const thisKBHash = yield this.getKBHash();
|
||||
yield cryptoCollection.updateKBHash(thisKBHash);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Make sure the keyring is up to date and synced.
|
||||
*
|
||||
* This is called on syncs to make sure that we don't sync anything
|
||||
* to any collection unless the key for that collection is on the
|
||||
* server.
|
||||
*/
|
||||
checkSyncKeyRing: Task.async(function* () {
|
||||
ensureCryptoCollection();
|
||||
|
||||
yield this.updateKeyRingKB();
|
||||
|
||||
const cryptoKeyRecord = yield cryptoCollection.getKeyRingRecord();
|
||||
if (cryptoKeyRecord && cryptoKeyRecord._status !== "synced") {
|
||||
// We haven't successfully synced the keyring since the last
|
||||
// change. This could be because kB changed and we touched the
|
||||
// keyring, or it could be because we failed to sync after
|
||||
// adding a key. Either way, take this opportunity to sync the
|
||||
// keyring.
|
||||
yield this._syncKeyRing(cryptoKeyRecord);
|
||||
}
|
||||
}),
|
||||
|
||||
_syncKeyRing: Task.async(function* (cryptoKeyRecord) {
|
||||
ensureCryptoCollection();
|
||||
|
||||
try {
|
||||
// Try to sync using server_wins.
|
||||
//
|
||||
// We use server_wins here because whatever is on the server is
|
||||
// at least consistent with itself -- the crypto in the keyring
|
||||
// matches the crypto on the collection records. This is because
|
||||
// we generate and upload keys just before syncing data.
|
||||
//
|
||||
// It's possible that we can't decode the version on the server.
|
||||
// This can happen if a user is locked out of their account, and
|
||||
// does a "reset password" to get in on a new device. In this
|
||||
// case, we are in a bind -- we can't decrypt the record on the
|
||||
// server, so we can't merge keys. If this happens, we try to
|
||||
// figure out if we're the one with the correct (new) kB or if
|
||||
// we just got locked out because we have the old kB. If we're
|
||||
// the one with the correct kB, we wipe the server and reupload
|
||||
// everything, including a new keyring.
|
||||
//
|
||||
// If another device has wiped the server, we need to reupload
|
||||
// everything we have on our end too, so we detect this by
|
||||
// adding a UUID to the keyring. UUIDs are preserved throughout
|
||||
// the lifetime of a keyring, so the only time a keyring UUID
|
||||
// changes is when a new keyring is uploaded, which only happens
|
||||
// after a server wipe. So when we get a "conflict" (resolved by
|
||||
// server_wins), we check whether the server version has a new
|
||||
// UUID. If so, reset our sync status, so that we'll reupload
|
||||
// everything.
|
||||
const result = yield cryptoCollection.sync();
|
||||
if (result.resolved.length > 0) {
|
||||
if (result.resolved[0].uuid != cryptoKeyRecord.uuid) {
|
||||
log.info(`Detected a new UUID (${result.resolved[0].uuid}, was ${cryptoKeyRecord.uuid}). Reseting sync status for everything.`);
|
||||
yield cryptoCollection.resetSyncStatus();
|
||||
|
||||
// Server version is now correct. Return that result.
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// No conflicts, or conflict was just someone else adding keys.
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (KeyRingEncryptionRemoteTransformer.isOutdatedKB(e)) {
|
||||
// Check if our token is still valid, or if we got locked out
|
||||
// between starting the sync and talking to Kinto.
|
||||
const isSessionValid = yield this._fxaService.sessionStatus();
|
||||
if (isSessionValid) {
|
||||
yield this._deleteBucket();
|
||||
yield cryptoCollection.resetSyncStatus();
|
||||
|
||||
// Reupload our keyring, which is the only new keyring.
|
||||
// We don't want client_wins here because another device
|
||||
// could have uploaded another keyring in the meantime.
|
||||
return yield cryptoCollection.sync();
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get the collection for an extension, and register the extension
|
||||
* as being "in use".
|
||||
*
|
||||
* @param {Extension} extension
|
||||
* The extension for which we are seeking
|
||||
* a collection.
|
||||
* @param {Context} context
|
||||
* The context of the extension, so that we can
|
||||
* stop syncing the collection when the extension ends.
|
||||
* @returns {Promise<Collection>}
|
||||
*/
|
||||
getCollection(extension, context) {
|
||||
if (prefPermitsStorageSync !== true) {
|
||||
return Promise.reject({message: `Please set ${STORAGE_SYNC_ENABLED_PREF} to true in about:config`});
|
||||
}
|
||||
// Register that the extension and context are in use.
|
||||
if (!extensionContexts.has(extension)) {
|
||||
extensionContexts.set(extension, new Set());
|
||||
}
|
||||
const contexts = extensionContexts.get(extension);
|
||||
if (!contexts.has(context)) {
|
||||
// New context. Register it and make sure it cleans itself up
|
||||
// when it closes.
|
||||
contexts.add(context);
|
||||
context.callOnClose({
|
||||
close: () => cleanUpForContext(extension, context),
|
||||
});
|
||||
}
|
||||
|
||||
return openCollection(extension, context);
|
||||
},
|
||||
|
||||
set: Task.async(function* (extension, items, context) {
|
||||
const coll = yield this.getCollection(extension, context);
|
||||
const keys = Object.keys(items);
|
||||
const ids = keys.map(keyToId);
|
||||
const changes = yield coll.execute(txn => {
|
||||
let changes = {};
|
||||
for (let [i, key] of keys.entries()) {
|
||||
const id = ids[i];
|
||||
let item = items[key];
|
||||
let {oldRecord} = txn.upsert({
|
||||
id,
|
||||
key,
|
||||
data: item,
|
||||
});
|
||||
changes[key] = {
|
||||
newValue: item,
|
||||
};
|
||||
if (oldRecord && oldRecord.data) {
|
||||
// Extract the "data" field from the old record, which
|
||||
// represents the value part of the key-value store
|
||||
changes[key].oldValue = oldRecord.data;
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}, {preloadIds: ids});
|
||||
this.notifyListeners(extension, changes);
|
||||
}),
|
||||
|
||||
remove: Task.async(function* (extension, keys, context) {
|
||||
const coll = yield this.getCollection(extension, context);
|
||||
keys = [].concat(keys);
|
||||
const ids = keys.map(keyToId);
|
||||
let changes = {};
|
||||
yield coll.execute(txn => {
|
||||
for (let [i, key] of keys.entries()) {
|
||||
const id = ids[i];
|
||||
const res = txn.deleteAny(id);
|
||||
if (res.deleted) {
|
||||
changes[key] = {
|
||||
oldValue: res.data.data,
|
||||
};
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}, {preloadIds: ids});
|
||||
if (Object.keys(changes).length > 0) {
|
||||
this.notifyListeners(extension, changes);
|
||||
}
|
||||
}),
|
||||
|
||||
clear: Task.async(function* (extension, context) {
|
||||
// We can't call Collection#clear here, because that just clears
|
||||
// the local database. We have to explicitly delete everything so
|
||||
// that the deletions can be synced as well.
|
||||
const coll = yield this.getCollection(extension, context);
|
||||
const res = yield coll.list();
|
||||
const records = res.data;
|
||||
const keys = records.map(record => record.key);
|
||||
yield this.remove(extension, keys, context);
|
||||
}),
|
||||
|
||||
get: Task.async(function* (extension, spec, context) {
|
||||
const coll = yield this.getCollection(extension, context);
|
||||
let keys, records;
|
||||
if (spec === null) {
|
||||
records = {};
|
||||
const res = yield coll.list();
|
||||
for (let record of res.data) {
|
||||
records[record.key] = record.data;
|
||||
}
|
||||
return records;
|
||||
}
|
||||
if (typeof spec === "string") {
|
||||
keys = [spec];
|
||||
records = {};
|
||||
} else if (Array.isArray(spec)) {
|
||||
keys = spec;
|
||||
records = {};
|
||||
} else {
|
||||
keys = Object.keys(spec);
|
||||
records = Cu.cloneInto(spec, global);
|
||||
}
|
||||
|
||||
for (let key of keys) {
|
||||
const res = yield coll.getAny(keyToId(key));
|
||||
if (res.data && res.data._status != "deleted") {
|
||||
records[res.data.key] = res.data.data;
|
||||
}
|
||||
}
|
||||
|
||||
return records;
|
||||
}),
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
removeOnChangedListener(extension, listener) {
|
||||
let listeners = this.listeners.get(extension);
|
||||
listeners.delete(listener);
|
||||
if (listeners.size == 0) {
|
||||
this.listeners.delete(extension);
|
||||
}
|
||||
},
|
||||
|
||||
notifyListeners(extension, changes) {
|
||||
Observers.notify("ext.storage.sync-changed");
|
||||
let listeners = this.listeners.get(extension) || new Set();
|
||||
if (listeners) {
|
||||
for (let listener of listeners) {
|
||||
runSafeSyncWithoutClone(listener, changes);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -4,8 +4,6 @@ var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
|
||||
"resource://gre/modules/ExtensionStorage.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorageSync",
|
||||
"resource://gre/modules/ExtensionStorageSync.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
var {
|
||||
@@ -31,34 +29,14 @@ function storageApiFactory(context) {
|
||||
},
|
||||
},
|
||||
|
||||
sync: {
|
||||
get: function(spec) {
|
||||
return ExtensionStorageSync.get(extension, spec, context);
|
||||
},
|
||||
set: function(items) {
|
||||
return ExtensionStorageSync.set(extension, items, context);
|
||||
},
|
||||
remove: function(keys) {
|
||||
return ExtensionStorageSync.remove(extension, keys, context);
|
||||
},
|
||||
clear: function() {
|
||||
return ExtensionStorageSync.clear(extension, context);
|
||||
},
|
||||
},
|
||||
|
||||
onChanged: new EventManager(context, "storage.onChanged", fire => {
|
||||
let listenerLocal = changes => {
|
||||
fire(changes, "local");
|
||||
};
|
||||
let listenerSync = changes => {
|
||||
fire(changes, "sync");
|
||||
};
|
||||
|
||||
ExtensionStorage.addOnChangedListener(extension.id, listenerLocal);
|
||||
ExtensionStorageSync.addOnChangedListener(extension, listenerSync, context);
|
||||
return () => {
|
||||
ExtensionStorage.removeOnChangedListener(extension.id, listenerLocal);
|
||||
ExtensionStorageSync.removeOnChangedListener(extension, listenerSync);
|
||||
};
|
||||
}).api(),
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@ EXTRA_JS_MODULES += [
|
||||
'ExtensionManagement.jsm',
|
||||
'ExtensionParent.jsm',
|
||||
'ExtensionStorage.jsm',
|
||||
'ExtensionStorageSync.jsm',
|
||||
'ExtensionUtils.jsm',
|
||||
'LegacyExtensionsUtils.jsm',
|
||||
'MessageChannel.jsm',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,9 +58,6 @@ skip-if = release_or_beta
|
||||
[test_ext_schemas_allowed_contexts.js]
|
||||
[test_ext_simple.js]
|
||||
[test_ext_storage.js]
|
||||
[test_ext_storage_sync.js]
|
||||
head = head.js head_sync.js
|
||||
skip-if = os == "android"
|
||||
[test_ext_topSites.js]
|
||||
skip-if = os == "android"
|
||||
[test_getAPILevelForWindow.js]
|
||||
|
||||
@@ -253,7 +253,6 @@ var AddonTestUtils = {
|
||||
Services.prefs.setCharPref("extensions.update.url", "http://127.0.0.1/updateURL");
|
||||
Services.prefs.setCharPref("extensions.update.background.url", "http://127.0.0.1/updateBackgroundURL");
|
||||
Services.prefs.setCharPref("extensions.blocklist.url", "http://127.0.0.1/blocklistURL");
|
||||
Services.prefs.setCharPref("services.settings.server", "http://localhost/dummy-kinto/v1");
|
||||
|
||||
// By default ignore bundled add-ons
|
||||
Services.prefs.setBoolPref("extensions.installDistroAddons", false);
|
||||
|
||||
@@ -627,17 +627,6 @@ Blocklist.prototype = {
|
||||
// make sure we have loaded it.
|
||||
if (!this._isBlocklistLoaded())
|
||||
this._loadBlocklist();
|
||||
|
||||
// If kinto update is enabled, do the kinto update
|
||||
if (gPref.getBoolPref(PREF_BLOCKLIST_UPDATE_ENABLED)) {
|
||||
const updater =
|
||||
Components.utils.import("resource://services-common/blocklist-updater.js",
|
||||
{});
|
||||
updater.checkVersions().catch(() => {
|
||||
// Before we enable this in release, we want to collect telemetry on
|
||||
// failed kinto updates - see bug 1254099
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onXMLLoad: Task.async(function*(aEvent) {
|
||||
|
||||
@@ -64,12 +64,6 @@ function load_blocklist(aFile, aCallback) {
|
||||
gPort + "/data/" + aFile);
|
||||
var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
|
||||
getService(Ci.nsITimerCallback);
|
||||
// if we're not using the blocklist.xml for certificate blocklist state,
|
||||
// ensure that kinto update is enabled
|
||||
if (!Services.prefs.getBoolPref("security.onecrl.via.amo")) {
|
||||
ok(Services.prefs.getBoolPref("services.blocklist.update_enabled", false),
|
||||
"Kinto update should be enabled");
|
||||
}
|
||||
blocklist.notify(null);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user