├── .gitignore ├── LICENSE ├── README.md ├── assets ├── chrome-store.png ├── little-rat-128x128.png ├── little-rat-16x16.png ├── little-rat-19x19.png ├── little-rat-38x38.png ├── little-rat-48x48.png ├── little-rat.png ├── screen-gh-local1.png ├── screen-gh-local2.png └── screen-gh-store1.png ├── manifest.json ├── scripts ├── build-chrome-store.sh └── gen-images.sh └── src ├── constants.js ├── ext-list.js ├── icons ├── toggle-off.svg ├── toggle-on.svg ├── volume-off.svg ├── volume-on.svg ├── wifi-off.svg └── wifi-on.svg ├── patch-dom.js ├── popup.css ├── popup.html ├── popup.js ├── rules_1.json └── service-worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | little-rat.zip 2 | build/ 3 | _metadata 4 | .DS_Store 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Daniel Nakov 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # little-rat 2 | 3 | 4 | # IMPORTANT: It's no longer possible to detect and block traffic from other extension unless you enable the *extensions-on-chrome-urls* flag `chrome://flags/#extensions-on-chrome-urls` or run Chrome with the `--extensions-on-chrome-urls`. See details [here](https://chromium-review.googlesource.com/c/chromium/src/+/5636396). 5 | 6 | 7 | 🐀 Small chrome extension to monitor (and optionally block) other extensions' network calls 8 | 9 | ### Manual Installation (Full Version) 10 | - Download the [ZIP](https://github.com/dnakov/little-rat/archive/refs/heads/main.zip) of this repo. 11 | - Unzip 12 | - Go to chromium/chrome *Extensions*. 13 | - Click to check *Developer mode*. 14 | - Click *Load unpacked extension...*. 15 | - In the file selector dialog: 16 | - Select the directory `little-rat-main` which was created above. 17 | - Click *Open*. 18 | - **IMPORTANT: ** Make sure you run Chrome with the `--extensions-on-chrome-urls` flag 19 | ### Screenshots 20 | Screenshot2 for Manual 21 | 22 | ### Open-Source Libraries <3 23 | - Icons from [feathericons.com](https://feathericons.com/) 24 | ### Author 25 | https://twitter.com/dnak0v 26 | 27 | 28 | -------------------------------------------------------------------------------- /assets/chrome-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnakov/little-rat/ba085e463d1ae17ee3ac582d90ec37ebc0370f57/assets/chrome-store.png -------------------------------------------------------------------------------- /assets/little-rat-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnakov/little-rat/ba085e463d1ae17ee3ac582d90ec37ebc0370f57/assets/little-rat-128x128.png -------------------------------------------------------------------------------- /assets/little-rat-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnakov/little-rat/ba085e463d1ae17ee3ac582d90ec37ebc0370f57/assets/little-rat-16x16.png -------------------------------------------------------------------------------- /assets/little-rat-19x19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnakov/little-rat/ba085e463d1ae17ee3ac582d90ec37ebc0370f57/assets/little-rat-19x19.png -------------------------------------------------------------------------------- /assets/little-rat-38x38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnakov/little-rat/ba085e463d1ae17ee3ac582d90ec37ebc0370f57/assets/little-rat-38x38.png -------------------------------------------------------------------------------- /assets/little-rat-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnakov/little-rat/ba085e463d1ae17ee3ac582d90ec37ebc0370f57/assets/little-rat-48x48.png -------------------------------------------------------------------------------- /assets/little-rat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnakov/little-rat/ba085e463d1ae17ee3ac582d90ec37ebc0370f57/assets/little-rat.png -------------------------------------------------------------------------------- /assets/screen-gh-local1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnakov/little-rat/ba085e463d1ae17ee3ac582d90ec37ebc0370f57/assets/screen-gh-local1.png -------------------------------------------------------------------------------- /assets/screen-gh-local2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnakov/little-rat/ba085e463d1ae17ee3ac582d90ec37ebc0370f57/assets/screen-gh-local2.png -------------------------------------------------------------------------------- /assets/screen-gh-store1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnakov/little-rat/ba085e463d1ae17ee3ac582d90ec37ebc0370f57/assets/screen-gh-store1.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "version": "1.4", 4 | "name": "Little Rat", 5 | "background": { 6 | "service_worker": "src/service-worker.js", 7 | "type": "module" 8 | }, 9 | "permissions": [ 10 | "declarativeNetRequest", "storage", "declarativeNetRequestFeedback", "management" 11 | ], 12 | "declarative_net_request": { 13 | "rule_resources": [ 14 | { 15 | "id": "ruleset_1", 16 | "enabled": true, 17 | "path": "src/rules_1.json" 18 | } 19 | ] 20 | }, 21 | "action": { 22 | "default_popup": "src/popup.html" 23 | }, 24 | "icons": { 25 | "128": "assets/little-rat-128x128.png", 26 | "48": "assets/little-rat-48x48.png", 27 | "16": "assets/little-rat-16x16.png" 28 | }, 29 | "options_page": "src/popup.html?dashboard" 30 | } -------------------------------------------------------------------------------- /scripts/build-chrome-store.sh: -------------------------------------------------------------------------------- 1 | VER=$1 2 | DIST=build/chrome 3 | rm -rf $DIST 4 | mkdir -p $DIST 5 | 6 | if [ ! -z "$VER" ]; then 7 | jq --arg VER "$VER" '.version = $VER | .optional_permissions = [ "management" ] | .permissions = .permissions - ["management", "declarativeNetRequestFeedback"]' manifest.json > $DIST/manifest.json 8 | else 9 | jq '.optional_permissions = [ "management" ] | .permissions = .permissions - ["management", "declarativeNetRequestFeedback"]' manifest.json > $DIST/manifest.json 10 | fi 11 | 12 | cp -r src $DIST/src 13 | mkdir -p $DIST/assets 14 | cp assets/little-rat-* $DIST/assets 15 | pushd $DIST 16 | zip little-rat.zip -qr ./* 17 | popd -------------------------------------------------------------------------------- /scripts/gen-images.sh: -------------------------------------------------------------------------------- 1 | declare -a sizes=("128x128" "48x48" "16x16" "19x19" "38x38") 2 | for size in "${sizes[@]}"; do 3 | convert assets/little-rat.png -resize $size assets/little-rat-$size.png 4 | done 5 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const IS_STORE = 'update_url' in chrome.runtime.getManifest() -------------------------------------------------------------------------------- /src/ext-list.js: -------------------------------------------------------------------------------- 1 | import { patchDOM } from './patch-dom.js'; 2 | let extensions = []; 3 | export async function updateList(exts = extensions, el = document.getElementById('exts-body')) { 4 | extensions = Object.values(exts).sort((a, b) => { 5 | const diff = (b.numRequestsAllowed + b.numRequestsBlocked) - (a.numRequestsAllowed + a.numRequestsBlocked); 6 | if(diff === 0) return a.name.localeCompare(b.name); 7 | return diff; 8 | }); 9 | 10 | 11 | const extsList = document.createElement('div'); 12 | extsList.id = 'exts-body' 13 | let innerHTML = '' 14 | for (let ext of extensions) { 15 | const tr = document.createElement('div'); 16 | tr.id = ext.id 17 | let icon 18 | if(!ext.icon || ext.icon.endsWith('undefined')) { 19 | icon = ''; 20 | } else { 21 | icon = `` 22 | } 23 | 24 | // NOTE: this is not XSS-safe but the risk is minimal, considering: 25 | // 1. Only runs in popup, where CSP doesn't not allow inline scripts 26 | // 2. Extension does not have the "tabs" permission or host permissions 27 | innerHTML += ` 28 |
29 | 30 |
31 |
32 |
33 |
34 |
${icon}
35 |
${ext.name}
36 |
${ext.numRequestsAllowed}${ext.numRequestsBlocked ? ` | ${ext.numRequestsBlocked}` : ''}
37 |
38 |
39 |
40 | ${Object.keys(ext.reqUrls).map(url => ` 41 |
42 |
${url}
43 |
${ext.reqUrls[url].allowed}${ext.reqUrls[url].blocked ? ` | ${ext.reqUrls[url].blocked}` : ''}
44 | `).join(' ')} 45 |
46 | 47 |
48 | ` 49 | } 50 | extsList.innerHTML = innerHTML; 51 | patchDOM(el, extsList, el.parent); 52 | } 53 | -------------------------------------------------------------------------------- /src/icons/toggle-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/toggle-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/volume-off.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/volume-on.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/wifi-off.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/wifi-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/patch-dom.js: -------------------------------------------------------------------------------- 1 | export function patchDOM(oldNode, newNode, parent) { 2 | if (!oldNode) { 3 | if (parent && newNode) { 4 | parent.appendChild(newNode); 5 | } 6 | return; 7 | } 8 | 9 | if (oldNode.isEqualNode(newNode)) return; 10 | 11 | if (oldNode.nodeName !== newNode.nodeName) { 12 | parent.replaceChild(newNode, oldNode); 13 | return; 14 | } 15 | 16 | if (oldNode.nodeType === Node.TEXT_NODE) { 17 | if (oldNode.textContent !== newNode.textContent) { 18 | oldNode.textContent = newNode.textContent; 19 | } 20 | return; 21 | } 22 | 23 | for (let i = oldNode.attributes.length - 1; i >= 0; i--) { 24 | const attrName = oldNode.attributes[i].name; 25 | if (!newNode.hasAttribute(attrName) && !attrName === 'open') { 26 | oldNode.removeAttribute(attrName); 27 | } 28 | } 29 | 30 | for (let i = 0; i < newNode.attributes.length; i++) { 31 | const attrName = newNode.attributes[i].name; 32 | const attrValue = newNode.attributes[i].value; 33 | oldNode.setAttribute(attrName, attrValue); 34 | } 35 | 36 | for (let i = 0; i < newNode.childNodes.length; i++) { 37 | if (oldNode.childNodes[i]) { 38 | patchDOM(oldNode.childNodes[i], newNode.childNodes[i], oldNode); 39 | } else { 40 | oldNode.appendChild(newNode.childNodes[i].cloneNode(true)); 41 | } 42 | } 43 | 44 | while (oldNode.childNodes.length > newNode.childNodes.length) { 45 | oldNode.removeChild(oldNode.lastChild); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/popup.css: -------------------------------------------------------------------------------- 1 | html { 2 | --bg-color-light: rgb(218,220,224); 3 | --bg-color-dark: #19191b; 4 | --text-color-light: #000; 5 | --text-color-dark: rgb(218,220,224); 6 | --stripe-color-light: #eeeeee7f; 7 | --stripe-color-dark: #3e3e3e7f; 8 | --stripe-color: var(--stripe-color-light); 9 | --bg-color: var(--bg-color-light); 10 | --text-color: var(--text-color-light); 11 | } 12 | html.dark { 13 | --bg-color: var(--bg-color-dark); 14 | --text-color: var(--text-color-dark); 15 | --stripe-color: var(--stripe-color-dark); 16 | background-color: var(--bg-color); 17 | color: var(--text-color); 18 | } 19 | #extensions tr { 20 | vertical-align: top; 21 | list-style-type: none; 22 | width: 100%; 23 | padding: 1em; 24 | } 25 | #container { 26 | min-width: 300px; 27 | font-size: 14px; 28 | } 29 | #logo { 30 | height: 24px; 31 | vertical-align:middle; 32 | } 33 | #reset { 34 | float: right; 35 | background: transparent; 36 | border: 1px solid var(--text-color); 37 | border-radius: 4px; 38 | color: inherit; 39 | } 40 | .grid { 41 | width: calc(100% - 20px); 42 | display: inline-grid; 43 | grid-template-columns: 16px 16px 16px 16px auto minmax(min-content, max-content); 44 | gap: .5rem; 45 | vertical-align: top; 46 | } 47 | .requests { 48 | margin: 0.5rem 0; 49 | width: 100%; 50 | display: inline-grid; 51 | row-gap: 4px; 52 | grid-template-columns: 20px auto minmax(50px, max-content); 53 | font-size: 0.75rem; 54 | > pre { 55 | word-break: break-all; 56 | white-space: pre-wrap; 57 | margin: 0; 58 | padding-left: 0.5rem; 59 | 60 | } 61 | > div { 62 | height: 100%; 63 | text-align: right; 64 | display: flex; 65 | align-items: center; 66 | justify-content: flex-end; 67 | padding-right: 0.5rem; 68 | } 69 | > *:nth-child(6n+4), > *:nth-child(6n+5), > *:nth-child(6n+6) { 70 | background-color: var(--stripe-color); 71 | } 72 | } 73 | .exts-body { 74 | width: 100%; 75 | } 76 | .pointer { 77 | cursor: pointer; 78 | } 79 | .item { 80 | > summary { 81 | margin-top: .5rem; 82 | } 83 | } 84 | .item-mute { 85 | background-image: url('icons/volume-on.svg'); 86 | background-size: 16px; 87 | background-repeat: no-repeat; 88 | } 89 | .item.muted { 90 | > summary { 91 | color: #999; 92 | } 93 | .item-mute { 94 | background-image: url('icons/volume-off.svg') 95 | } 96 | } 97 | .item-block { 98 | background-image: url('icons/wifi-on.svg'); 99 | background-size: 16px; 100 | background-repeat: no-repeat; 101 | } 102 | .item.blocked { 103 | > summary { 104 | color: #f00; 105 | } 106 | .item-block { 107 | background-image: url('icons/wifi-off.svg') 108 | } 109 | } 110 | 111 | .item-block.blocked { 112 | background-image: url('icons/wifi-off.svg') 113 | } 114 | 115 | .item-toggle { 116 | background-image: url('icons/toggle-off.svg'); 117 | background-size: 16px; 118 | background-repeat: no-repeat; 119 | } 120 | .theme { 121 | width: 16px; 122 | height: 16px; 123 | margin: auto 0; 124 | stroke: var(--text-color); 125 | } 126 | .buttons { 127 | display: flex; 128 | gap: 0.5rem; 129 | } 130 | .item:not(.enabled) { 131 | > summary { 132 | color: #5f6368; 133 | } 134 | } 135 | .item.enabled { 136 | .item-toggle { 137 | background-image: url('icons/toggle-on.svg'); 138 | } 139 | } 140 | .item[open] > summary { 141 | background-color: rgba(255, 255, 0, 0.25); 142 | } 143 | .app-header { 144 | display: flex; 145 | justify-content: space-between; 146 | border-bottom: 1px solid #5f6368; 147 | padding-bottom: 0.5em; 148 | } 149 | 150 | body.is-store .item { 151 | .item-mute, .item-reqNum { 152 | visibility: hidden; 153 | } 154 | > summary::marker { 155 | display: none; 156 | content: ""; 157 | } 158 | } 159 | 160 | .permissions { 161 | visibility: hidden; 162 | display: flex; 163 | justify-content: center; 164 | margin: 1rem; 165 | } 166 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Little Rat 4 | 5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 |

Little Rat

13 |
14 | 15 | 16 |
17 |
18 |
19 |
20 |
21 | 22 |

23 | NOTE
24 | You are using a version, installed from the store.
25 | This version has limited functionality due to APIs that are only available when the version is installed manually.
26 | For more information, visit the project page. 27 |

28 |
29 |
30 |
31 | 32 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | import { updateList } from './ext-list.js'; 2 | import { IS_STORE } from './constants.js'; 3 | 4 | const port = chrome.runtime.connect(undefined, { name: location.search?.includes('dashboard') ? 'dashboard' : 'popup' }); 5 | let theme; 6 | 7 | async function toggleMuteExt(e) { 8 | const id = e.target.id.replace('mute-', ''); 9 | await port.postMessage({ type: 'toggleMute', data: { id } }); 10 | await updateList(); 11 | } 12 | 13 | async function toggleBlockExt(e) { 14 | const id = e.target.id.replace('block-', ''); 15 | await port.postMessage({ type: 'toggleBlock', data: { id } }); 16 | await updateList(); 17 | } 18 | 19 | async function toggleBlockExtUrl(e) { 20 | const id = e.target.id.replace('block-ext-url-', ''); 21 | const url = e.target.dataset.url; 22 | const method = e.target.dataset.method; 23 | await port.postMessage({ type: 'toggleBlockExtUrl', data: { extId: id, method, url } }); 24 | await updateList(); 25 | } 26 | 27 | async function toggleExt(e) { 28 | const id = e.target.id.replace('toggle-', ''); 29 | await port.postMessage({ type: 'toggleExt', data: { id } }); 30 | await updateList(); 31 | } 32 | 33 | document.addEventListener('DOMContentLoaded', async () => { 34 | setTheme(); 35 | if (IS_STORE) { 36 | document.body.classList.add('is-store'); 37 | document.getElementById('reset').style.visibility = 'hidden'; 38 | } 39 | document.getElementById('exts-body').addEventListener('click', (e) => { 40 | if (e.target.id.startsWith('mute-')) { 41 | toggleMuteExt(e); 42 | e.preventDefault(); 43 | } else if (e.target.id.startsWith('block-ext-url-')) { 44 | toggleBlockExtUrl(e); 45 | e.preventDefault(); 46 | } else if (e.target.id.startsWith('block-')) { 47 | toggleBlockExt(e); 48 | e.preventDefault(); 49 | } else if (e.target.id.startsWith('toggle-')) { 50 | toggleExt(e); 51 | e.preventDefault(); 52 | } 53 | }) 54 | document.getElementById('reset').addEventListener('click', 55 | () => port.postMessage({ type: 'reset' })); 56 | document.getElementById('theme').addEventListener('click', toggleTheme); 57 | 58 | document.getElementById('request-perm').addEventListener('click', 59 | async () => { 60 | const granted = await chrome.permissions.request({ permissions: ['management'] }) 61 | if (granted) { 62 | chrome.runtime.reload(); 63 | } 64 | }); 65 | const hasPerm = await chrome.permissions.contains({ permissions: ['management'] }) 66 | if (!hasPerm) { 67 | document.querySelector('.permissions').style.visibility = 'visible'; 68 | } 69 | }); 70 | 71 | port.onMessage.addListener((message) => { 72 | if (message.type === 'init') { 73 | updateList(message.data); 74 | } 75 | }) 76 | 77 | function setTheme(theme) { 78 | theme = theme || localStorage.theme; 79 | if (!theme) { 80 | if (window.matchMedia('(prefers-color-scheme: dark)').matches) { 81 | theme = 'dark'; 82 | } else { 83 | theme = 'light'; 84 | } 85 | localStorage.theme = theme; 86 | } 87 | document.documentElement.classList.toggle('dark', theme === 'dark') 88 | } 89 | 90 | function toggleTheme() { 91 | localStorage.theme = localStorage.theme === 'dark' ? 'light' : 'dark'; 92 | setTheme(theme); 93 | } 94 | 95 | -------------------------------------------------------------------------------- /src/rules_1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 49999, 4 | "priority": 1, 5 | "action": { "type": "allow" }, 6 | "condition": { 7 | "urlFilter": "*", 8 | "resourceTypes": [ 9 | "main_frame", 10 | "sub_frame", 11 | "stylesheet", 12 | "script", 13 | "image", 14 | "font", 15 | "object", 16 | "xmlhttprequest", 17 | "ping", 18 | "csp_report", 19 | "media", 20 | "websocket", 21 | "webtransport", 22 | "webbundle", 23 | "other" 24 | ], 25 | "domainType": "thirdParty" 26 | } 27 | } 28 | ] -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | let badgeNum = 0; 2 | let ports = {}; 3 | let muted; 4 | let blocked; 5 | let blockedExtUrls = {}; 6 | let extRuleIds = {}; 7 | let recycleRuleIds = []; 8 | let maxRuleId = 1; 9 | let allRuleIds = [1]; 10 | let requests = {}; 11 | let needSave = false; 12 | let lastNotify = +new Date(); 13 | 14 | chrome.storage.local.get(s => { 15 | muted = s?.muted || {}; 16 | blocked = s?.blocked || {}; 17 | extRuleIds = s?.extRuleIds || {}; 18 | recycleRuleIds = s?.recycleRuleIds || []; 19 | maxRuleId = s?.maxRuleId || 1; 20 | allRuleIds = s?.allRuleIds || [1]; 21 | blockedExtUrls = s?.blockedExtUrls || {}; 22 | requests = s?.requests || {}; 23 | }); 24 | 25 | async function generateRuleId(extId) { 26 | extRuleIds[extId] = extRuleIds[extId] ?? []; 27 | let ruleId; 28 | if (recycleRuleIds.length > 0) { 29 | ruleId = recycleRuleIds.pop(); 30 | } else { 31 | ruleId = ++maxRuleId; 32 | } 33 | extRuleIds[extId].push(ruleId); 34 | extRuleIds[extId] = Array.from(new Set(extRuleIds[extId])); 35 | allRuleIds.push(ruleId); 36 | allRuleIds = Array.from(new Set(allRuleIds)); 37 | await chrome.storage.local.set({ extRuleIds, maxRuleId, allRuleIds }); 38 | return ruleId; 39 | } 40 | 41 | function debounce(func, delay) { 42 | let timeout; 43 | return function () { 44 | const context = this; 45 | const args = arguments; 46 | clearTimeout(timeout); 47 | timeout = setTimeout(() => func.apply(context, args), delay); 48 | }; 49 | } 50 | 51 | async function notifyPopup() { 52 | const data = await getExtensions(); 53 | Object.values(ports).forEach(port => port.postMessage({ type: 'init', data })); 54 | } 55 | 56 | const d_notifyPopup = debounce(notifyPopup, 1000); 57 | 58 | function updateBadge() { 59 | if (badgeNum > 0) { 60 | chrome.action.setBadgeBackgroundColor({ color: '#F00' }); 61 | chrome.action.setBadgeTextColor({ color: '#FFF' }); 62 | chrome.action.setBadgeText({ text: badgeNum.toString() }); 63 | } 64 | } 65 | 66 | async function setupListener() { 67 | const hasPerm = await chrome.permissions.contains({ permissions: ['declarativeNetRequestFeedback'] }) 68 | if (!hasPerm) { 69 | return; 70 | } 71 | if (!chrome.declarativeNetRequest?.onRuleMatchedDebug) return; 72 | chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((e) => { 73 | if (e.request.initiator?.startsWith('chrome-extension://')) { 74 | const k = e.request.initiator.replace('chrome-extension://', ''); 75 | if (!requests[k]) { 76 | requests[k] = { reqUrls: {}, numRequestsAllowed: 0, numRequestsBlocked: 0 }; 77 | } 78 | const req = requests[k]; 79 | const url = [e.request.method, e.request.url].filter(Boolean).join(' '); 80 | req.numRequestsAllowed = req.numRequestsAllowed || 0; 81 | req.numRequestsBlocked = req.numRequestsBlocked || 0; 82 | 83 | if (!req.reqUrls[url] || typeof req.reqUrls[url] !== 'object') { 84 | req.reqUrls[url] = { blocked: 0, allowed: typeof req.reqUrls[url] === 'number' ? req.reqUrls[url] : 0 }; 85 | } 86 | 87 | if (allRuleIds.includes(e.rule.ruleId)) { 88 | req.numRequestsBlocked += 1; 89 | req.reqUrls[url].blocked += 1; 90 | } else { 91 | req.numRequestsAllowed += 1; 92 | req.reqUrls[url].allowed += 1; 93 | } 94 | const urlObj = new URL(e.request.url); 95 | const blockedUrl = [urlObj.protocol, '//', urlObj.host, urlObj.pathname].filter(Boolean).join(''); 96 | req.reqUrls[url].isBlocked = blockedExtUrls[k]?.[blockedUrl] || false; 97 | 98 | needSave = true; 99 | 100 | if (!ports.popup && !muted?.[k]) { 101 | badgeNum += 1; 102 | updateBadge(); 103 | } 104 | d_notifyPopup(); 105 | } 106 | }); 107 | } 108 | 109 | setInterval(() => { 110 | if (needSave) { 111 | chrome.storage.local.set({ requests }); 112 | needSave = false; 113 | } 114 | }, 1000); 115 | 116 | async function getExtensions() { 117 | const extensions = {} 118 | const hasPerm = await chrome.permissions.contains({ permissions: ['management'] }) 119 | if (!hasPerm) return []; 120 | const extInfo = await chrome.management.getAll() 121 | for (let { enabled, name, id, icons } of extInfo) { 122 | extensions[id] = { 123 | name, 124 | id, 125 | numRequestsAllowed: 0, 126 | numRequestsBlocked: 0, 127 | reqUrls: {}, 128 | icon: icons?.[icons?.length - 1]?.url, 129 | blocked: blocked[id], 130 | muted: muted[id], 131 | enabled, 132 | ...(requests[id] || {}) 133 | }; 134 | } 135 | return extensions; 136 | } 137 | 138 | 139 | chrome.runtime.onConnect.addListener(async (port) => { 140 | const name = port.name; 141 | port.onDisconnect.addListener(() => { 142 | delete ports[name]; 143 | }) 144 | ports[name] = port; 145 | if (name === 'popup') { 146 | badgeNum = 0; 147 | chrome.action.setBadgeText({ text: '' }); 148 | } 149 | 150 | port.onMessage.addListener(async (message) => { 151 | if (message.type === 'reset') { 152 | requests = {}; 153 | const previousRules = await chrome.declarativeNetRequest.getDynamicRules(); 154 | await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: previousRules.map(rule => rule.id) }) 155 | await chrome.storage.local.set({ requests }); 156 | } 157 | await notifyPopup(); 158 | }); 159 | 160 | port.onMessage.addListener(async (message) => { 161 | if (message.type === 'toggleMute') { 162 | muted[message.data.id] = !muted[message.data.id]; 163 | chrome.storage.local.set({ muted }); 164 | } else if (message.type === 'toggleBlock') { 165 | blocked[message.data.id] = !blocked[message.data.id]; 166 | chrome.storage.local.set({ blocked }); 167 | updateBlockedRules(); 168 | } else if (message.type === 'toggleBlockExtUrl') { 169 | updateBlockedRules(message.data.extId, message.data.method, message.data.url); 170 | } else if (message.type === 'toggleExt') { 171 | const ext = await chrome.management.get(message.data.id) 172 | await chrome.management.setEnabled(message.data.id, !ext.enabled); 173 | } 174 | await notifyPopup(); 175 | }); 176 | 177 | await notifyPopup(); 178 | }); 179 | 180 | async function updateBlockedRules(extId, method, url) { 181 | if (!blocked[extId] && extId && url) { 182 | const urlObj = new URL(url); 183 | const blockUrl = [urlObj.protocol, '//', urlObj.host, urlObj.pathname].filter(Boolean).join(''); 184 | if (!blockedExtUrls[extId]) { 185 | blockedExtUrls[extId] = {} 186 | } 187 | blockedExtUrls[extId][blockUrl] = !blockedExtUrls[extId][blockUrl]; 188 | requests[extId] = requests[extId] ?? {}; 189 | requests[extId]['reqUrls'] = requests[extId]['reqUrls'] ?? {}; 190 | Object.entries(requests[extId]['reqUrls']).forEach(([url, urlInfo]) => { 191 | url.indexOf(blockUrl) > -1 && (urlInfo.isBlocked = blockedExtUrls[extId][blockUrl]); 192 | }); 193 | 194 | Object.entries(blockedExtUrls[extId]).forEach(([url, status]) => { 195 | !status && delete blockedExtUrls[extId][url]; 196 | }); 197 | 198 | d_notifyPopup(); 199 | await chrome.storage.local.set({ blockedExtUrls }); 200 | const removeRuleIds = extRuleIds[extId] || []; 201 | extRuleIds[extId] = []; 202 | recycleRuleIds = Array.from(new Set(recycleRuleIds.concat(removeRuleIds))); 203 | const urlFilters = Object.entries(blockedExtUrls[extId]).map(([url, status]) => url); 204 | const addRules = []; 205 | for (const url of urlFilters) { 206 | addRules.push({ 207 | id: await generateRuleId(extId), 208 | priority: 999, 209 | action: { type: 'block' }, 210 | condition: { 211 | resourceTypes: ['main_frame', 'sub_frame', 'stylesheet', 'script', 'image', 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', 'media', 'websocket', 'webtransport', 'webbundle', 'other'], 212 | domainType: 'thirdParty', 213 | initiatorDomains: [extId], 214 | urlFilter: `${url}*` 215 | } 216 | }) 217 | } 218 | try { 219 | await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds, addRules }) 220 | await chrome.storage.local.set({ recycleRuleIds, extRuleIds }); 221 | } catch (e) { 222 | const previousRules = await chrome.declarativeNetRequest.getDynamicRules(); 223 | console.log({ e, previousRules, removeRuleIds, addRules }) 224 | } 225 | } else { 226 | let initiatorDomains = [] 227 | for (let k in blocked) { 228 | if (blocked[k]) { 229 | initiatorDomains.push(k) 230 | } 231 | } 232 | let addRules; 233 | if (initiatorDomains.length) { 234 | addRules = [{ 235 | id: 1, 236 | priority: 999, 237 | action: { type: 'block' }, 238 | condition: { 239 | resourceTypes: ['main_frame', 'sub_frame', 'stylesheet', 'script', 'image', 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', 'media', 'websocket', 'webtransport', 'webbundle', 'other'], 240 | domainType: 'thirdParty', 241 | initiatorDomains 242 | } 243 | }] 244 | } 245 | 246 | await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [1], addRules }) 247 | } 248 | } 249 | 250 | setupListener(); 251 | chrome.runtime.onInstalled.addListener(() => { 252 | chrome.runtime.openOptionsPage(); 253 | }); 254 | --------------------------------------------------------------------------------