fune/toolkit/components/url-classifier/SafeBrowsing.jsm
Francois Marier 66ef6df55a Bug 1482306 - Ensure that tables are enabled when shared between features. r=dimi!
The enable/disable logic of the list manager was wrong. If multiple features
shared a given table (e.g. tracking protection and tracking annotations)
and only one of them was enabled, then updates could either be disabled
or enabled depending on which feature was checked first.

By disabling everything at the beginning and selectively re-enabling tables,
we can ensure that no table gets both enabled and disabled by different
feature toggles.

Differential Revision: https://phabricator.services.mozilla.com/D3259

--HG--
extra : moz-landing-system : lando
2018-08-15 12:13:16 +00:00

560 lines
19 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/. */
var EXPORTED_SYMBOLS = ["SafeBrowsing"];
ChromeUtils.import("resource://gre/modules/Services.jsm");
const PREF_DEBUG_ENABLED = "browser.safebrowsing.debug";
let loggingEnabled = false;
// Log only if browser.safebrowsing.debug is true
function log(...stuff) {
if (!loggingEnabled) {
return;
}
var d = new Date();
let msg = "SafeBrowsing: " + d.toTimeString() + ": " + stuff.join(" ");
dump(Services.urlFormatter.trimSensitiveURLs(msg) + "\n");
}
function getLists(prefName) {
log("getLists: " + prefName);
let pref = Services.prefs.getCharPref(prefName, "");
// Splitting an empty string returns [''], we really want an empty array.
if (!pref) {
return [];
}
return pref.split(",").map(value => value.trim());
}
const tablePreferences = [
"urlclassifier.phishTable",
"urlclassifier.malwareTable",
"urlclassifier.downloadBlockTable",
"urlclassifier.downloadAllowTable",
"urlclassifier.passwordAllowTable",
"urlclassifier.trackingAnnotationTable",
"urlclassifier.trackingAnnotationWhitelistTable",
"urlclassifier.trackingTable",
"urlclassifier.trackingWhitelistTable",
"urlclassifier.blockedTable",
"urlclassifier.flashAllowTable",
"urlclassifier.flashAllowExceptTable",
"urlclassifier.flashTable",
"urlclassifier.flashExceptTable",
"urlclassifier.flashSubDocTable",
"urlclassifier.flashSubDocExceptTable",
"urlclassifier.flashInfobarTable"
];
var SafeBrowsing = {
init() {
if (this.initialized) {
log("Already initialized");
return;
}
Services.prefs.addObserver("browser.contentblocking.enabled", this);
Services.prefs.addObserver("browser.safebrowsing", this);
Services.prefs.addObserver("privacy.trackingprotection", this);
Services.prefs.addObserver("urlclassifier", this);
Services.prefs.addObserver("plugins.flashBlock.enabled", this);
Services.prefs.addObserver("plugins.show_infobar", this);
this.readPrefs();
this.addMozEntries();
this.controlUpdateChecking();
this.initialized = true;
log("init() finished");
},
registerTableWithURLs(listname) {
let listManager = Cc["@mozilla.org/url-classifier/listmanager;1"].
getService(Ci.nsIUrlListManager);
let providerName = this.listToProvider[listname];
let provider = this.providers[providerName];
if (!providerName || !provider) {
log("No provider info found for " + listname);
log("Check browser.safebrowsing.provider.[google/mozilla].lists");
return;
}
if (!provider.updateURL) {
log("Invalid update url " + listname);
return;
}
listManager.registerTable(listname, providerName, provider.updateURL, provider.gethashURL);
},
registerTables() {
for (let i = 0; i < this.phishingLists.length; ++i) {
this.registerTableWithURLs(this.phishingLists[i]);
}
for (let i = 0; i < this.malwareLists.length; ++i) {
this.registerTableWithURLs(this.malwareLists[i]);
}
for (let i = 0; i < this.downloadBlockLists.length; ++i) {
this.registerTableWithURLs(this.downloadBlockLists[i]);
}
for (let i = 0; i < this.downloadAllowLists.length; ++i) {
this.registerTableWithURLs(this.downloadAllowLists[i]);
}
for (let i = 0; i < this.passwordAllowLists.length; ++i) {
this.registerTableWithURLs(this.passwordAllowLists[i]);
}
for (let i = 0; i < this.trackingAnnotationLists.length; ++i) {
this.registerTableWithURLs(this.trackingAnnotationLists[i]);
}
for (let i = 0; i < this.trackingAnnotationWhitelists.length; ++i) {
this.registerTableWithURLs(this.trackingAnnotationWhitelists[i]);
}
for (let i = 0; i < this.trackingProtectionLists.length; ++i) {
this.registerTableWithURLs(this.trackingProtectionLists[i]);
}
for (let i = 0; i < this.trackingProtectionWhitelists.length; ++i) {
this.registerTableWithURLs(this.trackingProtectionWhitelists[i]);
}
for (let i = 0; i < this.blockedLists.length; ++i) {
this.registerTableWithURLs(this.blockedLists[i]);
}
for (let i = 0; i < this.flashLists.length; ++i) {
this.registerTableWithURLs(this.flashLists[i]);
}
for (let i = 0; i < this.flashInfobarLists.length; ++i) {
this.registerTableWithURLs(this.flashInfobarLists[i]);
}
},
unregisterTables(obsoleteLists) {
let listManager = Cc["@mozilla.org/url-classifier/listmanager;1"].
getService(Ci.nsIUrlListManager);
for (let i = 0; i < obsoleteLists.length; ++i) {
for (let j = 0; j < obsoleteLists[i].length; ++j) {
listManager.unregisterTable(obsoleteLists[i][j]);
}
}
},
initialized: false,
phishingEnabled: false,
malwareEnabled: false,
downloadsEnabled: false,
passwordsEnabled: false,
trackingEnabled: false,
blockedEnabled: false,
trackingAnnotations: false,
flashBlockEnabled: false,
flashInfobarListEnabled: false,
phishingLists: [],
malwareLists: [],
downloadBlockLists: [],
downloadAllowLists: [],
passwordAllowLists: [],
trackingAnnotationLists: [],
trackingAnnotationWhiteLists: [],
trackingProtectionLists: [],
trackingProtectionWhitelists: [],
blockedLists: [],
flashLists: [],
flashInfobarLists: [],
updateURL: null,
gethashURL: null,
reportURL: null,
getReportURL(kind, info) {
let pref;
switch (kind) {
case "Phish":
pref = "browser.safebrowsing.reportPhishURL";
break;
case "PhishMistake":
case "MalwareMistake":
pref = "browser.safebrowsing.provider." + info.provider + ".report" + kind + "URL";
break;
default:
let err = "SafeBrowsing getReportURL() called with unknown kind: " + kind;
Cu.reportError(err);
throw err;
}
// The "Phish" reports are about submitting new phishing URLs to Google so
// they don't have an associated list URL
if (kind != "Phish" && (!info.list || !info.uri)) {
return null;
}
let reportUrl = Services.urlFormatter.formatURLPref(pref);
// formatURLPref might return "about:blank" if getting the pref fails
if (reportUrl == "about:blank") {
reportUrl = null;
}
if (reportUrl) {
reportUrl += encodeURIComponent(info.uri);
}
return reportUrl;
},
observe(aSubject, aTopic, aData) {
// skip nextupdatetime and lastupdatetime
if (aData.includes("lastupdatetime") || aData.includes("nextupdatetime")) {
return;
}
if (aData == PREF_DEBUG_ENABLED) {
loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED);
return;
}
this.readPrefs();
},
readPrefs() {
loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED);
log("reading prefs");
let contentBlockingEnabled = Services.prefs.getBoolPref("browser.contentblocking.enabled", true);
this.phishingEnabled = Services.prefs.getBoolPref("browser.safebrowsing.phishing.enabled");
this.malwareEnabled = Services.prefs.getBoolPref("browser.safebrowsing.malware.enabled");
this.downloadsEnabled = Services.prefs.getBoolPref("browser.safebrowsing.downloads.enabled");
this.passwordsEnabled = Services.prefs.getBoolPref("browser.safebrowsing.passwords.enabled");
this.trackingEnabled = contentBlockingEnabled && (Services.prefs.getBoolPref("privacy.trackingprotection.enabled") || Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled"));
this.blockedEnabled = Services.prefs.getBoolPref("browser.safebrowsing.blockedURIs.enabled");
this.trackingAnnotations = Services.prefs.getBoolPref("privacy.trackingprotection.annotate_channels");
this.flashBlockEnabled = Services.prefs.getBoolPref("plugins.flashBlock.enabled");
this.flashInfobarListEnabled = Services.prefs.getBoolPref("plugins.show_infobar", false);
let flashAllowTable, flashAllowExceptTable, flashTable,
flashExceptTable, flashSubDocTable,
flashSubDocExceptTable;
let obsoleteLists;
// Make a copy of the original lists before we re-read the prefs.
if (this.initialized) {
obsoleteLists = [this.phishingLists,
this.malwareLists,
this.downloadBlockLists,
this.downloadAllowLists,
this.passwordAllowLists,
this.trackingAnnotationLists,
this.trackingAnnotationWhitelists,
this.trackingProtectionLists,
this.trackingProtectionWhitelists,
this.blockedLists,
this.flashLists,
this.flashInfobarLists];
}
[this.phishingLists,
this.malwareLists,
this.downloadBlockLists,
this.downloadAllowLists,
this.passwordAllowLists,
this.trackingAnnotationLists,
this.trackingAnnotationWhitelists,
this.trackingProtectionLists,
this.trackingProtectionWhitelists,
this.blockedLists,
flashAllowTable,
flashAllowExceptTable,
flashTable,
flashExceptTable,
flashSubDocTable,
flashSubDocExceptTable,
this.flashInfobarLists] = tablePreferences.map(getLists);
this.flashLists = flashAllowTable.concat(flashAllowExceptTable,
flashTable,
flashExceptTable,
flashSubDocTable,
flashSubDocExceptTable);
if (obsoleteLists) {
let newLists = [this.phishingLists,
this.malwareLists,
this.downloadBlockLists,
this.downloadAllowLists,
this.passwordAllowLists,
this.trackingAnnotationLists,
this.trackingAnnotationWhitelists,
this.trackingProtectionLists,
this.trackingProtectionWhitelists,
this.blockedLists,
this.flashLists,
this.flashInfobarLists];
for (let i = 0; i < obsoleteLists.length; ++i) {
obsoleteLists[i] = obsoleteLists[i]
.filter(list => !newLists[i].includes(list));
}
}
this.updateProviderURLs();
this.registerTables();
if (obsoleteLists) {
this.unregisterTables(obsoleteLists);
}
// XXX The listManager backend gets confused if this is called before the
// lists are registered. So only call it here when a pref changes, and not
// when doing initialization. I expect to refactor this later, so pardon the hack.
if (this.initialized) {
this.controlUpdateChecking();
}
},
updateProviderURLs() {
try {
var clientID = Services.prefs.getCharPref("browser.safebrowsing.id");
} catch (e) {
clientID = Services.appinfo.name;
}
log("initializing safe browsing URLs, client id", clientID);
// Get the different providers
let branch = Services.prefs.getBranch("browser.safebrowsing.provider.");
let children = branch.getChildList("", {});
this.providers = {};
this.listToProvider = {};
for (let child of children) {
log("Child: " + child);
let prefComponents = child.split(".");
let providerName = prefComponents[0];
this.providers[providerName] = {};
}
if (loggingEnabled) {
let providerStr = "";
Object.keys(this.providers).forEach(function(provider) {
if (providerStr === "") {
providerStr = provider;
} else {
providerStr += ", " + provider;
}
});
log("Providers: " + providerStr);
}
Object.keys(this.providers).forEach(function(provider) {
if (provider == "test") {
return; // skip
}
let updateURL = Services.urlFormatter.formatURLPref(
"browser.safebrowsing.provider." + provider + ".updateURL");
let gethashURL = Services.urlFormatter.formatURLPref(
"browser.safebrowsing.provider." + provider + ".gethashURL");
updateURL = updateURL.replace("SAFEBROWSING_ID", clientID);
gethashURL = gethashURL.replace("SAFEBROWSING_ID", clientID);
// Disable updates and gethash if the Google API key is missing.
let googleKey = Services.urlFormatter.formatURL("%GOOGLE_API_KEY%").trim();
if ((provider == "google" || provider == "google4") &&
(!googleKey || googleKey == "no-google-api-key")) {
log("Missing Google API key, clearing updateURL and gethashURL.");
updateURL = "";
gethashURL = "";
}
log("Provider: " + provider + " updateURL=" + updateURL);
log("Provider: " + provider + " gethashURL=" + gethashURL);
// Urls used to update DB
this.providers[provider].updateURL = updateURL;
this.providers[provider].gethashURL = gethashURL;
// Get lists this provider manages
let lists = getLists("browser.safebrowsing.provider." + provider + ".lists");
if (lists) {
lists.forEach(function(list) {
this.listToProvider[list] = provider;
}, this);
} else {
log("Update URL given but no lists managed for provider: " + provider);
}
}, this);
},
controlUpdateChecking() {
log("phishingEnabled:", this.phishingEnabled,
"malwareEnabled:", this.malwareEnabled,
"downloadsEnabled:", this.downloadsEnabled,
"passwordsEnabled:", this.passwordsEnabled,
"trackingEnabled:", this.trackingEnabled,
"blockedEnabled:", this.blockedEnabled,
"trackingAnnotations", this.trackingAnnotations,
"flashBlockEnabled", this.flashBlockEnabled,
"flashInfobarListEnabled:", this.flashInfobarListEnabled);
let listManager = Cc["@mozilla.org/url-classifier/listmanager;1"].
getService(Ci.nsIUrlListManager);
listManager.disableAllUpdates();
for (let i = 0; i < this.phishingLists.length; ++i) {
if (this.phishingEnabled) {
listManager.enableUpdate(this.phishingLists[i]);
}
}
for (let i = 0; i < this.malwareLists.length; ++i) {
if (this.malwareEnabled) {
listManager.enableUpdate(this.malwareLists[i]);
}
}
for (let i = 0; i < this.downloadBlockLists.length; ++i) {
if (this.malwareEnabled && this.downloadsEnabled) {
listManager.enableUpdate(this.downloadBlockLists[i]);
}
}
for (let i = 0; i < this.downloadAllowLists.length; ++i) {
if (this.malwareEnabled && this.downloadsEnabled) {
listManager.enableUpdate(this.downloadAllowLists[i]);
}
}
for (let i = 0; i < this.passwordAllowLists.length; ++i) {
if (this.passwordsEnabled) {
listManager.enableUpdate(this.passwordAllowLists[i]);
}
}
for (let i = 0; i < this.trackingAnnotationLists.length; ++i) {
if (this.trackingAnnotations) {
listManager.enableUpdate(this.trackingAnnotationLists[i]);
}
}
for (let i = 0; i < this.trackingAnnotationWhitelists.length; ++i) {
if (this.trackingAnnotations) {
listManager.enableUpdate(this.trackingAnnotationWhitelists[i]);
}
}
for (let i = 0; i < this.trackingProtectionLists.length; ++i) {
if (this.trackingEnabled) {
listManager.enableUpdate(this.trackingProtectionLists[i]);
}
}
for (let i = 0; i < this.trackingProtectionWhitelists.length; ++i) {
if (this.trackingEnabled) {
listManager.enableUpdate(this.trackingProtectionWhitelists[i]);
}
}
for (let i = 0; i < this.blockedLists.length; ++i) {
if (this.blockedEnabled) {
listManager.enableUpdate(this.blockedLists[i]);
}
}
for (let i = 0; i < this.flashLists.length; ++i) {
if (this.flashBlockEnabled) {
listManager.enableUpdate(this.flashLists[i]);
}
}
for (let i = 0; i < this.flashInfobarLists.length; ++i) {
if (this.flashInfobarListEnabled) {
listManager.enableUpdate(this.flashInfobarLists[i]);
}
}
listManager.maybeToggleUpdateChecking();
},
addMozEntries() {
// Add test entries to the DB.
// XXX bug 779008 - this could be done by DB itself?
const phishURL = "itisatrap.org/firefox/its-a-trap.html";
const malwareURL = "itisatrap.org/firefox/its-an-attack.html";
const unwantedURL = "itisatrap.org/firefox/unwanted.html";
const harmfulURL = "itisatrap.org/firefox/harmful.html";
const trackerURLs = [
"trackertest.org/",
"itisatracker.org/",
];
const whitelistURL = "itisatrap.org/?resource=itisatracker.org";
const blockedURL = "itisatrap.org/firefox/blocked.html";
let update = "n:1000\ni:test-malware-simple\nad:1\n" +
"a:1:32:" + malwareURL.length + "\n" +
malwareURL + "\n";
update += "n:1000\ni:test-phish-simple\nad:1\n" +
"a:1:32:" + phishURL.length + "\n" +
phishURL + "\n";
update += "n:1000\ni:test-unwanted-simple\nad:1\n" +
"a:1:32:" + unwantedURL.length + "\n" +
unwantedURL + "\n";
update += "n:1000\ni:test-harmful-simple\nad:1\n" +
"a:1:32:" + harmfulURL.length + "\n" +
harmfulURL + "\n";
update += "n:1000\ni:test-track-simple\n" +
"ad:" + trackerURLs.length + "\n";
trackerURLs.forEach((trackerURL, i) => {
update += "a:" + (i + 1) + ":32:" + trackerURL.length + "\n" +
trackerURL + "\n";
});
update += "n:1000\ni:test-trackwhite-simple\nad:1\n" +
"a:1:32:" + whitelistURL.length + "\n" +
whitelistURL;
update += "n:1000\ni:test-block-simple\nad:1\n" +
"a:1:32:" + blockedURL.length + "\n" +
blockedURL;
log("addMozEntries:", update);
let db = Cc["@mozilla.org/url-classifier/dbservice;1"].
getService(Ci.nsIUrlClassifierDBService);
// nsIUrlClassifierUpdateObserver
let dummyListener = {
updateUrlRequested() { },
streamFinished() { },
// We notify observers when we're done in order to be able to make perf
// test results more consistent
updateError() {
Services.obs.notifyObservers(db, "mozentries-update-finished", "error");
},
updateSuccess() {
Services.obs.notifyObservers(db, "mozentries-update-finished", "success");
}
};
try {
let tables = "test-malware-simple,test-phish-simple,test-unwanted-simple,test-harmful-simple,test-track-simple,test-trackwhite-simple,test-block-simple";
db.beginUpdate(dummyListener, tables, "");
db.beginStream("", "");
db.updateStream(update);
db.finishStream();
db.finishUpdate();
} catch (ex) {
// beginUpdate will throw harmlessly if there's an existing update in progress, ignore failures.
log("addMozEntries failed!", ex);
Services.obs.notifyObservers(db, "mozentries-update-finished", "exception");
}
},
addMozEntriesFinishedPromise: new Promise(resolve => {
let finished = (subject, topic, data) => {
Services.obs.removeObserver(finished, "mozentries-update-finished");
if (data == "error") {
Cu.reportError("addMozEntries failed to update the db!");
}
resolve();
};
Services.obs.addObserver(finished, "mozentries-update-finished");
}),
};