зеркало из https://github.com/mozilla/gecko-dev.git
294 строки
10 KiB
JavaScript
294 строки
10 KiB
JavaScript
/* 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).getMetadata()
|
|
.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 records;
|
|
if (!ignoreLocal) {
|
|
const localRecords = (yield collection.list()).data;
|
|
records = mergeChanges(localRecords, payload.changes);
|
|
} else {
|
|
records = payload.data;
|
|
}
|
|
const serialized = CanonicalJSON.stringify(records);
|
|
|
|
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), replace the
|
|
// local data
|
|
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) {
|
|
if (item.issuerName && item.serialNumber) {
|
|
certList.revokeCertByIssuerAndSerial(item.issuerName,
|
|
item.serialNumber);
|
|
} else if (item.subject && item.pubKeyHash) {
|
|
certList.revokeCertBySubjectAndPubKey(item.subject,
|
|
item.pubKeyHash);
|
|
} else {
|
|
throw new Error("Cert blocklist record has incomplete data");
|
|
}
|
|
}
|
|
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)
|
|
);
|