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 };