Bug 1429178 - Policy: Implement website blocklist/allowlist. r=mixedpuppy
MozReview-Commit-ID: EAX0VwxlggK
--- 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")