diff options
Diffstat (limited to 'toolkit/components/url-classifier/SafeBrowsing.jsm')
-rw-r--r-- | toolkit/components/url-classifier/SafeBrowsing.jsm | 586 |
1 files changed, 586 insertions, 0 deletions
diff --git a/toolkit/components/url-classifier/SafeBrowsing.jsm b/toolkit/components/url-classifier/SafeBrowsing.jsm new file mode 100644 index 0000000000..5c4a619e55 --- /dev/null +++ b/toolkit/components/url-classifier/SafeBrowsing.jsm @@ -0,0 +1,586 @@ +/* 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"]; + +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 FEATURES = [ + { + name: "phishing", + list: ["urlclassifier.phishTable"], + enabled() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.phishing.enabled" + ); + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.phishing.update", + this.enabled() + ); + }, + }, + { + name: "malware", + list: ["urlclassifier.malwareTable"], + enabled() { + return Services.prefs.getBoolPref("browser.safebrowsing.malware.enabled"); + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.malware.update", + this.enabled() + ); + }, + }, + { + name: "blockedURIs", + list: ["urlclassifier.blockedTable"], + enabled() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.blockedURIs.enabled" + ); + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.blockedURIs.update", + this.enabled() + ); + }, + }, + { + name: "passwords", + list: ["urlclassifier.passwordAllowTable"], + enabled() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.passwords.enabled" + ); + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.passwords.update", + this.enabled() + ); + }, + }, + { + name: "downloads", + list: [ + "urlclassifier.downloadBlockTable", + "urlclassifier.downloadAllowTable", + ], + enabled() { + return ( + Services.prefs.getBoolPref("browser.safebrowsing.downloads.enabled") && + Services.prefs.getBoolPref("browser.safebrowsing.malware.enabled") + ); + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.downloads.update", + this.enabled() + ); + }, + }, + { + name: "trackingAnnotation", + list: [ + "urlclassifier.trackingAnnotationTable", + "urlclassifier.trackingAnnotationWhitelistTable", + ], + enabled() { + return Services.prefs.getBoolPref( + "privacy.trackingprotection.annotate_channels" + ); + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.trackingAnnotation.update", + this.enabled() + ); + }, + }, + { + name: "trackingProtection", + list: [ + "urlclassifier.trackingTable", + "urlclassifier.trackingWhitelistTable", + ], + enabled() { + return ( + Services.prefs.getBoolPref("privacy.trackingprotection.enabled") || + Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled") + ); + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.trackingProtection.update", + this.enabled() + ); + }, + }, + { + name: "fingerprinting-annotation", + list: [ + "urlclassifier.features.fingerprinting.annotate.blacklistTables", + "urlclassifier.features.fingerprinting.annotate.whitelistTables", + ], + enabled() { + // Annotation features are enabled by default. + return true; + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.fingerprinting.annotate.update", + this.enabled() + ); + }, + }, + { + name: "fingerprinting-protection", + list: [ + "urlclassifier.features.fingerprinting.blacklistTables", + "urlclassifier.features.fingerprinting.whitelistTables", + ], + enabled() { + return Services.prefs.getBoolPref( + "privacy.trackingprotection.fingerprinting.enabled", + false + ); + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.fingerprinting.update", + this.enabled() + ); + }, + }, + { + name: "cryptomining-annotation", + list: [ + "urlclassifier.features.cryptomining.annotate.blacklistTables", + "urlclassifier.features.cryptomining.annotate.whitelistTables", + ], + enabled() { + // Annotation features are enabled by default. + return true; + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.cryptomining.annotate.update", + this.enabled() + ); + }, + }, + { + name: "cryptomining-protection", + list: [ + "urlclassifier.features.cryptomining.blacklistTables", + "urlclassifier.features.cryptomining.whitelistTables", + ], + enabled() { + return Services.prefs.getBoolPref( + "privacy.trackingprotection.cryptomining.enabled", + false + ); + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.cryptomining.update", + this.enabled() + ); + }, + }, + { + name: "socialtracking-annotation", + list: [ + "urlclassifier.features.socialtracking.annotate.blacklistTables", + "urlclassifier.features.socialtracking.annotate.whitelistTables", + ], + enabled() { + // Annotation features are enabled by default. + return true; + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.socialtracking.annotate.update", + this.enabled() + ); + }, + }, + { + name: "socialtracking-protection", + list: [ + "urlclassifier.features.socialtracking.blacklistTables", + "urlclassifier.features.socialtracking.whitelistTables", + ], + enabled() { + return Services.prefs.getBoolPref( + "privacy.trackingprotection.socialtracking.enabled", + false + ); + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.socialtracking.update", + this.enabled() + ); + }, + }, + { + name: "emailtracking-protection", + list: [ + "urlclassifier.features.emailtracking.blocklistTables", + "urlclassifier.features.emailtracking.allowlistTables", + ], + enabled() { + return Services.prefs.getBoolPref( + "privacy.trackingprotection.emailtracking.enabled", + false + ); + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.emailtracking.update", + this.enabled() + ); + }, + }, + { + name: "emailtracking-data-collection", + list: [ + "urlclassifier.features.emailtracking.datacollection.blocklistTables", + "urlclassifier.features.emailtracking.datacollection.allowlistTables", + ], + enabled() { + // Data collection features are enabled by default. + return true; + }, + update() { + return Services.prefs.getBoolPref( + "browser.safebrowsing.features.emailtracking.datacollection.update", + this.enabled() + ); + }, + }, +]; + +var SafeBrowsing = { + init() { + if (this.initialized) { + log("Already initialized"); + return; + } + + Services.prefs.addObserver("browser.safebrowsing", this); + Services.prefs.addObserver("privacy.trackingprotection", this); + Services.prefs.addObserver("urlclassifier", this); + + this.readPrefs(); + + 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() { + this.features.forEach(feature => { + feature.list.forEach(table => { + this.registerTableWithURLs(table); + }); + }); + }, + + unregisterTables(obsoleteLists) { + let listManager = Cc[ + "@mozilla.org/url-classifier/listmanager;1" + ].getService(Ci.nsIUrlListManager); + + obsoleteLists.forEach(list => { + list.forEach(table => { + listManager.unregisterTable(table); + }); + }); + }, + + initialized: false, + + features: [], + + 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 obsoleteLists = []; + // Make a copy of the original lists before we re-read the prefs. + if (this.initialized) { + obsoleteLists = this.features.map(feature => { + return feature.list; + }); + } + + // Allow to disable all feature updates with a single preference for tests. + let update = Services.prefs.getBoolPref( + "browser.safebrowsing.update.enabled", + true + ); + + this.features = []; + for (let i = 0; i < FEATURES.length; ++i) { + this.features[i] = { + name: FEATURES[i].name, + list: [], + enabled: FEATURES[i].enabled(), + update: FEATURES[i].update() && update, + }; + + FEATURES[i].list.forEach(pref => { + this.features[i].list.push(...getLists(pref)); + }); + } + + for (let i = 0; i < obsoleteLists.length; ++i) { + obsoleteLists[i] = obsoleteLists[i].filter( + list => !this.features[i].list.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 googleSafebrowsingKey = Services.urlFormatter + .formatURL("%GOOGLE_SAFEBROWSING_API_KEY%") + .trim(); + if ( + (provider == "google" || provider == "google4") && + (!googleSafebrowsingKey || + googleSafebrowsingKey == "no-google-safebrowsing-api-key") + ) { + log( + "Missing Google SafeBrowsing 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() { + if (loggingEnabled) { + this.features.forEach(feature => { + log("feature " + feature.name + ":"); + log(" enabled:" + feature.enabled); + log(" update:" + feature.update); + log(" tables:" + feature.list); + }); + } + + let listManager = Cc[ + "@mozilla.org/url-classifier/listmanager;1" + ].getService(Ci.nsIUrlListManager); + + listManager.disableAllUpdates(); + + this.features.forEach(feature => { + if (feature.update) { + feature.list.forEach(table => { + listManager.enableUpdate(table); + }); + } + }); + + listManager.maybeToggleUpdateChecking(); + }, +}; |