Bug 1429178 - Policy: Implement website blocklist/allowlist. r=mixedpuppy draft
authorFelipe Gomes <felipc@gmail.com>
Wed, 28 Mar 2018 09:29:17 -0500
changeset 775407 d795b96a0318e7b2bb21bf24c15413111628aa73
parent 775406 8534439b13fc8e62dadc7d6e22be357242b3c531
child 775467 7329dd6d372ab3472f5b746f49f88d6d845214b3
push id104713
push userfelipc@gmail.com
push dateSat, 31 Mar 2018 00:41:23 +0000
reviewersmixedpuppy
bugs1429178
milestone61.0a1
Bug 1429178 - Policy: Implement website blocklist/allowlist. r=mixedpuppy MozReview-Commit-ID: EAX0VwxlggK
browser/components/enterprisepolicies/Policies.jsm
browser/components/enterprisepolicies/helpers/WebsiteFilter.jsm
browser/components/enterprisepolicies/helpers/moz.build
browser/components/enterprisepolicies/helpers/sample_websitefilter.json
browser/components/enterprisepolicies/schemas/policies-schema.json
browser/components/enterprisepolicies/tests/browser/browser.ini
browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js
browser/components/enterprisepolicies/tests/browser/head.js
browser/components/enterprisepolicies/tests/browser/policy_websitefilter_block.html
browser/components/enterprisepolicies/tests/browser/policy_websitefilter_exception.html
js/xpconnect/src/xpc.msg
--- a/browser/components/enterprisepolicies/Policies.jsm
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -6,20 +6,21 @@
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "gXulStore",
                                    "@mozilla.org/xul/xulstore;1",
                                    "nsIXULStore");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
+  AddonManager: "resource://gre/modules/AddonManager.jsm",
   BookmarksPolicies: "resource:///modules/policies/BookmarksPolicies.jsm",
+  CustomizableUI: "resource:///modules/CustomizableUI.jsm",
   ProxyPolicies: "resource:///modules/policies/ProxyPolicies.jsm",
-  AddonManager: "resource://gre/modules/AddonManager.jsm",
-  CustomizableUI: "resource:///modules/CustomizableUI.jsm",
+  WebsiteFilter: "resource:///modules/policies/WebsiteFilter.jsm",
 });
 
 const PREF_LOGLEVEL           = "browser.policies.loglevel";
 const BROWSER_DOCUMENT_URL    = "chrome://browser/content/browser.xul";
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm", {});
   return new ConsoleAPI({
@@ -504,17 +505,24 @@ var Policies = {
               } catch (ex) {
                 log.error("Unable to set the default search engine", ex);
               }
             }
           });
         }
       });
     }
