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
5this.EXPORTED_SYMBOLS = ["SafeBrowsing"];
6
7const Cc = Components.classes;
8const Ci = Components.interfaces;
9const Cu = Components.utils;
10
11Cu.import("resource://gre/modules/Services.jsm");
12
13// Log only if browser.safebrowsing.debug is true
14function log(...stuff) {
15  let logging = null;
16  try {
17    logging = Services.prefs.getBoolPref("browser.safebrowsing.debug");
18  } catch(e) {
19    return;
20  }
21  if (!logging) {
22    return;
23  }
24
25  var d = new Date();
26  let msg = "SafeBrowsing: " + d.toTimeString() + ": " + stuff.join(" ");
27  dump(Services.urlFormatter.trimSensitiveURLs(msg) + "\n");
28}
29
30function getLists(prefName) {
31  log("getLists: " + prefName);
32  let pref = null;
33  try {
34    pref = Services.prefs.getCharPref(prefName);
35  } catch(e) {
36    return null;
37  }
38  // Splitting an empty string returns [''], we really want an empty array.
39  if (!pref) {
40    return [];
41  }
42  return pref.split(",")
43    .map(function(value) { return value.trim(); });
44}
45
46const tablePreferences = [
47  "urlclassifier.phishTable",
48  "urlclassifier.malwareTable",
49  "urlclassifier.downloadBlockTable",
50  "urlclassifier.downloadAllowTable",
51  "urlclassifier.trackingTable",
52  "urlclassifier.trackingWhitelistTable",
53  "urlclassifier.blockedTable"
54];
55
56this.SafeBrowsing = {
57
58  init: function() {
59    if (this.initialized) {
60      log("Already initialized");
61      return;
62    }
63
64    Services.prefs.addObserver("browser.safebrowsing", this, false);
65    Services.prefs.addObserver("privacy.trackingprotection", this, false);
66    Services.prefs.addObserver("urlclassifier", this, false);
67
68    this.readPrefs();
69    this.addMozEntries();
70
71    this.controlUpdateChecking();
72    this.initialized = true;
73
74    log("init() finished");
75  },
76
77  registerTableWithURLs: function(listname) {
78    let listManager = Cc["@mozilla.org/url-classifier/listmanager;1"].
79      getService(Ci.nsIUrlListManager);
80
81    let providerName = this.listToProvider[listname];
82    let provider = this.providers[providerName];
83
84    if (!providerName || !provider) {
85      log("No provider info found for " + listname);
86      log("Check browser.safebrowsing.provider.[google/mozilla].lists");
87      return;
88    }
89
90    listManager.registerTable(listname, providerName, provider.updateURL, provider.gethashURL);
91  },
92
93  registerTables: function() {
94    for (let i = 0; i < this.phishingLists.length; ++i) {
95      this.registerTableWithURLs(this.phishingLists[i]);
96    }
97    for (let i = 0; i < this.malwareLists.length; ++i) {
98      this.registerTableWithURLs(this.malwareLists[i]);
99    }
100    for (let i = 0; i < this.downloadBlockLists.length; ++i) {
101      this.registerTableWithURLs(this.downloadBlockLists[i]);
102    }
103    for (let i = 0; i < this.downloadAllowLists.length; ++i) {
104      this.registerTableWithURLs(this.downloadAllowLists[i]);
105    }
106    for (let i = 0; i < this.trackingProtectionLists.length; ++i) {
107      this.registerTableWithURLs(this.trackingProtectionLists[i]);
108    }
109    for (let i = 0; i < this.trackingProtectionWhitelists.length; ++i) {
110      this.registerTableWithURLs(this.trackingProtectionWhitelists[i]);
111    }
112    for (let i = 0; i < this.blockedLists.length; ++i) {
113      this.registerTableWithURLs(this.blockedLists[i]);
114    }
115  },
116
117
118  initialized:      false,
119  phishingEnabled:  false,
120  malwareEnabled:   false,
121  trackingEnabled:  false,
122  blockedEnabled:   false,
123
124  phishingLists:                [],
125  malwareLists:                 [],
126  downloadBlockLists:           [],
127  downloadAllowLists:           [],
128  trackingProtectionLists:      [],
129  trackingProtectionWhitelists: [],
130  blockedLists:                 [],
131
132  updateURL:             null,
133  gethashURL:            null,
134
135  reportURL:             null,
136
137  getReportURL: function(kind, URI) {
138    let pref;
139    switch (kind) {
140      case "Phish":
141        pref = "browser.safebrowsing.reportPhishURL";
142        break;
143      case "PhishMistake":
144        pref = "browser.safebrowsing.reportPhishMistakeURL";
145        break;
146      case "MalwareMistake":
147        pref = "browser.safebrowsing.reportMalwareMistakeURL";
148        break;
149
150      default:
151        let err = "SafeBrowsing getReportURL() called with unknown kind: " + kind;
152        Components.utils.reportError(err);
153        throw err;
154    }
155    let reportUrl = Services.urlFormatter.formatURLPref(pref);
156
157    let pageUri = URI.clone();
158
159    // Remove the query to avoid including potentially sensitive data
160    if (pageUri instanceof Ci.nsIURL)
161      pageUri.query = '';
162
163    reportUrl += encodeURIComponent(pageUri.asciiSpec);
164
165    return reportUrl;
166  },
167
168  observe: function(aSubject, aTopic, aData) {
169    // skip nextupdatetime and lastupdatetime
170    if (aData.indexOf("lastupdatetime") >= 0 || aData.indexOf("nextupdatetime") >= 0) {
171      return;
172    }
173    this.readPrefs();
174  },
175
176  readPrefs: function() {
177    log("reading prefs");
178
179    this.debug = Services.prefs.getBoolPref("browser.safebrowsing.debug");
180    this.phishingEnabled = Services.prefs.getBoolPref("browser.safebrowsing.phishing.enabled");
181    this.malwareEnabled = Services.prefs.getBoolPref("browser.safebrowsing.malware.enabled");
182    this.trackingEnabled = Services.prefs.getBoolPref("privacy.trackingprotection.enabled") || Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled");
183    this.blockedEnabled = Services.prefs.getBoolPref("browser.safebrowsing.blockedURIs.enabled");
184
185    [this.phishingLists,
186     this.malwareLists,
187     this.downloadBlockLists,
188     this.downloadAllowLists,
189     this.trackingProtectionLists,
190     this.trackingProtectionWhitelists,
191     this.blockedLists] = tablePreferences.map(getLists);
192
193    this.updateProviderURLs();
194    this.registerTables();
195
196    // XXX The listManager backend gets confused if this is called before the
197    // lists are registered. So only call it here when a pref changes, and not
198    // when doing initialization. I expect to refactor this later, so pardon the hack.
199    if (this.initialized) {
200      this.controlUpdateChecking();
201    }
202  },
203
204
205  updateProviderURLs: function() {
206    try {
207      var clientID = Services.prefs.getCharPref("browser.safebrowsing.id");
208    } catch(e) {
209      clientID = Services.appinfo.name;
210    }
211
212    log("initializing safe browsing URLs, client id", clientID);
213
214    // Get the different providers
215    let branch = Services.prefs.getBranch("browser.safebrowsing.provider.");
216    let children = branch.getChildList("", {});
217    this.providers = {};
218    this.listToProvider = {};
219
220    for (let child of children) {
221      log("Child: " + child);
222      let prefComponents =  child.split(".");
223      let providerName = prefComponents[0];
224      this.providers[providerName] = {};
225    }
226
227    if (this.debug) {
228      let providerStr = "";
229      Object.keys(this.providers).forEach(function(provider) {
230        if (providerStr === "") {
231          providerStr = provider;
232        } else {
233          providerStr += ", " + provider;
234        }
235      });
236      log("Providers: " + providerStr);
237    }
238
239    Object.keys(this.providers).forEach(function(provider) {
240      let updateURL = Services.urlFormatter.formatURLPref(
241        "browser.safebrowsing.provider." + provider + ".updateURL");
242      let gethashURL = Services.urlFormatter.formatURLPref(
243        "browser.safebrowsing.provider." + provider + ".gethashURL");
244      updateURL = updateURL.replace("SAFEBROWSING_ID", clientID);
245      gethashURL = gethashURL.replace("SAFEBROWSING_ID", clientID);
246
247      log("Provider: " + provider + " updateURL=" + updateURL);
248      log("Provider: " + provider + " gethashURL=" + gethashURL);
249
250      // Urls used to update DB
251      this.providers[provider].updateURL  = updateURL;
252      this.providers[provider].gethashURL = gethashURL;
253
254      // Get lists this provider manages
255      let lists = getLists("browser.safebrowsing.provider." + provider + ".lists");
256      if (lists) {
257        lists.forEach(function(list) {
258          this.listToProvider[list] = provider;
259        }, this);
260      } else {
261        log("Update URL given but no lists managed for provider: " + provider);
262      }
263    }, this);
264  },
265
266  controlUpdateChecking: function() {
267    log("phishingEnabled:", this.phishingEnabled, "malwareEnabled:",
268        this.malwareEnabled, "trackingEnabled:", this.trackingEnabled,
269        "blockedEnabled:", this.blockedEnabled);
270
271    let listManager = Cc["@mozilla.org/url-classifier/listmanager;1"].
272                      getService(Ci.nsIUrlListManager);
273
274    for (let i = 0; i < this.phishingLists.length; ++i) {
275      if (this.phishingEnabled) {
276        listManager.enableUpdate(this.phishingLists[i]);
277      } else {
278        listManager.disableUpdate(this.phishingLists[i]);
279      }
280    }
281    for (let i = 0; i < this.malwareLists.length; ++i) {
282      if (this.malwareEnabled) {
283        listManager.enableUpdate(this.malwareLists[i]);
284      } else {
285        listManager.disableUpdate(this.malwareLists[i]);
286      }
287    }
288    for (let i = 0; i < this.downloadBlockLists.length; ++i) {
289      if (this.malwareEnabled) {
290        listManager.enableUpdate(this.downloadBlockLists[i]);
291      } else {
292        listManager.disableUpdate(this.downloadBlockLists[i]);
293      }
294    }
295    for (let i = 0; i < this.downloadAllowLists.length; ++i) {
296      if (this.malwareEnabled) {
297        listManager.enableUpdate(this.downloadAllowLists[i]);
298      } else {
299        listManager.disableUpdate(this.downloadAllowLists[i]);
300      }
301    }
302    for (let i = 0; i < this.trackingProtectionLists.length; ++i) {
303      if (this.trackingEnabled) {
304        listManager.enableUpdate(this.trackingProtectionLists[i]);
305      } else {
306        listManager.disableUpdate(this.trackingProtectionLists[i]);
307      }
308    }
309    for (let i = 0; i < this.trackingProtectionWhitelists.length; ++i) {
310      if (this.trackingEnabled) {
311        listManager.enableUpdate(this.trackingProtectionWhitelists[i]);
312      } else {
313        listManager.disableUpdate(this.trackingProtectionWhitelists[i]);
314      }
315    }
316    for (let i = 0; i < this.blockedLists.length; ++i) {
317      if (this.blockedEnabled) {
318        listManager.enableUpdate(this.blockedLists[i]);
319      } else {
320        listManager.disableUpdate(this.blockedLists[i]);
321      }
322    }
323    listManager.maybeToggleUpdateChecking();
324  },
325
326
327  addMozEntries: function() {
328    // Add test entries to the DB.
329    // XXX bug 779008 - this could be done by DB itself?
330    const phishURL    = "itisatrap.org/firefox/its-a-trap.html";
331    const malwareURL  = "itisatrap.org/firefox/its-an-attack.html";
332    const unwantedURL = "itisatrap.org/firefox/unwanted.html";
333    const trackerURLs = [
334      "trackertest.org/",
335      "itisatracker.org/",
336    ];
337    const whitelistURL  = "itisatrap.org/?resource=itisatracker.org";
338    const blockedURL    = "itisatrap.org/firefox/blocked.html";
339
340    const flashDenyURL = "flashblock.itisatrap.org/";
341    const flashDenyExceptURL = "except.flashblock.itisatrap.org/";
342    const flashAllowURL = "flashallow.itisatrap.org/";
343    const flashAllowExceptURL = "except.flashallow.itisatrap.org/";
344    const flashSubDocURL = "flashsubdoc.itisatrap.org/";
345    const flashSubDocExceptURL = "except.flashsubdoc.itisatrap.org/";
346
347    let update = "n:1000\ni:test-malware-simple\nad:1\n" +
348                 "a:1:32:" + malwareURL.length + "\n" +
349                 malwareURL + "\n";
350    update += "n:1000\ni:test-phish-simple\nad:1\n" +
351              "a:1:32:" + phishURL.length + "\n" +
352              phishURL  + "\n";
353    update += "n:1000\ni:test-unwanted-simple\nad:1\n" +
354              "a:1:32:" + unwantedURL.length + "\n" +
355              unwantedURL + "\n";
356    update += "n:1000\ni:test-track-simple\n" +
357              "ad:" + trackerURLs.length + "\n";
358    trackerURLs.forEach((trackerURL, i) => {
359      update += "a:" + (i + 1) + ":32:" + trackerURL.length + "\n" +
360                trackerURL + "\n";
361    });
362    update += "n:1000\ni:test-trackwhite-simple\nad:1\n" +
363              "a:1:32:" + whitelistURL.length + "\n" +
364              whitelistURL;
365    update += "n:1000\ni:test-block-simple\nad:1\n" +
366              "a:1:32:" + blockedURL.length + "\n" +
367              blockedURL;
368    update += "n:1000\ni:test-flash-simple\nad:1\n" +
369              "a:1:32:" + flashDenyURL.length + "\n" +
370              flashDenyURL;
371    update += "n:1000\ni:testexcept-flash-simple\nad:1\n" +
372              "a:1:32:" + flashDenyExceptURL.length + "\n" +
373              flashDenyExceptURL;
374    update += "n:1000\ni:test-flashallow-simple\nad:1\n" +
375              "a:1:32:" + flashAllowURL.length + "\n" +
376              flashAllowURL;
377    update += "n:1000\ni:testexcept-flashallow-simple\nad:1\n" +
378              "a:1:32:" + flashAllowExceptURL.length + "\n" +
379              flashAllowExceptURL;
380    update += "n:1000\ni:test-flashsubdoc-simple\nad:1\n" +
381              "a:1:32:" + flashSubDocURL.length + "\n" +
382              flashSubDocURL;
383    update += "n:1000\ni:testexcept-flashsubdoc-simple\nad:1\n" +
384              "a:1:32:" + flashSubDocExceptURL.length + "\n" +
385              flashSubDocExceptURL;
386    log("addMozEntries:", update);
387
388    let db = Cc["@mozilla.org/url-classifier/dbservice;1"].
389             getService(Ci.nsIUrlClassifierDBService);
390
391    // nsIUrlClassifierUpdateObserver
392    let dummyListener = {
393      updateUrlRequested: function() { },
394      streamFinished:     function() { },
395      // We notify observers when we're done in order to be able to make perf
396      // test results more consistent
397      updateError:        function() {
398        Services.obs.notifyObservers(db, "mozentries-update-finished", "error");
399      },
400      updateSuccess:      function() {
401        Services.obs.notifyObservers(db, "mozentries-update-finished", "success");
402      }
403    };
404
405    try {
406      let tables = "test-malware-simple,test-phish-simple,test-unwanted-simple,test-track-simple,test-trackwhite-simple,test-block-simple,test-flash-simple,testexcept-flash-simple,test-flashallow-simple,testexcept-flashallow-simple,test-flashsubdoc-simple,testexcept-flashsubdoc-simple";
407      db.beginUpdate(dummyListener, tables, "");
408      db.beginStream("", "");
409      db.updateStream(update);
410      db.finishStream();
411      db.finishUpdate();
412    } catch(ex) {
413      // beginUpdate will throw harmlessly if there's an existing update in progress, ignore failures.
414      log("addMozEntries failed!", ex);
415      Services.obs.notifyObservers(db, "mozentries-update-finished", "exception");
416    }
417  },
418
419  addMozEntriesFinishedPromise: new Promise(resolve => {
420    let finished = (subject, topic, data) => {
421      Services.obs.removeObserver(finished, "mozentries-update-finished");
422      if (data == "error") {
423        Cu.reportError("addMozEntries failed to update the db!");
424      }
425      resolve();
426    };
427    Services.obs.addObserver(finished, "mozentries-update-finished", false);
428  }),
429};
430