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