-  }
+  },
+
+  "WebsiteFilter": {
+    onBeforeUIStartup(manager, param) {
+      this.filter = new WebsiteFilter(param.Block || [], param.Exceptions || []);
+    }
+  },
+
 };
 
 /*
  * ====================
  * = HELPER FUNCTIONS =
  * ====================
  *
  * The functions below are helpers to be used by several policies.
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/helpers/WebsiteFilter.jsm
@@ -0,0 +1,108 @@
+/* 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/. */
+
+"use strict";
+
+/*
+ * This module implements the policy to block websites from being visited,
+ * or to only allow certain websites to be visited.
+ *
+ * The blocklist takes as input an array of MatchPattern strings, as documented
+ * at https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns.
+ *
+ * The exceptions list takes the same as input. This list opens up
+ * exceptions for rules on the blocklist that might be too strict.
+ *
+ * In addition to that, this allows the user to create a whitelist approach,
+ * by using the special "<all_urls>" pattern for the blocklist, and then
+ * adding all whitelisted websites on the exceptions list.
+ *
+ * Note that this module only blocks top-level website navigations. It doesn't
+ * block any other accesses to these urls: image tags, scripts, XHR, etc.,
+ * because that could cause unexpected breakage. This is a policy to block
+ * users from visiting certain websites, and not from blocking any network
+ * connections to those websites. If the admin is looking for that, the recommended
+ * way is to configure that with extensions or through a company firewall.
+ */
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const LIST_LENGTH_LIMIT = 1000;
+
+const PREF_LOGLEVEL = "browser.policies.loglevel";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm", {});
+  return new ConsoleAPI({
+    prefix: "WebsiteFilter Policy",
+    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+    // messages during development. See LOG_LEVELS in Console.jsm for details.
+    maxLogLevel: "error",
+    maxLogLevelPref: PREF_LOGLEVEL,
+  });
+});
+
+var EXPORTED_SYMBOLS = [ "WebsiteFilter" ];
+
+function WebsiteFilter(blocklist, exceptionlist) {
+  let blockArray = [], exceptionArray = [];
+
+  for (let i = 0; i < blocklist.length && i < LIST_LENGTH_LIMIT; i++) {
+    try {
+      let pattern = new MatchPattern(blocklist[i]);
+      blockArray.push(pattern);
+      log.debug(`Pattern added to WebsiteFilter.Block list: ${blocklist[i]}`);
+    } catch (e) {
+      log.error(`Invalid pattern on WebsiteFilter.Block: ${blocklist[i]}`);
+    }
+  }
+
+  this._blockPatterns = new MatchPatternSet(blockArray);
+
+  for (let i = 0; i < exceptionlist.length && i < LIST_LENGTH_LIMIT; i++) {
+    try {
+      let pattern = new MatchPattern(exceptionlist[i]);
+      exceptionArray.push(pattern);
+      log.debug(`Pattern added to WebsiteFilter.Exceptions list: ${exceptionlist[i]}`);
+    } catch (e) {
+      log.error(`Invalid pattern on WebsiteFilter.Exceptions: ${exceptionlist[i]}`);
+    }
+  }
+
+  if (exceptionArray.length) {
+    this._exceptionsPatterns = new MatchPatternSet(exceptionArray);
+  }
+
+  Services.obs.addObserver(this, "http-on-modify-request", true);
+}
+
+WebsiteFilter.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+
+  observe(subject, topic, data) {
+    let channel, isDocument = false;
+    try {
+      channel = subject.QueryInterface(Ci.nsIHttpChannel);
+      isDocument = channel.isDocument;
+    } catch (e) {
+      return;
+    }
+
+    // Only filter document accesses
+    if (!isDocument) {
+      return;
+    }
+
+    if (this._blockPatterns.matches(channel.URI)) {
+      if (!this._exceptionsPatterns ||
+          !this._exceptionsPatterns.matches(channel.URI)) {
+        // NS_ERROR_BLOCKED_BY_POLICY displays the error message
+        // designed for policy-related blocks.
+        channel.cancel(Cr.NS_ERROR_BLOCKED_BY_POLICY);
+      }
+    }
+  }
+};
--- a/browser/components/enterprisepolicies/helpers/moz.build
+++ b/browser/components/enterprisepolicies/helpers/moz.build
@@ -5,9 +5,10 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 with Files("**"):
     BUG_COMPONENT = ("Firefox", "Enterprise Policies")
 
 EXTRA_JS_MODULES.policies += [
     'BookmarksPolicies.jsm',
     'ProxyPolicies.jsm',
+    'WebsiteFilter.jsm',
 ]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/helpers/sample_websitefilter.json
@@ -0,0 +1,14 @@
+{
+  "policies": {
+    "WebsiteFilter": {
+      "Block": [
+        "*://*.mozilla.org/*",
+        "invalid_pattern"
+      ],
+
+      "Exceptions": [
+        "*://*.mozilla.org/*about*"
+      ]
+    }
+  }
+}
--- a/browser/components/enterprisepolicies/schemas/policies-schema.json
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -454,11 +454,34 @@
         },
         "Default": {
           "type": "string"
         },
         "PreventInstalls": {
           "type": "boolean"
         }
       }
+    },
+
+    "WebsiteFilter": {
+      "description": "Blocks websites from being visited. The parameters take an array of Match Patterns, as documented in https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns. Only http/https accesses are supported at the moment. The arrays are limited to 1000 entries each.",
+      "first_available": "60.0",
+      "enterprise_only": "true",
+
+      "type": "object",
+      "properties": {
+        "Block": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+
+        "Exceptions": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        }
+      }
     }
   }
 }
--- a/browser/components/enterprisepolicies/tests/browser/browser.ini
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -1,16 +1,18 @@
 [DEFAULT]
 support-files =
   head.js
   config_popups_cookies_addons_flash.json
   config_broken_json.json
   opensearch.html
   opensearchEngine.xml
   policytest.xpi
+  policy_websitefilter_block.html
+  policy_websitefilter_exception.html
 
 [browser_policies_basic_tests.js]
 [browser_policies_broken_json.js]
 [browser_policies_enterprise_only.js]
 [browser_policies_notice_in_aboutpreferences.js]
 [browser_policies_popups_cookies_addons_flash.js]
 [browser_policies_runOnce_helper.js]
 [browser_policies_setAndLockPref_API.js]
@@ -40,8 +42,9 @@ support-files =
 [browser_policy_disable_telemetry.js]
 [browser_policy_display_bookmarks.js]
 [browser_policy_display_menu.js]
 [browser_policy_extensions.js]
 [browser_policy_proxy.js]
 [browser_policy_search_engine.js]
 [browser_policy_searchbar.js]
 [browser_policy_set_homepage.js]
