/** * This file is part of Haketilo. * * Function: Main background script. * * Copyright (C) 2021 Wojtek Kosior * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * As additional permission under GNU GPL version 3 section 7, you * may distribute forms of that code without the copy of the GNU * GPL normally required by section 4, provided you include this * license notice and, in case of non-source distribution, a URL * through which recipients can access the Corresponding Source. * If you modify file(s) with this exception, you may extend this * exception to your version of the file(s), but you are not * obligated to do so. If you do not wish to do so, delete this * exception statement from your version. * * As a special exception to the GPL, any HTML file which merely * makes function calls to this code, and for that purpose * includes it by reference shall be deemed a separate work for * copyright law purposes. If you modify this code, you may extend * this exception to your version of the code, but you are not * obligated to do so. If you do not wish to do so, delete this * exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * I, Wojtek Kosior, thereby promise not to sue for violation of this file's * license. Although I request that you do not make use this code in a * proprietary program, I am not going to enforce this in court. */ /* * IMPORTS_START * IMPORT TYPE_PREFIX * IMPORT get_storage * IMPORT light_storage * IMPORT start_storage_server * IMPORT start_page_actions_server * IMPORT browser * IMPORT is_privileged_url * IMPORT query_best * IMPORT inject_csp_headers * IMPORT apply_stream_filter * IMPORT is_chrome * IMPORT is_mozilla * IMPORTS_END */ start_storage_server(); start_page_actions_server(); async function init_ext(install_details) { if (install_details.reason != "install") return; let storage = await get_storage(); await storage.clear(); /* * Below we add sample settings to the extension. */ for (let setting of // The next line is replaced with the contents of /default_settings.json by the build script `DEFAULT SETTINGS` ) { let [key, value] = Object.entries(setting)[0]; storage.set(key[0], key.substring(1), value); } } browser.runtime.onInstalled.addListener(init_ext); /* * The function below implements a more practical interface for what it does by * wrapping the old query_best() function. */ function decide_policy_for_url(storage, policy_observable, url) { if (storage === undefined) return {allow: false}; const settings = {allow: policy_observable !== undefined && policy_observable.value}; const [pattern, queried_settings] = query_best(storage, url); if (queried_settings) { settings.payload = queried_settings.components; settings.allow = !!queried_settings.allow && !settings.payload; settings.pattern = pattern; } return settings; } let storage; let policy_observable = {}; function sanitize_web_page(details) { const url = details.url; if (is_privileged_url(details.url)) return; const policy = decide_policy_for_url(storage, policy_observable, details.url); let headers = details.responseHeaders; headers = inject_csp_headers(headers, policy); let skip = false; for (const header of headers) { if ((header.name.toLowerCase().trim() === "content-disposition" && /^\s*attachment\s*(;.*)$/i.test(header.value))) skip = true; } skip = skip || (details.statusCode >= 300 && details.statusCode < 400); if (!skip) { /* Check for API availability. */ if (browser.webRequest.filterResponseData) headers = apply_stream_filter(details, headers, policy); } return {responseHeaders: headers}; } const request_url_regex = /^[^?]*\?url=(.*)$/; const redirect_url_template = browser.runtime.getURL("dummy") + "?settings="; function synchronously_smuggle_policy(details) { /* * Content script will make a synchronous XmlHttpRequest to extension's * `dummy` file to query settings for given URL. We smuggle that * information in query parameter of the URL we redirect to. * A risk of fingerprinting arises if a page with script execution allowed * guesses the dummy file URL and makes an AJAX call to it. It is currently * a problem in ManifestV2 Chromium-family port of Haketilo because Chromium * uses predictable URLs for web-accessible resources. We plan to fix it in * the future ManifestV3 port. */ if (details.type !== "xmlhttprequest") return {cancel: true}; console.debug(`Settings queried using XHR for '${details.url}'.`); let policy = {allow: false}; try { /* * request_url should be of the following format: * <url_for_extension's_dummy_file>?url=<valid_urlencoded_url> */ const match = request_url_regex.exec(details.url); const queried_url = decodeURIComponent(match[1]); if (details.initiator && !queried_url.startsWith(details.initiator)) { console.warn(`Blocked suspicious query of '${url}' by '${details.initiator}'. This might be the result of page fingerprinting the browser.`); return {cancel: true}; } policy = decide_policy_for_url(storage, policy_observable, queried_url); } catch (e) { console.warn(`Bad request! Expected ${browser.runtime.getURL("dummy")}?url=<valid_urlencoded_url>. Got ${request_url}. This might be the result of page fingerprinting the browser.`); } const encoded_policy = encodeURIComponent(JSON.stringify(policy)); return {redirectUrl: redirect_url_template + encoded_policy}; } const all_types = [ "main_frame", "sub_frame", "stylesheet", "script", "image", "font", "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket", "other", "main_frame", "sub_frame" ]; async function start_webRequest_operations() { storage = await get_storage(); const extra_opts = ["blocking"]; if (is_chrome) extra_opts.push("extraHeaders"); browser.webRequest.onHeadersReceived.addListener( sanitize_web_page, {urls: ["<all_urls>"], types: ["main_frame", "sub_frame"]}, extra_opts.concat("responseHeaders") ); const dummy_url_pattern = browser.runtime.getURL("dummy") + "?url=*"; browser.webRequest.onBeforeRequest.addListener( synchronously_smuggle_policy, {urls: [dummy_url_pattern], types: ["xmlhttprequest"]}, extra_opts ); policy_observable = await light_storage.observe_var("default_allow"); } start_webRequest_operations(); const code = `\ console.warn("Hi, I'm Mr Dynamic!"); console.debug("let's see how window.killtheweb looks like now"); console.log("killtheweb", window.killtheweb); ` async function test_dynamic_content_scripts() { browser.contentScripts.register({ "js": [{code}], "matches": ["<all_urls>"], "allFrames": true, "runAt": "document_start" }); } if (is_mozilla) test_dynamic_content_scripts();