├── .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 |
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 |
18 |
19 |
20 |
21 |
Grant permission to see extensions
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 |
--------------------------------------------------------------------------------