+[browser_policy_websitefilter.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const SUPPORT_FILES_PATH = "http://mochi.test:8888/browser/browser/components/enterprisepolicies/tests/browser";
+const BLOCKED_PAGE   = `${SUPPORT_FILES_PATH}/policy_websitefilter_block.html`;
+const EXCEPTION_PAGE = `${SUPPORT_FILES_PATH}/policy_websitefilter_exception.html`;
+
+add_task(async function test() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "WebsiteFilter": {
+        "Block": [
+          "*://mochi.test/*policy_websitefilter_*"
+        ],
+        "Exceptions": [
+          "*://mochi.test/*_websitefilter_exception*"
+        ]
+      }
+    }
+  });
+
+  await checkBlockedPage(BLOCKED_PAGE, true);
+  await checkBlockedPage(EXCEPTION_PAGE, false);
+});
--- a/browser/components/enterprisepolicies/tests/browser/head.js
+++ b/browser/components/enterprisepolicies/tests/browser/head.js
@@ -23,16 +23,33 @@ async function setupPolicyEngineWithJson
 function checkLockedPref(prefName, prefValue) {
   EnterprisePolicyTesting.checkPolicyPref(prefName, prefValue, true);
 }
 
 function checkUnlockedPref(prefName, prefValue) {
   EnterprisePolicyTesting.checkPolicyPref(prefName, prefValue, false);
 }
 
+// Checks that a page was blocked by seeing if it was replaced with about:neterror
+async function checkBlockedPage(url, expectedBlocked) {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url,
+    waitForLoad: false,
+    waitForStateStop: true,
+  }, async function() {
+    await BrowserTestUtils.waitForCondition(async function() {
+      let blocked = await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
+        return content.document.documentURI.startsWith("about:neterror");
+      });
+      return blocked == expectedBlocked;
+    }, `Page ${url} block was correct (expected=${expectedBlocked}).`);
+  });
+}
+
 add_task(async function policies_headjs_startWithCleanSlate() {
   if (Services.policies.status != Ci.nsIEnterprisePolicies.INACTIVE) {
     await setupPolicyEngineWithJson("");
   }
   is(Services.policies.status, Ci.nsIEnterprisePolicies.INACTIVE, "Engine is inactive at the start of the test");
 });
 
 registerCleanupFunction(async function policies_headjs_finishWithCleanSlate() {
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/policy_websitefilter_block.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>This page should be blocked</title>
+  </head>
+  <body>
+    This page should not be seen.
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/policy_websitefilter_exception.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>This page should not be blocked</title>
+  </head>
+  <body>
+    This page should be seen.
+  </body>
+</html>
--- a/js/xpconnect/src/xpc.msg
+++ b/js/xpconnect/src/xpc.msg
@@ -152,16 +152,17 @@ XPC_MSG_DEF(NS_ERROR_NET_RESET          
 XPC_MSG_DEF(NS_ERROR_NET_INTERRUPT                  , "The connection was established, but the data transfer was interrupted")
 XPC_MSG_DEF(NS_ERROR_NET_PARTIAL_TRANSFER           , "A transfer was only partially done when it completed")
 XPC_MSG_DEF(NS_ERROR_NOT_RESUMABLE                  , "This request is not resumable, but it was tried to resume it, or to request resume-specific data")
 XPC_MSG_DEF(NS_ERROR_ENTITY_CHANGED                 , "It was attempted to resume the request, but the entity has changed in the meantime")
 XPC_MSG_DEF(NS_ERROR_REDIRECT_LOOP                  , "The request failed as a result of a detected redirection loop")
 XPC_MSG_DEF(NS_ERROR_UNSAFE_CONTENT_TYPE            , "The request failed because the content type returned by the server was not a type expected by the channel")
 XPC_MSG_DEF(NS_ERROR_REMOTE_XUL                     , "Attempt to access remote XUL document that is not in website's whitelist")
 XPC_MSG_DEF(NS_ERROR_LOAD_SHOWED_ERRORPAGE          , "The load caused an error page to be displayed.")
+XPC_MSG_DEF(NS_ERROR_BLOCKED_BY_POLICY              , "The request was blocked by a policy set by the system administrator.")
 
 XPC_MSG_DEF(NS_ERROR_FTP_LOGIN                      , "FTP error while logging in")
 XPC_MSG_DEF(NS_ERROR_FTP_CWD                        , "FTP error while changing directory")
 XPC_MSG_DEF(NS_ERROR_FTP_PASV                       , "FTP error while changing to passive mode")
 XPC_MSG_DEF(NS_ERROR_FTP_PWD                        , "FTP error while retrieving current directory")
 XPC_MSG_DEF(NS_ERROR_FTP_LIST                       , "FTP error while retrieving a directory listing")
 XPC_MSG_DEF(NS_ERROR_UNKNOWN_HOST                   , "The lookup of the hostname failed")
 XPC_MSG_DEF(NS_ERROR_DNS_LOOKUP_QUEUE_FULL          , "The DNS lookup queue is full")