Add Android support + now don't block the whole page if a sub-resource is served via CF

This commit is contained in:
libroot 2026-10-11 21:39:08 +00:00
commit b73bf5d4ca
7 changed files with 132 additions and 114 deletions

1
README
View file

@ -3,5 +3,6 @@
* Detects Cloudflare-related headers in outgoing requests. * Detects Cloudflare-related headers in outgoing requests.
* Blocks the request if Cloudflare is present. * Blocks the request if Cloudflare is present.
* Toggle the blocker on/off via the extension popup. * Toggle the blocker on/off via the extension popup.
* Android support added in version 1.1.0
Available from Mozilla Add-ons: [https://addons.mozilla.org/en-US/firefox/addon/cloudflare-blocker/](https://addons.mozilla.org/en-US/firefox/addon/cloudflare-blocker/) Available from Mozilla Add-ons: [https://addons.mozilla.org/en-US/firefox/addon/cloudflare-blocker/](https://addons.mozilla.org/en-US/firefox/addon/cloudflare-blocker/)

View file

@ -1,69 +1,65 @@
// These are Cloudflare's headers, taken from here:
// https://developers.cloudflare.com/fundamentals/reference/http-headers/
const CLOUDFLARE_HEADERS = new Set([
'cf-connecting-ip',
'cf-connecting-ipv6',
'cf-ew-via',
'cf-pseudo-ipv4',
'cf-ray',
'cf-ipcountry',
'cf-visitor',
'cf-worker'
]);
let blockingEnabled = true; let blockingEnabled = true;
let stateLoaded = false;
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { const stateReady = browser.storage.local.get({ blockingEnabled: true })
if (message.action === 'toggleBlocking') { .then(result => { blockingEnabled = result.blockingEnabled; })
blockingEnabled = message.enabled; .catch(() => { /* keep the default */ })
} .then(() => { stateLoaded = true; });
if (message.action === 'getBlockingState') { browser.storage.onChanged.addListener((changes, area) => {
sendResponse({ action: 'setBlockingState', enabled: blockingEnabled }); if (area === 'local' && changes.blockingEnabled) {
blockingEnabled = changes.blockingEnabled.newValue;
} }
}); });
chrome.webRequest.onHeadersReceived.addListener( function blockedPageUrl(header, url) {
function(details) { const query = new URLSearchParams({
if (!blockingEnabled) { name: header.name,
return { cancel: false }; value: header.value || '',
} url: url
}).toString();
// These are Cloudflare's headers, taken from here: return browser.runtime.getURL('blocked.html') + '?' + query;
// https://developers.cloudflare.com/fundamentals/reference/http-headers/ }
const cloudflare_headers = [
'cf-connecting-ip',
'cf-connecting-ipv6',
'cf-ew-via',
'cf-Pseudo-ipv4',
'cf-ray',
'cf-ipcountry',
'cf-visitor',
'cf-worker'
];
// You might want capture all headers prefixed with "cf-":
// const cf_header = details.responseHeaders.find(header =>
// header.name.toLowerCase().startsWith('cf-')
// );
const cf_header = details.responseHeaders.find(header =>
cloudflare_headers.includes(header.name.toLowerCase())
);
if (cf_header) { function checkResponse(details) {
console.log('Cloudflare header found:', cf_header); if (!blockingEnabled) {
return {};
const query_params = new URLSearchParams({ }
name: cf_header.name,
value: cf_header.value,
url: details.url
}).toString();
/* const cfHeader = (details.responseHeaders || []).find(header =>
* It would be cool to pass the tab URL in the query params, but how? CLOUDFLARE_HEADERS.has(header.name.toLowerCase())
* When accessing chrome.tabs here, the active tab URL is "about:newtab". );
* Because we block the request immediately and the tab URL state doesn't if (!cfHeader) {
* never update to the URL we tried to access. I'd like to pass the URL return {};
* to the web.archive.org/tab_url_here in blocked.html. }
* Email contact@libroot.org if you know a solution!
*/
// Redirect to our custom blocked.html page if (details.type === 'main_frame') {
const url = chrome.runtime.getURL('blocked.html') + '?' + query_params; return { redirectUrl: blockedPageUrl(cfHeader, details.url) };
console.log('Redirecting to blocked page:', url); }
chrome.tabs.update(details.tabId, { url: url });
// Block the request, reject Cloudflare! // Subresource (image, script, stylesheet, font, iframe, fetch...): drop
return { cancel: true }; // just that request. The page itself keeps loading without it.
} // Makes the extension a bit more usable.
}, return { cancel: true };
}
browser.webRequest.onHeadersReceived.addListener(
details => stateLoaded
? checkResponse(details)
: stateReady.then(() => checkResponse(details)),
{ urls: ['<all_urls>'] }, { urls: ['<all_urls>'] },
['blocking', 'responseHeaders'] ['blocking', 'responseHeaders']
); );

