1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5var EXPORTED_SYMBOLS = ["SafeBrowsing"]; 6 7ChromeUtils.import("resource://gre/modules/Services.jsm"); 8 9const PREF_DEBUG_ENABLED = "browser.safebrowsing.debug"; 10let loggingEnabled = false; 11 12// Log only if browser.safebrowsing.debug is true 13function log(...stuff) { 14 if (!loggingEnabled) { 15 return; 16 } 17 18 var d = new Date(); 19 let msg = "SafeBrowsing: " + d.toTimeString() + ": " + stuff.join(" "); 20 dump(Services.urlFormatter.trimSensitiveURLs(msg) + "\n"); 21} 22 23function getLists(prefName) { 24 log("getLists: " + prefName); 25 let pref = Services.prefs.getCharPref(prefName, ""); 26 27 // Splitting an empty string returns [''], we really want an empty array. 28 if (!pref) { 29 return []; 30 } 31 32 return pref.split(",").map(value => value.trim()); 33} 34 35const tablePreferences = [ 36 "urlclassifier.phishTable", 37 "urlclassifier.malwareTable", 38 "urlclassifier.downloadBlockTable", 39 "urlclassifier.downloadAllowTable", 40 "urlclassifier.passwordAllowTable", 41 "urlclassifier.trackingTable", 42 "urlclassifier.trackingWhitelistTable", 43 "urlclassifier.blockedTable", 44 "urlclassifier.flashAllowTable", 45 "urlclassifier.flashAllowExceptTable", 46 "urlclassifier.flashTable", 47 "urlclassifier.flashExceptTable", 48 "urlclassifier.flashSubDocTable", 49 "urlclassifier.flashSubDocExceptTable", 50 "urlclassifier.flashInfobarTable" 51]; 52 53var SafeBrowsing = { 54 55 init() { 56 if (this.initialized) { 57 log("Already initialized"); 58 return; 59 } 60 61 Services.prefs.addObserver("browser.safebrowsing", this); 62 Services.prefs.addObserver("privacy.trackingprotection", this); 63 Services.prefs.addObserver("urlclassifier", this); 64 Services.prefs.addObserver("plugins.flashBlock.enabled", this); 65 Services.prefs.addObserver("plugins.show_infobar", this); 66 67 this.readPrefs(); 68 this.addMozEntries(); 69 70 this.controlUpdateChecking(); 71 this.initialized = true; 72 73 log("init() finished"); 74 }, 75 76 registerTableWithURLs(listname) { 77 let listManager = Cc["@mozilla.org/url-classifier/listmanager;1"]. 78 getService(Ci.nsIUrlListManager); 79 80 let providerName = this.listToProvider[listname]; 81 let provider = this.providers[providerName]; 82 83 if (!providerName || !provider) { 84 log("No provider info found for " + listname); 85 log("Check browser.safebrowsing.provider.[google/mozilla].lists"); 86 return; 87 } 88 89 if (!provider.updateURL) { 90 log("Invalid update url " + listname); 91 return; 92 } 93 94 listManager.registerTable(listname, providerName, provider.updateURL, provider.gethashURL); 95 }, 96 97 registerTables() { 98 for (let i = 0; i < this.phishingLists.length; ++i) { 99 this.registerTableWithURLs(this.phishingLists[i]); 100 } 101 for (let i = 0; i < this.malwareLists.length; ++i) { 102 this.registerTableWithURLs(this.malwareLists[i]); 103 } 104 for (let i = 0; i < this.downloadBlockLists.length; ++i) { 105 this.registerTableWithURLs(this.downloadBlockLists[i]); 106 } 107 for (let i = 0; i < this.downloadAllowLists.length; ++i) { 108 this.registerTableWithURLs(this.downloadAllowLists[i]); 109 } 110 for (let i = 0; i < this.passwordAllowLists.length; ++i) { 111 this.registerTableWithURLs(this.passwordAllowLists[i]); 112 } 113 for (let i = 0; i < this.trackingProtectionLists.length; ++i) { 114 this.registerTableWithURLs(this.trackingProtectionLists[i]); 115 } 116 for (let i = 0; i < this.trackingProtectionWhitelists.length; ++i) { 117 this.registerTableWithURLs(this.trackingProtectionWhitelists[i]); 118 } 119 for (let i = 0; i < this.blockedLists.length; ++i) { 120 this.registerTableWithURLs(this.blockedLists[i]); 121 } 122 for (let i = 0; i < this.flashLists.length; ++i) { 123 this.registerTableWithURLs(this.flashLists[i]); 124 } 125 for (let i = 0; i < this.flashInfobarLists.length; ++i) { 126 this.registerTableWithURLs(this.flashInfobarLists[i]); 127 } 128 }, 129 130 unregisterTables(obsoleteLists) { 131 let listManager = Cc["@mozilla.org/url-classifier/listmanager;1"]. 132 getService(Ci.nsIUrlListManager); 133 134 for (let i = 0; i < obsoleteLists.length; ++i) { 135 for (let j = 0; j < obsoleteLists[i].length; ++j) { 136 listManager.unregisterTable(obsoleteLists[i][j]); 137 } 138 } 139 }, 140 141 142 initialized: false, 143 phishingEnabled: false, 144 malwareEnabled: false, 145 downloadsEnabled: false, 146 passwordsEnabled: false, 147 trackingEnabled: false, 148 blockedEnabled: false, 149 trackingAnnotations: false, 150 flashBlockEnabled: false, 151 flashInfobarListEnabled: false, 152 153 phishingLists: [], 154 malwareLists: [], 155 downloadBlockLists: [], 156 downloadAllowLists: [], 157 passwordAllowLists: [], 158 trackingProtectionLists: [], 159 trackingProtectionWhitelists: [], 160 blockedLists: [], 161 flashLists: [], 162 flashInfobarLists: [], 163 164 updateURL: null, 165 gethashURL: null, 166 167 reportURL: null, 168 169 getReportURL(kind, info) { 170 let pref; 171 switch (kind) { 172 case "Phish": 173 pref = "browser.safebrowsing.reportPhishURL"; 174 break; 175 176 case "PhishMistake": 177 case "MalwareMistake": 178 pref = "browser.safebrowsing.provider." + info.provider + ".report" + kind + "URL"; 179 break; 180 181 default: 182 let err = "SafeBrowsing getReportURL() called with unknown kind: " + kind; 183 Cu.reportError(err); 184 throw err; 185 } 186 187 // The "Phish" reports are about submitting new phishing URLs to Google so 188 // they don't have an associated list URL 189 if (kind != "Phish" && (!info.list || !info.uri)) { 190 return null; 191 } 192 193 let reportUrl = Services.urlFormatter.formatURLPref(pref); 194 // formatURLPref might return "about:blank" if getting the pref fails 195 if (reportUrl == "about:blank") { 196 reportUrl = null; 197 } 198 199 if (reportUrl) { 200 reportUrl += encodeURIComponent(info.uri); 201 } 202 return reportUrl; 203 }, 204 205 observe(aSubject, aTopic, aData) { 206 // skip nextupdatetime and lastupdatetime 207 if (aData.includes("lastupdatetime") || aData.includes("nextupdatetime")) { 208 return; 209 } 210 211 if (aData == PREF_DEBUG_ENABLED) { 212 loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED); 213 return; 214 } 215 216 this.readPrefs(); 217 }, 218 219 readPrefs() { 220 loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED); 221 log("reading prefs"); 222 223 this.phishingEnabled = Services.prefs.getBoolPref("browser.safebrowsing.phishing.enabled"); 224 this.malwareEnabled = Services.prefs.getBoolPref("browser.safebrowsing.malware.enabled"); 225 this.downloadsEnabled = Services.prefs.getBoolPref("browser.safebrowsing.downloads.enabled"); 226 this.passwordsEnabled = Services.prefs.getBoolPref("browser.safebrowsing.passwords.enabled"); 227 this.trackingEnabled = Services.prefs.getBoolPref("privacy.trackingprotection.enabled") || Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled"); 228 this.blockedEnabled = Services.prefs.getBoolPref("browser.safebrowsing.blockedURIs.enabled"); 229 this.trackingAnnotations = Services.prefs.getBoolPref("privacy.trackingprotection.annotate_channels"); 230 this.flashBlockEnabled = Services.prefs.getBoolPref("plugins.flashBlock.enabled"); 231 this.flashInfobarListEnabled = Services.prefs.getBoolPref("plugins.show_infobar", false); 232 233 let flashAllowTable, flashAllowExceptTable, flashTable, 234 flashExceptTable, flashSubDocTable, 235 flashSubDocExceptTable; 236 237 let obsoleteLists; 238 // Make a copy of the original lists before we re-read the prefs. 239 if (this.initialized) { 240 obsoleteLists = [this.phishingLists, 241 this.malwareLists, 242 this.downloadBlockLists, 243 this.downloadAllowLists, 244 this.passwordAllowLists, 245 this.trackingProtectionLists, 246 this.trackingProtectionWhitelists, 247 this.blockedLists, 248 this.flashLists, 249 this.flashInfobarLists]; 250 } 251 252 [this.phishingLists, 253 this.malwareLists, 254 this.downloadBlockLists, 255 this.downloadAllowLists, 256 this.passwordAllowLists, 257 this.trackingProtectionLists, 258 this.trackingProtectionWhitelists, 259 this.blockedLists, 260 flashAllowTable, 261 flashAllowExceptTable, 262 flashTable, 263 flashExceptTable, 264 flashSubDocTable, 265 flashSubDocExceptTable, 266 this.flashInfobarLists] = tablePreferences.map(getLists); 267 268 this.flashLists = flashAllowTable.concat(flashAllowExceptTable, 269 flashTable, 270 flashExceptTable, 271 flashSubDocTable, 272 flashSubDocExceptTable); 273 274 if (obsoleteLists) { 275 let newLists = [this.phishingLists, 276 this.malwareLists, 277 this.downloadBlockLists, 278 this.downloadAllowLists, 279 this.passwordAllowLists, 280 this.trackingProtectionLists, 281 this.trackingProtectionWhitelists, 282 this.blockedLists, 283 this.flashLists, 284 this.flashInfobarLists]; 285 286 for (let i = 0; i < obsoleteLists.length; ++i) { 287 obsoleteLists[i] = obsoleteLists[i] 288 .filter(list => !newLists[i].includes(list)); 289 } 290 } 291 292 this.updateProviderURLs(); 293 this.registerTables(); 294 if (obsoleteLists) { 295 this.unregisterTables(obsoleteLists); 296 } 297 298 // XXX The listManager backend gets confused if this is called before the 299 // lists are registered. So only call it here when a pref changes, and not 300 // when doing initialization. I expect to refactor this later, so pardon the hack. 301 if (this.initialized) { 302 this.controlUpdateChecking(); 303 } 304 }, 305 306 307 updateProviderURLs() { 308 try { 309 var clientID = Services.prefs.getCharPref("browser.safebrowsing.id"); 310 } catch (e) { 311 clientID = Services.appinfo.name; 312 } 313 314 log("initializing safe browsing URLs, client id", clientID); 315 316 // Get the different providers 317 let branch = Services.prefs.getBranch("browser.safebrowsing.provider."); 318 let children = branch.getChildList("", {}); 319 this.providers = {}; 320 this.listToProvider = {}; 321 322 for (let child of children) { 323 log("Child: " + child); 324 let prefComponents = child.split("."); 325 let providerName = prefComponents[0]; 326 this.providers[providerName] = {}; 327 } 328 329 if (loggingEnabled) { 330 let providerStr = ""; 331 Object.keys(this.providers).forEach(function(provider) { 332 if (providerStr === "") { 333 providerStr = provider; 334 } else { 335 providerStr += ", " + provider; 336 } 337 }); 338 log("Providers: " + providerStr); 339 } 340 341 Object.keys(this.providers).forEach(function(provider) { 342 if (provider == "test") { 343 return; // skip 344 } 345 let updateURL = Services.urlFormatter.formatURLPref( 346 "browser.safebrowsing.provider." + provider + ".updateURL"); 347 let gethashURL = Services.urlFormatter.formatURLPref( 348 "browser.safebrowsing.provider." + provider + ".gethashURL"); 349 updateURL = updateURL.replace("SAFEBROWSING_ID", clientID); 350 gethashURL = gethashURL.replace("SAFEBROWSING_ID", clientID); 351 352 // Disable updates and gethash if the Google API key is missing. 353 let googleSafebrowsingKey = Services.urlFormatter.formatURL("%GOOGLE_SAFEBROWSING_API_KEY%").trim(); 354 if ((provider == "google" || provider == "google4") && 355 (!googleSafebrowsingKey || googleSafebrowsingKey == "no-google-safebrowsing-api-key")) { 356 log("Missing Google SafeBrowsing API key, clearing updateURL and gethashURL."); 357 updateURL = ""; 358 gethashURL = ""; 359 } 360 361 log("Provider: " + provider + " updateURL=" + updateURL); 362 log("Provider: " + provider + " gethashURL=" + gethashURL); 363 364 // Urls used to update DB 365 this.providers[provider].updateURL = updateURL; 366 this.providers[provider].gethashURL = gethashURL; 367 368 // Get lists this provider manages 369 let lists = getLists("browser.safebrowsing.provider." + provider + ".lists"); 370 if (lists) { 371 lists.forEach(function(list) { 372 this.listToProvider[list] = provider; 373 }, this); 374 } else { 375 log("Update URL given but no lists managed for provider: " + provider); 376 } 377 }, this); 378 }, 379 380 controlUpdateChecking() { 381 log("phishingEnabled:", this.phishingEnabled, 382 "malwareEnabled:", this.malwareEnabled, 383 "downloadsEnabled:", this.downloadsEnabled, 384 "passwordsEnabled:", this.passwordsEnabled, 385 "trackingEnabled:", this.trackingEnabled, 386 "blockedEnabled:", this.blockedEnabled, 387 "trackingAnnotations", this.trackingAnnotations, 388 "flashBlockEnabled", this.flashBlockEnabled, 389 "flashInfobarListEnabled:", this.flashInfobarListEnabled); 390 391 let listManager = Cc["@mozilla.org/url-classifier/listmanager;1"]. 392 getService(Ci.nsIUrlListManager); 393 394 for (let i = 0; i < this.phishingLists.length; ++i) { 395 if (this.phishingEnabled) { 396 listManager.enableUpdate(this.phishingLists[i]); 397 } else { 398 listManager.disableUpdate(this.phishingLists[i]); 399 } 400 } 401 for (let i = 0; i < this.malwareLists.length; ++i) { 402 if (this.malwareEnabled) { 403 listManager.enableUpdate(this.malwareLists[i]); 404 } else { 405 listManager.disableUpdate(this.malwareLists[i]); 406 } 407 } 408 for (let i = 0; i < this.downloadBlockLists.length; ++i) { 409 if (this.malwareEnabled && this.downloadsEnabled) { 410 listManager.enableUpdate(this.downloadBlockLists[i]); 411 } else { 412 listManager.disableUpdate(this.downloadBlockLists[i]); 413 } 414 } 415 for (let i = 0; i < this.downloadAllowLists.length; ++i) { 416 if (this.malwareEnabled && this.downloadsEnabled) { 417 listManager.enableUpdate(this.downloadAllowLists[i]); 418 } else { 419 listManager.disableUpdate(this.downloadAllowLists[i]); 420 } 421 } 422 for (let i = 0; i < this.passwordAllowLists.length; ++i) { 423 if (this.passwordsEnabled) { 424 listManager.enableUpdate(this.passwordAllowLists[i]); 425 } else { 426 listManager.disableUpdate(this.passwordAllowLists[i]); 427 } 428 } 429 for (let i = 0; i < this.trackingProtectionLists.length; ++i) { 430 if (this.trackingEnabled || this.trackingAnnotations) { 431 listManager.enableUpdate(this.trackingProtectionLists[i]); 432 } else { 433 listManager.disableUpdate(this.trackingProtectionLists[i]); 434 } 435 } 436 for (let i = 0; i < this.trackingProtectionWhitelists.length; ++i) { 437 if (this.trackingEnabled || this.trackingAnnotations) { 438 listManager.enableUpdate(this.trackingProtectionWhitelists[i]); 439 } else { 440 listManager.disableUpdate(this.trackingProtectionWhitelists[i]); 441 } 442 } 443 for (let i = 0; i < this.blockedLists.length; ++i) { 444 if (this.blockedEnabled) { 445 listManager.enableUpdate(this.blockedLists[i]); 446 } else { 447 listManager.disableUpdate(this.blockedLists[i]); 448 } 449 } 450 for (let i = 0; i < this.flashLists.length; ++i) { 451 if (this.flashBlockEnabled) { 452 listManager.enableUpdate(this.flashLists[i]); 453 } else { 454 listManager.disableUpdate(this.flashLists[i]); 455 } 456 } 457 for (let i = 0; i < this.flashInfobarLists.length; ++i) { 458 if (this.flashInfobarListEnabled) { 459 listManager.enableUpdate(this.flashInfobarLists[i]); 460 } else { 461 listManager.disableUpdate(this.flashInfobarLists[i]); 462 } 463 } 464 listManager.maybeToggleUpdateChecking(); 465 }, 466 467 468 addMozEntries() { 469 // Add test entries to the DB. 470 // XXX bug 779008 - this could be done by DB itself? 471 const phishURL = "itisatrap.org/firefox/its-a-trap.html"; 472 const malwareURL = "itisatrap.org/firefox/its-an-attack.html"; 473 const unwantedURL = "itisatrap.org/firefox/unwanted.html"; 474 const harmfulURL = "itisatrap.org/firefox/harmful.html"; 475 const trackerURLs = [ 476 "trackertest.org/", 477 "itisatracker.org/", 478 ]; 479 const whitelistURL = "itisatrap.org/?resource=itisatracker.org"; 480 const blockedURL = "itisatrap.org/firefox/blocked.html"; 481 482 let update = "n:1000\ni:test-malware-simple\nad:1\n" + 483 "a:1:32:" + malwareURL.length + "\n" + 484 malwareURL + "\n"; 485 update += "n:1000\ni:test-phish-simple\nad:1\n" + 486 "a:1:32:" + phishURL.length + "\n" + 487 phishURL + "\n"; 488 update += "n:1000\ni:test-unwanted-simple\nad:1\n" + 489 "a:1:32:" + unwantedURL.length + "\n" + 490 unwantedURL + "\n"; 491 update += "n:1000\ni:test-harmful-simple\nad:1\n" + 492 "a:1:32:" + harmfulURL.length + "\n" + 493 harmfulURL + "\n"; 494 update += "n:1000\ni:test-track-simple\n" + 495 "ad:" + trackerURLs.length + "\n"; 496 trackerURLs.forEach((trackerURL, i) => { 497 update += "a:" + (i + 1) + ":32:" + trackerURL.length + "\n" + 498 trackerURL + "\n"; 499 }); 500 update += "n:1000\ni:test-trackwhite-simple\nad:1\n" + 501 "a:1:32:" + whitelistURL.length + "\n" + 502 whitelistURL; 503 update += "n:1000\ni:test-block-simple\nad:1\n" + 504 "a:1:32:" + blockedURL.length + "\n" + 505 blockedURL; 506 log("addMozEntries:", update); 507 508 let db = Cc["@mozilla.org/url-classifier/dbservice;1"]. 509 getService(Ci.nsIUrlClassifierDBService); 510 511 // nsIUrlClassifierUpdateObserver 512 let dummyListener = { 513 updateUrlRequested() { }, 514 streamFinished() { }, 515 // We notify observers when we're done in order to be able to make perf 516 // test results more consistent 517 updateError() { 518 Services.obs.notifyObservers(db, "mozentries-update-finished", "error"); 519 }, 520 updateSuccess() { 521 Services.obs.notifyObservers(db, "mozentries-update-finished", "success"); 522 } 523 }; 524 525 try { 526 let tables = "test-malware-simple,test-phish-simple,test-unwanted-simple,test-harmful-simple,test-track-simple,test-trackwhite-simple,test-block-simple"; 527 db.beginUpdate(dummyListener, tables, ""); 528 db.beginStream("", ""); 529 db.updateStream(update); 530 db.finishStream(); 531 db.finishUpdate(); 532 } catch (ex) { 533 // beginUpdate will throw harmlessly if there's an existing update in progress, ignore failures. 534 log("addMozEntries failed!", ex); 535 Services.obs.notifyObservers(db, "mozentries-update-finished", "exception"); 536 } 537 }, 538 539 addMozEntriesFinishedPromise: new Promise(resolve => { 540 let finished = (subject, topic, data) => { 541 Services.obs.removeObserver(finished, "mozentries-update-finished"); 542 if (data == "error") { 543 Cu.reportError("addMozEntries failed to update the db!"); 544 } 545 resolve(); 546 }; 547 Services.obs.addObserver(finished, "mozentries-update-finished"); 548 }), 549}; 550