tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

WebsiteFilter.sys.mjs (5759B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /*
      6 * This module implements the policy to block websites from being visited,
      7 * or to only allow certain websites to be visited.
      8 *
      9 * The blocklist takes as input an array of MatchPattern strings, as documented
     10 * at https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns.
     11 *
     12 * The exceptions list takes the same as input. This list opens up
     13 * exceptions for rules on the blocklist that might be too strict.
     14 *
     15 * In addition to that, this allows the user to create an allowlist approach,
     16 * by using the special "<all_urls>" pattern for the blocklist, and then
     17 * adding all allowlisted websites on the exceptions list.
     18 *
     19 * Note that this module only blocks top-level website navigations and embeds.
     20 * It does not block any other accesses to these urls: image tags, scripts, XHR, etc.,
     21 * because that could cause unexpected breakage. This is a policy to block
     22 * users from visiting certain websites, and not from blocking any network
     23 * connections to those websites. If the admin is looking for that, the recommended
     24 * way is to configure that with extensions or through a company firewall.
     25 */
     26 
     27 const LIST_LENGTH_LIMIT = 1000;
     28 
     29 const PREF_LOGLEVEL = "browser.policies.loglevel";
     30 
     31 const lazy = {};
     32 
     33 ChromeUtils.defineLazyGetter(lazy, "log", () => {
     34  let { ConsoleAPI } = ChromeUtils.importESModule(
     35    "resource://gre/modules/Console.sys.mjs"
     36  );
     37  return new ConsoleAPI({
     38    prefix: "WebsiteFilter Policy",
     39    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
     40    // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
     41    maxLogLevel: "error",
     42    maxLogLevelPref: PREF_LOGLEVEL,
     43  });
     44 });
     45 
     46 export let WebsiteFilter = {
     47  _observerAdded: false,
     48 
     49  init(blocklist, exceptionlist) {
     50    let blockArray = [],
     51      exceptionArray = [];
     52 
     53    for (let i = 0; i < blocklist.length && i < LIST_LENGTH_LIMIT; i++) {
     54      try {
     55        let pattern = new MatchPattern(blocklist[i].toLowerCase());
     56        blockArray.push(pattern);
     57        lazy.log.debug(
     58          `Pattern added to WebsiteFilter. Block: ${blocklist[i]}`
     59        );
     60      } catch (e) {
     61        lazy.log.error(
     62          `Invalid pattern on WebsiteFilter. Block: ${blocklist[i]}`
     63        );
     64      }
     65    }
     66 
     67    this._blockPatterns = new MatchPatternSet(blockArray);
     68 
     69    for (let i = 0; i < exceptionlist.length && i < LIST_LENGTH_LIMIT; i++) {
     70      try {
     71        let pattern = new MatchPattern(exceptionlist[i].toLowerCase());
     72        exceptionArray.push(pattern);
     73        lazy.log.debug(
     74          `Pattern added to WebsiteFilter. Exception: ${exceptionlist[i]}`
     75        );
     76      } catch (e) {
     77        lazy.log.error(
     78          `Invalid pattern on WebsiteFilter. Exception: ${exceptionlist[i]}`
     79        );
     80      }
     81    }
     82 
     83    if (exceptionArray.length) {
     84      this._exceptionsPatterns = new MatchPatternSet(exceptionArray);
     85    }
     86 
     87    let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
     88 
     89    if (!registrar.isContractIDRegistered(this.contractID)) {
     90      registrar.registerFactory(
     91        this.classID,
     92        this.classDescription,
     93        this.contractID,
     94        this
     95      );
     96 
     97      Services.catMan.addCategoryEntry(
     98        "content-policy",
     99        this.contractID,
    100        this.contractID,
    101        false,
    102        true
    103      );
    104    }
    105    // We have to do this to catch 30X redirects.
    106    // See bug 456957.
    107    if (!this._observerAdded) {
    108      this._observerAdded = true;
    109      // We rely on weak references, so we never remove this observer.
    110      Services.obs.addObserver(this, "http-on-examine-response", true);
    111    }
    112  },
    113 
    114  shouldLoad(contentLocation, loadInfo) {
    115    let contentType = loadInfo.externalContentPolicyType;
    116    let url = contentLocation.spec.toLowerCase();
    117    if (contentLocation.scheme == "view-source") {
    118      url = contentLocation.pathQueryRef;
    119    } else if (url.startsWith("about:reader?url=")) {
    120      url = decodeURIComponent(url.substr(17));
    121    }
    122    if (
    123      contentType == Ci.nsIContentPolicy.TYPE_DOCUMENT ||
    124      contentType == Ci.nsIContentPolicy.TYPE_SUBDOCUMENT
    125    ) {
    126      if (!this.isAllowed(url)) {
    127        return Ci.nsIContentPolicy.REJECT_POLICY;
    128      }
    129    }
    130    return Ci.nsIContentPolicy.ACCEPT;
    131  },
    132  shouldProcess() {
    133    return Ci.nsIContentPolicy.ACCEPT;
    134  },
    135  observe(subject) {
    136    try {
    137      let channel = subject.QueryInterface(Ci.nsIHttpChannel);
    138      if (
    139        !channel.isDocument ||
    140        channel.responseStatus < 300 ||
    141        channel.responseStatus >= 400
    142      ) {
    143        return;
    144      }
    145      let location = channel.getResponseHeader("location");
    146      // location might not be a fully qualified URL
    147      let url = URL.parse(location);
    148      if (!url) {
    149        url = URL.parse(location, channel.URI.spec);
    150      }
    151      if (url && !this.isAllowed(url.href)) {
    152        channel.cancel(Cr.NS_ERROR_BLOCKED_BY_POLICY);
    153      }
    154    } catch (e) {}
    155  },
    156  classDescription: "Policy Engine File Content Policy",
    157  contractID: "@mozilla-org/policy-engine-file-content-policy-service;1",
    158  classID: Components.ID("{c0bbb557-813e-4e25-809d-b46a531a258f}"),
    159  QueryInterface: ChromeUtils.generateQI([
    160    "nsIContentPolicy",
    161    "nsIObserver",
    162    "nsISupportsWeakReference",
    163  ]),
    164  createInstance(iid) {
    165    return this.QueryInterface(iid);
    166  },
    167  isAllowed(url) {
    168    if (this._blockPatterns?.matches(url.toLowerCase())) {
    169      if (
    170        !this._exceptionsPatterns ||
    171        !this._exceptionsPatterns.matches(url.toLowerCase())
    172      ) {
    173        return false;
    174      }
    175    }
    176    return true;
    177  },
    178 };