View file

@ -5,16 +5,27 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloudflare page blocked</title> <title>Cloudflare page blocked</title>
<style> <style>
:root {
color-scheme: light dark;
}
body { body {
font-family: Arial, sans-serif; font-family: system-ui, Arial, sans-serif;
padding: 20px; padding: 16px;
text-align: center; text-align: center;
max-width: 40em;
margin: 0 auto;
} }
h1 { h1 {
color: red; color: red;
font-size: 1.4em;
}
pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
text-align: left;
} }
a { a {
color: blue; color: #0060df;
text-decoration: none; text-decoration: none;
} }
</style> </style>
@ -22,18 +33,17 @@
<body> <body>
<h1>This page has been blocked by the Cloudflare Blocker extension due to a Cloudflare header.</h1> <h1>This page has been blocked by the Cloudflare Blocker extension due to a Cloudflare header.</h1>
<p> <div>
<strong>Blocked Header:</strong> <strong>Blocked Header:</strong>
<pre id="header_info">Loading...</pre> <pre id="header_info">Loading...</pre>
</p> </div>
<p> <div>
<strong>Blocked response URL:</strong> <strong>Blocked response URL:</strong>
<pre id="url">Loading...</pre> <pre id="url">Loading...</pre>
</p> </div>
<br> <p>You can probably view the page on the <a id="wayback" href="https://web.archive.org" target="_blank" rel="noopener">Wayback Machine</a>.</p>
<p>You can probably view the page on the <a href="https://web.archive.org" target="_blank">Wayback Machine</a>.</p>
<script src="blocked.js"></script> <script src="blocked.js"></script>
</body> </body>

View file

@ -10,12 +10,14 @@ function getUrlParams() {
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const params = getUrlParams(); const params = getUrlParams();
if (params.name && params.value) { if (params.name) {
const header_info = `Name: ${params.name}\nValue: ${params.value}`; const header_info = `Name: ${params.name}\nValue: ${params.value || ''}`;
document.getElementById('header_info').textContent = header_info; document.getElementById('header_info').textContent = header_info;
} }
if (params.url) { if (params.url) {
document.getElementById('url').textContent = params.url; document.getElementById('url').textContent = params.url;
document.getElementById('wayback').href =
'https://web.archive.org/web/' + params.url;
} }
}); });

View file

