├── .gitignore ├── Makefile ├── screenshot.png ├── src ├── settings.html ├── content.js ├── settings.css ├── shared.js ├── settings.js ├── storage.js ├── popup.css ├── popup.html ├── popup.js └── bg.js ├── icon.svg ├── manifest.json ├── LICENSE ├── README.md └── CHANGES.md /.gitignore: -------------------------------------------------------------------------------- 1 | icon-128.png 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | bundle.zip: manifest.json icon.svg src/* 2 | zip $@ $^ 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xi/xiMatrix/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | xiMatrix — edit rules 7 | 8 | 9 |
10 | 14 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/content.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | const TYPE_MAP = { 4 | 'style-src': 'css', 5 | 'style-src-elem': 'css', 6 | 'style-src-attr': 'css', 7 | 'script-src': 'script', 8 | 'script-src-elem': 'script', 9 | 'script-src-attr': 'script', 10 | 'img-src': 'media', 11 | 'media-src': 'media', 12 | }; 13 | 14 | document.addEventListener('securitypolicyviolation', event => { 15 | var type = TYPE_MAP[event.effectiveDirective]; 16 | if (type) { 17 | browser.runtime.sendMessage({ 18 | type: 'securitypolicyviolation', 19 | data: type, 20 | }); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/settings.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | form { 12 | block-size: 100vb; 13 | display: grid; 14 | grid-template-rows: 1fr min-content; 15 | grid-template-columns: 1fr 1fr; 16 | grid-gap: 0.5em; 17 | padding: 0.5em; 18 | } 19 | 20 | label { 21 | display: flex; 22 | flex-direction: column; 23 | } 24 | 25 | textarea { 26 | block-size: 100%; 27 | resize: none; 28 | } 29 | 30 | button { 31 | padding-block: 0.5em; 32 | padding-inline: 2em; 33 | justify-self: end; 34 | grid-column: 1 / 3; 35 | } 36 | 37 | @media (prefers-color-scheme: dark) { 38 | :root { 39 | color-scheme: dark; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/shared.js: -------------------------------------------------------------------------------- 1 | export const TYPES = ['cookie', 'font', 'css', 'media', 'script', 'xhr', 'frame', 'other']; 2 | export const TYPE_MAP = { 3 | 'stylesheet': 'css', 4 | 'font': 'font', 5 | 'image': 'media', 6 | 'imageset': 'media', 7 | 'media': 'media', 8 | 'script': 'script', 9 | 'beacon': 'xhr', 10 | 'xmlhttprequest': 'xhr', 11 | 'websocket': 'xhr', 12 | 'sub_frame': 'frame', 13 | }; 14 | 15 | export const shouldAllow = function(rules, context, hostname, type) { 16 | var hostnames = ['*', hostname]; 17 | var parts = hostname.split('.'); 18 | while (parts.length > 2) { 19 | parts.shift(); 20 | hostnames.push(parts.join('.')); 21 | } 22 | if (context !== '*' && hostnames.some(h => h === context)) { 23 | hostnames.push('first-party'); 24 | } 25 | 26 | return [context, '*'].some(c => { 27 | return rules[c] && hostnames.some(h => { 28 | return rules[c][h] && [type, '*'].some(t => { 29 | return !!rules[c][h][t]; 30 | }); 31 | }); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | import * as storage from './storage.js'; 2 | 3 | const form = document.querySelector('form'); 4 | const textarea1 = document.querySelector('textarea.rules'); 5 | const textarea2 = document.querySelector('textarea.savedRules'); 6 | 7 | Promise.all([ 8 | storage.get('rules'), 9 | storage.get('savedRules'), 10 | ]).then(([rules, savedRules]) => { 11 | textarea1.value = JSON.stringify(rules, null, 2); 12 | textarea2.value = JSON.stringify(savedRules, null, 2); 13 | }); 14 | 15 | form.addEventListener('change', event => { 16 | try { 17 | JSON.parse(event.target.value); 18 | event.target.setCustomValidity(''); 19 | } catch (e) { 20 | event.target.setCustomValidity(e); 21 | event.target.reportValidity(); 22 | } 23 | }); 24 | 25 | form.addEventListener('submit', async event => { 26 | event.preventDefault(); 27 | await storage.change('rules', () => { 28 | return JSON.parse(textarea1.value); 29 | }); 30 | await storage.change('savedRules', () => { 31 | return JSON.parse(textarea2.value); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "xiMatrix", 4 | "author": "Tobias Bengfort", 5 | "homepage_url": "https://github.com/xi/xiMatrix", 6 | "description": "block requests based on domain and type", 7 | "version": "0.11.1", 8 | "action": { 9 | "default_title": "xiMatrix", 10 | "default_popup": "src/popup.html" 11 | }, 12 | "icons": { 13 | "32": "icon.svg", 14 | "64": "icon.svg" 15 | }, 16 | "background": { 17 | "scripts": ["src/bg.js"], 18 | "type": "module" 19 | }, 20 | "content_scripts": [{ 21 | "js": ["src/content.js"], 22 | "matches": [""], 23 | "run_at": "document_start" 24 | }], 25 | "options_ui": { 26 | "page": "src/settings.html", 27 | "open_in_tab": true 28 | }, 29 | "permissions": [ 30 | "storage", 31 | "tabs", 32 | "webNavigation", 33 | "webRequest", 34 | "webRequestBlocking" 35 | ], 36 | "host_permissions": [ 37 | "" 38 | ], 39 | "browser_specific_settings": { 40 | "gecko": { 41 | "id": "{936cea12-8e61-4929-b589-caece971bbd7}" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/storage.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | const STORAGE_DEFAULTS = { 4 | 'rules': {}, 5 | 'savedRules': {}, 6 | 'requests': {}, 7 | 'totals': {}, 8 | }; 9 | const STORAGE_AREAS = { 10 | 'rules': browser.storage.local, 11 | 'savedRules': browser.storage.local, 12 | 'requests': browser.storage.session, 13 | 'totals': browser.storage.session, 14 | }; 15 | var lock = Promise.resolve(); 16 | const cache = {}; 17 | 18 | const _get = async function(key) { 19 | var data = await STORAGE_AREAS[key].get(key); 20 | return data[key] ?? STORAGE_DEFAULTS[key]; 21 | }; 22 | 23 | export const get = function(key) { 24 | if (!cache[key]) { 25 | cache[key] = _get(key); 26 | } 27 | return cache[key]; 28 | }; 29 | 30 | const _change = async function(key, fn) { 31 | var oldValue = await get(key); 32 | var data = {}; 33 | data[key] = fn(oldValue); 34 | cache[key] = data[key]; 35 | await STORAGE_AREAS[key].set(data); 36 | }; 37 | 38 | export const change = async function(key, fn) { 39 | lock = lock.then(() => _change(key, fn)); 40 | await lock; 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tobias Bengfort 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/popup.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --green-light: #d0f0d0; 3 | --green-dark: #080; 4 | --red-light: #f8d0d0; 5 | --grey: #ccc; 6 | --text-on-light: #000; 7 | --text-on-dark: #fff; 8 | } 9 | 10 | .toolbar { 11 | display: flex; 12 | gap: 0.5em; 13 | justify-content: end; 14 | } 15 | 16 | table { 17 | background: var(--red-light); 18 | color: var(--text-on-light); 19 | border-spacing: 0; 20 | margin-block-end: 0.2em; 21 | } 22 | th, td { 23 | position: relative; 24 | border: 1px solid #fff; 25 | min-inline-size: 2.4em; 26 | line-height: 1.8; 27 | text-align: center; 28 | font-weight: normal; 29 | } 30 | th:first-child { 31 | text-align: right; 32 | } 33 | td.disabled { 34 | background-color: var(--grey); 35 | } 36 | 37 | table input { 38 | appearance: none; 39 | position: absolute; 40 | top: 0; 41 | bottom: 0; 42 | left: 0; 43 | right: 0; 44 | margin: 0; 45 | cursor: pointer; 46 | } 47 | table input:focus-visible { 48 | outline: 2px solid; 49 | z-index: 1; 50 | } 51 | .inherit-allow { 52 | background: var(--green-light); 53 | } 54 | table input:checked { 55 | background: var(--green-dark); 56 | } 57 | table input ~ span { 58 | pointer-events: none; 59 | position: relative; 60 | z-index: 1; 61 | } 62 | table input:checked ~ span { 63 | color: var(--text-on-dark); 64 | } 65 | 66 | @media (prefers-color-scheme: dark) { 67 | :root { 68 | color-scheme: dark; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | xiMatrix - block requests based on domain and type 2 | 3 | This extension is heavily inspired by 4 | [uMatrix](https://github.com/gorhill/uMatrix), but much simpler. By default, 5 | all web requests as well as inline code are blocked. You can then define rules 6 | to allow only those requests you want. Definitely for advanced users. 7 | 8 | Available for Firefox here: https://addons.mozilla.org/firefox/addon/ximatrix/ 9 | 10 | ![screenshot](screenshot.png) 11 | 12 | ## Differences to uMatrix 13 | 14 | - advantages 15 | - uMatrix is no longer maintained. The last release was in July 2021. 16 | - keyboard navigation 17 | - simpler code 18 | - rules are encoded as JSON 19 | - it is possible to control inline scripts, styles, and images 20 | - there is a separate column for fonts 21 | - disadvantages / simplifications 22 | - no settings at all 23 | - less flexible rules 24 | - there are no block rules, so it is not possible to overwrite an extensive 25 | allow rule with a more specific block rule 26 | - images and media have been combined into a single column 27 | - I am sure some of the details of the rule inheritance are different. I 28 | tried to produce something that works for me rather than copying every 29 | detail. 30 | - the popup is not updated while it is open. You have to close and open it 31 | again to refresh the data. 32 | - blocked images are not replaced by a placeholder 33 | 34 | ## Known issues 35 | 36 | - cached requests are not included in the request counts 37 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | 14 |
15 |
16 | Help 17 |

In the table above, the columns represent different types of requests. The rows represent domains. Numbers (if recording is enabled) show how many requests of a given type the current tab tries to make to the given domain. Red cells are blocked, green cells are allowed. Light green cells are allowed indirectly, e.g. because they represent a sub-domain of an allowed domain. Grey cells are disabled.

18 |

By default, everything is blocked. You can click on a cell to allow it. You can also click on the domain or type to allow the complete row or column. There are also some special rows:

19 |
    20 |
  • inline: This row is in charge of blocking inline code, e.g. <script>-elements that are directly embedded into the HTML code. Note that some cells are disabled, e.g. there is no such thing as an "inline XHR".
  • 21 |
  • first-party: This row allows you to set global defaults for requests to the same domain as the page itself.
  • 22 |
  • sub-domains: If you allow a domain, all of its sub-domains are allowed along with it.
  • 23 |
24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # 0.11.1 (2025-10-05) 2 | 3 | - prompt for host permissions when not available 4 | 5 | 6 | # 0.11.0 (2025-10-04) 7 | 8 | - only close popup on toplevel navigation 9 | - display number of blocked requests in badge 10 | - remove the option to disable recording 11 | 12 | 13 | # 0.10.0 (2025-04-25) 14 | 15 | - update to manifest v3 16 | - client side JSON validation for rules 17 | 18 | 19 | # 0.9.0 (2023-12-09) 20 | 21 | - Performance improvements 22 | 23 | 24 | # 0.8.0 (2023-12-08) 25 | 26 | - Store requests to session storage instead of local storage 27 | - Add experimental support for domain patterns 28 | 29 | 30 | # 0.7.1 (2023-10-08) 31 | 32 | - Fix detection of inline scripts and CSS in firefox 115 33 | 34 | 35 | # 0.7.0 (2023-07-06) 36 | 37 | - fix: do not count main request as "other" 38 | - add dark mode 39 | 40 | 41 | # 0.6.0 (2023-05-18) 42 | 43 | - allow to block cookies 44 | 45 | 46 | # 0.5.0 (2023-05-03) 47 | 48 | - add reset button 49 | - flow-relative CSS 50 | - settings: add labels 51 | - add lang attribute in HTML 52 | 53 | 54 | # 0.4.0 (2023-02-25) 55 | 56 | - allow to have temporary and permanent rules 57 | - fix: add title for settings page 58 | 59 | 60 | # 0.3.1 (2022-11-27) 61 | 62 | - fix: do not clear request counts on navigation inside frame 63 | 64 | 65 | # 0.3.0 (2022-11-27) 66 | 67 | - fix concurrency issue when recording requests 68 | - allow to disable recording of requests 69 | - move edit raw rules to settings page 70 | - add explicit extension ID 71 | 72 | 73 | # 0.2.1 (2022-10-15) 74 | 75 | - fix compatibility with event pages 76 | - fix: add DOCTYPE for popup 77 | 78 | 79 | # 0.2.0 (2022-08-08) 80 | 81 | - allow to edit rules as JSON 82 | - fix inline counts if allowed 83 | - fix outdated help text 84 | 85 | 86 | # 0.1.0 (2022-08-07) 87 | 88 | - close popup on reload 89 | - detect counts of blocked inline code 90 | - display URLs of blocked subframes 91 | - fix layout warnings 92 | 93 | 94 | # 0.0.1 (2022-08-01) 95 | 96 | - fix extension icon 97 | - fix: include subdomains in first-party rule 98 | - fix hostname sorting for first party subdomains 99 | 100 | 101 | # 0.0.0 (2022-07-30) 102 | 103 | - initial release 104 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | import * as shared from './shared.js'; 4 | 5 | var context; 6 | var requests; 7 | var rules; 8 | 9 | const table = document.querySelector('table'); 10 | const commitButton = document.querySelector('[name="commit"]'); 11 | const resetButton = document.querySelector('[name="reset"]'); 12 | const permissionsButton = document.querySelector('[name="permissions"]'); 13 | 14 | const permissions = { 15 | origins: [''], 16 | }; 17 | 18 | const sendMessage = async function(type, data) { 19 | return await browser.runtime.sendMessage({type: type, data: data}); 20 | }; 21 | 22 | const getHostnames = function() { 23 | var hostnames = []; 24 | 25 | var addSubdomains = function(h) { 26 | if (['inline', 'first-party', '*'].includes(h)) { 27 | return; 28 | } 29 | hostnames.unshift(h); 30 | var parts = h.split('.'); 31 | while (parts.length > 2) { 32 | parts.shift(); 33 | hostnames.unshift(parts.join('.')); 34 | } 35 | }; 36 | 37 | for (const hostname in rules[context]) { 38 | addSubdomains(hostname); 39 | } 40 | for (const hostname in requests) { 41 | addSubdomains(hostname); 42 | } 43 | 44 | addSubdomains(context); 45 | 46 | var contextRoot = context.split('.').slice(-2).join('.'); 47 | hostnames = hostnames 48 | .map(h => { 49 | var parts = h.split('.'); 50 | var root = parts.slice(-2).join('.'); 51 | var isContext = root === contextRoot ? 0 : 1; 52 | return [isContext, parts.reverse()]; 53 | }) 54 | .sort() 55 | .map(a => a[1].reverse().join('.')); 56 | 57 | return hostnames.filter((value, i) => hostnames.indexOf(value) === i); 58 | }; 59 | 60 | const updateInherit = function(type) { 61 | var selector = 'input'; 62 | if (type !== '*') { 63 | selector += `[data-type="${type}"]`; 64 | } 65 | table.querySelectorAll(selector).forEach(input => { 66 | input.classList.toggle('inherit-allow', shared.shouldAllow( 67 | rules, 68 | context, 69 | input.dataset.hostname, 70 | input.dataset.type, 71 | )); 72 | }); 73 | }; 74 | 75 | const createCheckbox = function(hostname, type) { 76 | var input = document.createElement('input'); 77 | input.type = 'checkbox'; 78 | input.dataset.hostname = hostname; 79 | input.dataset.type = type; 80 | 81 | var c = (hostname === 'first-party') ? '*' : context; 82 | input.checked = (rules[c][hostname] || {})[type]; 83 | 84 | input.onchange = async () => { 85 | var newRules = await sendMessage('setRule', { 86 | context: context, 87 | hostname: hostname, 88 | type: type, 89 | value: input.checked, 90 | }); 91 | rules = newRules; 92 | commitButton.disabled = !rules.dirty; 93 | resetButton.disabled = !rules.dirty; 94 | updateInherit(type); 95 | }; 96 | 97 | return input; 98 | }; 99 | 100 | const createCell = function(tag, hostname, type, text) { 101 | const cell = document.createElement(tag); 102 | cell.append(createCheckbox(hostname, type)); 103 | 104 | const span = document.createElement('span'); 105 | span.textContent = text; 106 | cell.append(span); 107 | 108 | return cell; 109 | }; 110 | 111 | const createHeader = function() { 112 | var tr = document.createElement('tr'); 113 | 114 | var th = document.createElement('th'); 115 | th.textContent = context; 116 | tr.append(th); 117 | 118 | for (const type of shared.TYPES) { 119 | tr.append(createCell('th', '*', type, type)); 120 | } 121 | return tr; 122 | }; 123 | 124 | const createRow = function(hostname) { 125 | var tr = document.createElement('tr'); 126 | tr.append(createCell('th', hostname, '*', hostname)); 127 | for (const type of shared.TYPES) { 128 | const count = (requests[hostname] || {})[type]; 129 | 130 | if (hostname !== 'inline' || ['css', 'script', 'media'].includes(type)) { 131 | tr.append(createCell('td', hostname, type, count)); 132 | } else { 133 | const td = document.createElement('td'); 134 | td.className = 'disabled'; 135 | tr.append(td); 136 | } 137 | } 138 | return tr; 139 | }; 140 | 141 | const loadContext = async function() { 142 | var data = await sendMessage('get'); 143 | context = data.context; 144 | requests = data.requests; 145 | rules = data.rules; 146 | commitButton.disabled = !rules.dirty; 147 | resetButton.disabled = !rules.dirty; 148 | 149 | table.innerHTML = ''; 150 | table.append(createHeader()); 151 | table.append(createRow('inline')); 152 | table.append(createRow('first-party')); 153 | 154 | for (const hostname of getHostnames()) { 155 | table.append(createRow(hostname)); 156 | } 157 | 158 | updateInherit('*'); 159 | }; 160 | 161 | browser.webNavigation.onBeforeNavigate.addListener(details => { 162 | if (details.frameId === 0) { 163 | window.close(); 164 | } 165 | }); 166 | 167 | document.querySelector('[name="settings"]').addEventListener('click', () => { 168 | browser.runtime.openOptionsPage(); 169 | }); 170 | 171 | document.addEventListener('DOMContentLoaded', async () => { 172 | var hasPermissions = await browser.permissions.contains(permissions); 173 | if (hasPermissions) { 174 | await loadContext(); 175 | } else { 176 | permissionsButton.hidden = false; 177 | } 178 | }); 179 | 180 | commitButton.addEventListener('click', async () => { 181 | await sendMessage('commit', context); 182 | commitButton.disabled = true; 183 | resetButton.disabled = true; 184 | }); 185 | 186 | resetButton.addEventListener('click', async () => { 187 | await sendMessage('reset', context); 188 | await loadContext(); 189 | }); 190 | 191 | permissionsButton.addEventListener('click', async () => { 192 | await browser.permissions.request(permissions); 193 | window.close(); 194 | }); 195 | -------------------------------------------------------------------------------- /src/bg.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 3 | import * as shared from './shared.js'; 4 | import * as storage from './storage.js'; 5 | 6 | const glob = function(s, pattern) { 7 | var p = pattern.split('*'); 8 | return s.startsWith(p[0]) && s.endsWith(p.at(-1)); 9 | }; 10 | 11 | const getHostname = function(url, patterns) { 12 | var u = new URL(url); 13 | 14 | for (var pattern of patterns) { 15 | if (glob(u.hostname, pattern)) { 16 | return pattern; 17 | } 18 | } 19 | 20 | return u.hostname; 21 | }; 22 | 23 | const setRule = async function(context, hostname, type, rule) { 24 | var savedRules = await storage.get('savedRules'); 25 | await storage.change('rules', rules => { 26 | if (hostname === 'first-party') { 27 | context = '*'; 28 | } 29 | if (!rules[context]) { 30 | rules[context] = savedRules[context] || {}; 31 | } 32 | if (!rules[context][hostname]) { 33 | rules[context][hostname] = {}; 34 | } 35 | if (rule) { 36 | rules[context][hostname][type] = rule; 37 | } else { 38 | delete rules[context][hostname][type]; 39 | if (Object.keys(rules[context][hostname]).length === 0) { 40 | delete rules[context][hostname]; 41 | } 42 | if (Object.keys(rules[context]).length === 0 && !savedRules[context]) { 43 | delete rules[context]; 44 | } 45 | } 46 | return rules; 47 | }); 48 | }; 49 | 50 | const getPatterns = async function() { 51 | var savedRules = await storage.get('savedRules'); 52 | return savedRules._patterns || []; 53 | }; 54 | 55 | const getRules = async function(context) { 56 | var [rules, savedRules] = await Promise.all([ 57 | storage.get('rules'), 58 | storage.get('savedRules'), 59 | ]); 60 | var restricted = {}; 61 | restricted['*'] = rules['*'] || savedRules['*'] || {}; 62 | restricted[context] = rules[context] || savedRules[context] || {}; 63 | restricted.dirty = !!rules[context]; 64 | return restricted; 65 | }; 66 | 67 | const increaseTotals = async function(tabId) { 68 | var value = 0; 69 | await storage.change('totals', totals => { 70 | value = (totals[tabId] || 0) + 1; 71 | totals[tabId] = value; 72 | return totals; 73 | }); 74 | await browser.action.setBadgeBackgroundColor({color: '#6b6b6b', tabId: tabId}); 75 | await browser.action.setBadgeText({text: '' + value, tabId: tabId}); 76 | }; 77 | 78 | const pushRequest = async function(tabId, hostname, type, allowed) { 79 | await storage.change('requests', requests => { 80 | if (!requests[tabId]) { 81 | requests[tabId] = {}; 82 | } 83 | if (!requests[tabId][hostname]) { 84 | requests[tabId][hostname] = {}; 85 | } 86 | if (!requests[tabId][hostname][type]) { 87 | requests[tabId][hostname][type] = 0; 88 | } 89 | requests[tabId][hostname][type] += 1; 90 | return requests; 91 | }); 92 | if (!allowed) { 93 | await increaseTotals(tabId); 94 | } 95 | }; 96 | 97 | const clearRequests = async function(tabId) { 98 | await Promise.all([ 99 | storage.change('requests', requests => { 100 | if (requests[tabId]) { 101 | delete requests[tabId]; 102 | } 103 | return requests; 104 | }), 105 | storage.change('totals', totals => { 106 | if (totals[tabId]) { 107 | delete totals[tabId]; 108 | } 109 | return totals; 110 | }), 111 | ]); 112 | }; 113 | 114 | const getCurrentTab = async function() { 115 | var tabs = await browser.tabs.query({ 116 | active: true, 117 | currentWindow: true, 118 | }); 119 | return tabs[0]; 120 | }; 121 | 122 | browser.runtime.onMessage.addListener(async (msg, sender) => { 123 | if (msg.type === 'get') { 124 | const [tab, patterns] = await Promise.all([ 125 | getCurrentTab(), 126 | getPatterns(), 127 | ]); 128 | const context = getHostname(tab.url, patterns); 129 | const [rules, requests] = await Promise.all([ 130 | getRules(context), 131 | storage.get('requests'), 132 | ]); 133 | return { 134 | context: context, 135 | rules: rules, 136 | requests: requests[tab.id] || {}, 137 | }; 138 | } else if (msg.type === 'setRule') { 139 | await setRule( 140 | msg.data.context, 141 | msg.data.hostname, 142 | msg.data.type, 143 | msg.data.value, 144 | ); 145 | return await getRules(msg.data.context); 146 | } else if (msg.type === 'commit') { 147 | let r; 148 | await storage.change('rules', rules => { 149 | r = rules[msg.data]; 150 | delete rules[msg.data]; 151 | return rules; 152 | }); 153 | await storage.change('savedRules', savedRules => { 154 | if (Object.keys(r).length === 0) { 155 | delete savedRules[msg.data]; 156 | } else { 157 | savedRules[msg.data] = r; 158 | } 159 | return savedRules; 160 | }); 161 | } else if (msg.type === 'reset') { 162 | await storage.change('rules', rules => { 163 | delete rules[msg.data]; 164 | return rules; 165 | }); 166 | } else if (msg.type === 'securitypolicyviolation') { 167 | var patterns = await getPatterns(); 168 | var context = getHostname(sender.tab.url, patterns); 169 | var rules = await getRules(context); 170 | var allowed = shared.shouldAllow(rules, context, 'inline', msg.data); 171 | await pushRequest(sender.tab.id, 'inline', msg.data, allowed); 172 | } 173 | }); 174 | 175 | browser.tabs.onRemoved.addListener(clearRequests); 176 | browser.webNavigation.onBeforeNavigate.addListener(async details => { 177 | if (details.frameId === 0) { 178 | await clearRequests(details.tabId); 179 | } 180 | }); 181 | 182 | browser.webRequest.onBeforeSendHeaders.addListener(async details => { 183 | var patterns = await getPatterns(); 184 | var context = getHostname(details.documentUrl || details.url, patterns); 185 | if (details.frameAncestors && details.frameAncestors.length) { 186 | var last = details.frameAncestors.length - 1; 187 | context = getHostname(details.frameAncestors[last].url, patterns); 188 | } 189 | var hostname = getHostname(details.url, patterns); 190 | var type = shared.TYPE_MAP[details.type] || 'other'; 191 | 192 | var rules = await getRules(context); 193 | 194 | if (details.type !== 'main_frame') { 195 | const allowed = shared.shouldAllow(rules, context, hostname, type); 196 | await pushRequest(details.tabId, hostname, type, allowed); 197 | } 198 | 199 | var isCookie = h => h.name.toLowerCase() === 'cookie'; 200 | if (details.requestHeaders.some(isCookie)) { 201 | const allowed = shared.shouldAllow(rules, context, hostname, 'cookie'); 202 | await pushRequest(details.tabId, hostname, 'cookie', allowed); 203 | } 204 | 205 | if ( 206 | details.type !== 'main_frame' 207 | && !shared.shouldAllow(rules, context, hostname, type) 208 | ) { 209 | if (details.type === 'sub_frame') { 210 | // this can in turn be blocked by a local CSP 211 | return {redirectUrl: 'data:,' + encodeURIComponent(details.url)}; 212 | } else { 213 | return {cancel: true}; 214 | } 215 | } 216 | 217 | if (shared.shouldAllow(rules, context, hostname, 'cookie')) { 218 | return {requestHeaders: details.requestHeaders}; 219 | } else { 220 | var filtered = details.requestHeaders.filter(h => !isCookie(h)); 221 | return {requestHeaders: filtered}; 222 | } 223 | }, {urls: ['']}, ['blocking', 'requestHeaders']); 224 | 225 | browser.webRequest.onHeadersReceived.addListener(async details => { 226 | var patterns = await getPatterns(); 227 | var context = getHostname(details.url, patterns); 228 | var rules = await getRules(context); 229 | var csp = (type, value) => { 230 | var name = 'Content-Security-Policy'; 231 | if (shared.shouldAllow(rules, context, 'inline', type)) { 232 | name = 'Content-Security-Policy-Report-Only'; 233 | } 234 | details.responseHeaders.push({ 235 | name: name, 236 | value: value, 237 | }); 238 | }; 239 | 240 | csp('css', "style-src 'self' *"); 241 | csp('script', "script-src 'self' *"); 242 | csp('media', "img-src 'self' *"); 243 | 244 | return {responseHeaders: details.responseHeaders}; 245 | }, { 246 | urls: [''], 247 | types: ['main_frame'], 248 | }, ['blocking', 'responseHeaders']); 249 | --------------------------------------------------------------------------------