@ -1,6 +1,6 @@
{ {
"name": "Cloudflare Blocker", "name": "Cloudflare Blocker",
"version": "1.0.0", "version": "1.1.0",
"author": "libroot.org", "author": "libroot.org",
"developer": { "developer": {
"name": "libroot", "name": "libroot",
@ -8,10 +8,19 @@
}, },
"description": "Blocks requests if they have Cloudflare's headers, and shows a blocked page.", "description": "Blocks requests if they have Cloudflare's headers, and shows a blocked page.",
"manifest_version": 2, "manifest_version": 2,
"browser_specific_settings": {
"gecko": {
"id": "cloudflare-blocker@libroot.org",
"strict_min_version": "115.0"
},
"gecko_android": {
"strict_min_version": "120.0"
}
},
"permissions": [ "permissions": [
"webRequest", "webRequest",
"webRequestBlocking", "webRequestBlocking",
"tabs", "storage",
"<all_urls>" "<all_urls>"
], ],
"background": { "background": {
@ -20,7 +29,8 @@
}, },
"browser_action": { "browser_action": {
"default_popup": "popup.html", "default_popup": "popup.html",
"default_icon": "icons/icon.png" "default_icon": "icons/icon.png",
"default_title": "Cloudflare Blocker"
}, },
"web_accessible_resources": [ "web_accessible_resources": [
"blocked.html" "blocked.html"

View file

@ -5,49 +5,60 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloudflare Blocker</title> <title>Cloudflare Blocker</title>
<style> <style>
:root {
color-scheme: light dark;
}
body { body {
font-family: Arial, sans-serif; font-family: system-ui, Arial, sans-serif;
padding: 10px; padding: 16px;
width: 200px; min-width: 200px;
margin: 0;
}
h3 {
margin-top: 0;
} }
#toggle { #toggle {
width: 50px; appearance: none;
height: 25px; border: 0;
padding: 0;
width: 72px;
height: 40px;
position: relative; position: relative;
border-radius: 15px; border-radius: 20px;
background-color: #ccc; background-color: #ccc;
cursor: pointer; cursor: pointer;
margin: 4px 0;
} }
#toggle .slider { #toggle .slider {
width: 25px; width: 32px;
height: 25px; height: 32px;
border-radius: 50%; border-radius: 50%;
background-color: white; background-color: white;
position: absolute; position: absolute;
top: 0; top: 4px;
transition: 0.3s; left: 4px;
transition: left 0.2s;
} }
#toggle.active { #toggle.active {
background-color: #4CAF50; background-color: #4CAF50;
} }
#toggle.active .slider { #toggle.active .slider {
left: 25px; left: 36px;
} }
#status { #status {
margin-top: 10px; margin-top: 12px;
font-size: 14px; font-size: 16px;
color: #333;
} }
</style> </style>
</head> </head>
<body> <body>
<h3>Cloudflare Blocker</h3> <h3>Cloudflare Blocker</h3>
<div id="toggle" class="active"> <button id="toggle" class="active" role="switch" aria-checked="true" aria-label="Block Cloudflare sites">
<div class="slider"></div> <span class="slider"></span>
</div> </button>
<p id="status">Blocking is ON</p> <p id="status">Blocking is ON</p>
<script src="popup.js"></script> <script src="popup.js"></script>
</body> </body>
</html> </html>

View file

@ -1,32 +1,20 @@
const toggle = document.getElementById('toggle'); const toggle = document.getElementById('toggle');
const status = document.getElementById('status'); const status = document.getElementById('status');
chrome.runtime.sendMessage({ action: 'getBlockingState' }, (response) => { function render(enabled) {
if (response && response.enabled !== undefined) { toggle.classList.toggle('active', enabled);
const blockingEnabled = response.enabled; toggle.setAttribute('aria-checked', String(enabled));
status.textContent = enabled ? 'Blocking is ON' : 'Blocking is OFF';
}
if (blockingEnabled) { // read and write the state straight from storage, so the toggle works even
toggle.classList.add('active'); // if the background page is currently unloaded
status.textContent = 'Blocking is ON'; browser.storage.local.get({ blockingEnabled: true })
} else { .then(result => render(result.blockingEnabled))
toggle.classList.remove('active'); .catch(() => render(true));
status.textContent = 'Blocking is OFF';
} toggle.addEventListener('click', () => {
} const enabled = !toggle.classList.contains('active');
render(enabled);
browser.storage.local.set({ blockingEnabled: enabled });
}); });
toggle.addEventListener('click', function () {
const isActive = toggle.classList.toggle('active');
if (isActive) {
status.textContent = 'Blocking is ON';
} else {
status.textContent = 'Blocking is OFF';
}
chrome.runtime.sendMessage({
action: 'toggleBlocking',
enabled: isActive
});
});