Revision 96068ada
Added by koszko almost 2 years ago
| background/cookie_filter.js | ||
|---|---|---|
| 1 | 
    /**  | 
|
| 2 | 
    * This file is part of Haketilo.  | 
|
| 3 | 
    *  | 
|
| 4 | 
    * Function: Filtering request headers to remove haketilo cookies that might  | 
|
| 5 | 
    * have slipped through.  | 
|
| 6 | 
    *  | 
|
| 7 | 
    * Copyright (C) 2021 Wojtek Kosior  | 
|
| 8 | 
    * Redistribution terms are gathered in the `copyright' file.  | 
|
| 9 | 
    */  | 
|
| 10 | 
     | 
|
| 11 | 
    /*  | 
|
| 12 | 
    * IMPORTS_START  | 
|
| 13 | 
    * IMPORT extract_signed  | 
|
| 14 | 
    * IMPORTS_END  | 
|
| 15 | 
    */  | 
|
| 16 | 
     | 
|
| 17 | 
    function is_valid_haketilo_cookie(cookie)  | 
|
| 18 | 
    {
   | 
|
| 19 | 
    const match = /^haketilo-(\w*)=(.*)$/.exec(cookie);  | 
|
| 20 | 
    if (!match)  | 
|
| 21 | 
    return false;  | 
|
| 22 | 
     | 
|
| 23 | 
    return !extract_signed(match.slice(1, 3)).fail;  | 
|
| 24 | 
    }  | 
|
| 25 | 
     | 
|
| 26 | 
    function remove_haketilo_cookies(header)  | 
|
| 27 | 
    {
   | 
|
| 28 | 
    if (header.name !== "Cookie")  | 
|
| 29 | 
    return header;  | 
|
| 30 | 
     | 
|
| 31 | 
        const cookies = header.value.split("; ");
   | 
|
| 32 | 
        const value = cookies.filter(c => !is_valid_haketilo_cookie(c)).join("; ");
   | 
|
| 33 | 
     | 
|
| 34 | 
        return value ? {name: "Cookie", value} : null;
   | 
|
| 35 | 
    }  | 
|
| 36 | 
     | 
|
| 37 | 
    function filter_cookie_headers(headers)  | 
|
| 38 | 
    {
   | 
|
| 39 | 
    return headers.map(remove_haketilo_cookies).filter(h => h);  | 
|
| 40 | 
    }  | 
|
| 41 | 
     | 
|
| 42 | 
    /*  | 
|
| 43 | 
    * EXPORTS_START  | 
|
| 44 | 
    * EXPORT filter_cookie_headers  | 
|
| 45 | 
    * EXPORTS_END  | 
|
| 46 | 
    */  | 
|
| background/main.js | ||
|---|---|---|
| 17 | 17 | 
    * IMPORT browser  | 
| 18 | 18 | 
    * IMPORT is_privileged_url  | 
| 19 | 19 | 
    * IMPORT query_best  | 
| 20 | 
    * IMPORT gen_nonce  | 
|
| 21 | 20 | 
    * IMPORT inject_csp_headers  | 
| 22 | 21 | 
    * IMPORT apply_stream_filter  | 
| 23 | 
    * IMPORT filter_cookie_headers  | 
|
| 24 | 22 | 
    * IMPORT is_chrome  | 
| 23 | 
    * IMPORT is_mozilla  | 
|
| 25 | 24 | 
    * IMPORTS_END  | 
| 26 | 25 | 
    */  | 
| 27 | 26 | 
     | 
| ... | ... | |
| 51 | 50 | 
     | 
| 52 | 51 | 
    browser.runtime.onInstalled.addListener(init_ext);  | 
| 53 | 52 | 
     | 
| 53 | 
    /*  | 
|
| 54 | 
    * The function below implements a more practical interface for what it does by  | 
|
| 55 | 
    * wrapping the old query_best() function.  | 
|
| 56 | 
    */  | 
|
| 57 | 
    function decide_policy_for_url(storage, policy_observable, url)  | 
|
| 58 | 
    {
   | 
|
| 59 | 
    if (storage === undefined)  | 
|
| 60 | 
    	return {allow: false};
   | 
|
| 61 | 
     | 
|
| 62 | 
    const settings =  | 
|
| 63 | 
    	{allow: policy_observable !== undefined && policy_observable.value};
   | 
|
| 64 | 
     | 
|
| 65 | 
    const [pattern, queried_settings] = query_best(storage, url);  | 
|
| 66 | 
     | 
|
| 67 | 
        if (queried_settings) {
   | 
|
| 68 | 
    settings.payload = queried_settings.components;  | 
|
| 69 | 
    settings.allow = !!queried_settings.allow && !settings.payload;  | 
|
| 70 | 
    settings.pattern = pattern;  | 
|
| 71 | 
    }  | 
|
| 72 | 
     | 
|
| 73 | 
    return settings;  | 
|
| 74 | 
    }  | 
|
| 54 | 75 | 
     | 
| 55 | 76 | 
    let storage;  | 
| 56 | 77 | 
    let policy_observable = {};
   | 
| 57 | 78 | 
     | 
| 58 | 
    function on_headers_received(details)
   | 
|
| 79 | 
    function sanitize_web_page(details)
   | 
|
| 59 | 80 | 
    {
   | 
| 60 | 81 | 
    const url = details.url;  | 
| 61 | 82 | 
    if (is_privileged_url(details.url))  | 
| 62 | 83 | 
    return;  | 
| 63 | 84 | 
     | 
| 64 | 
    const [pattern, settings] = query_best(storage, details.url);  | 
|
| 65 | 
    const has_payload = !!(settings && settings.components);  | 
|
| 66 | 
    const allow = !has_payload &&  | 
|
| 67 | 
    !!(settings ? settings.allow : policy_observable.value);  | 
|
| 68 | 
    const nonce = gen_nonce();  | 
|
| 69 | 
        const policy = {allow, url, nonce, has_payload};
   | 
|
| 85 | 
    const policy =  | 
|
| 86 | 
    decide_policy_for_url(storage, policy_observable, details.url);  | 
|
| 70 | 87 | 
     | 
| 71 | 88 | 
    let headers = details.responseHeaders;  | 
| 89 | 
     | 
|
| 90 | 
    headers = inject_csp_headers(headers, policy);  | 
|
| 91 | 
     | 
|
| 72 | 92 | 
    let skip = false;  | 
| 73 | 93 | 
        for (const header of headers) {
   | 
| 74 | 94 | 
    if ((header.name.toLowerCase().trim() === "content-disposition" &&  | 
| 75 | 95 | 
    /^\s*attachment\s*(;.*)$/i.test(header.value)))  | 
| 76 | 96 | 
    skip = true;  | 
| 77 | 97 | 
    }  | 
| 78 | 
     | 
|
| 79 | 
    headers = inject_csp_headers(headers, policy);  | 
|
| 80 | 
     | 
|
| 81 | 98 | 
    skip = skip || (details.statusCode >= 300 && details.statusCode < 400);  | 
| 99 | 
     | 
|
| 82 | 100 | 
        if (!skip) {
   | 
| 83 | 101 | 
    /* Check for API availability. */  | 
| 84 | 102 | 
    if (browser.webRequest.filterResponseData)  | 
| ... | ... | |
| 88 | 106 | 
        return {responseHeaders: headers};
   | 
| 89 | 107 | 
    }  | 
| 90 | 108 | 
     | 
| 91 | 
    function on_before_send_headers(details)  | 
|
| 109 | 
    const request_url_regex = /^[^?]*\?url=(.*)$/;  | 
|
| 110 | 
    const redirect_url_template = browser.runtime.getURL("dummy") + "?settings=";
   | 
|
| 111 | 
     | 
|
| 112 | 
    function synchronously_smuggle_policy(details)  | 
|
| 92 | 113 | 
    {
   | 
| 93 | 
    let headers = details.requestHeaders;  | 
|
| 94 | 
    headers = filter_cookie_headers(headers);  | 
|
| 95 | 
        return {requestHeaders: headers};
   | 
|
| 114 | 
    /*  | 
|
| 115 | 
    * Content script will make a synchronous XmlHttpRequest to extension's  | 
|
| 116 | 
    * `dummy` file to query settings for given URL. We smuggle that  | 
|
| 117 | 
    * information in query parameter of the URL we redirect to.  | 
|
| 118 | 
    * A risk of fingerprinting arises if a page with script execution allowed  | 
|
| 119 | 
    * guesses the dummy file URL and makes an AJAX call to it. It is currently  | 
|
| 120 | 
    * a problem in ManifestV2 Chromium-family port of Haketilo because Chromium  | 
|
| 121 | 
    * uses predictable URLs for web-accessible resources. We plan to fix it in  | 
|
| 122 | 
    * the future ManifestV3 port.  | 
|
| 123 | 
    */  | 
|
| 124 | 
    if (details.type !== "xmlhttprequest")  | 
|
| 125 | 
    	return {cancel: true};
   | 
|
| 126 | 
     | 
|
| 127 | 
        console.debug(`Settings queried using XHR for '${details.url}'.`);
   | 
|
| 128 | 
     | 
|
| 129 | 
        let policy = {allow: false};
   | 
|
| 130 | 
     | 
|
| 131 | 
        try {
   | 
|
| 132 | 
    /*  | 
|
| 133 | 
    * request_url should be of the following format:  | 
|
| 134 | 
    * <url_for_extension's_dummy_file>?url=<valid_urlencoded_url>  | 
|
| 135 | 
    */  | 
|
| 136 | 
    const match = request_url_regex.exec(details.url);  | 
|
| 137 | 
    const queried_url = decodeURIComponent(match[1]);  | 
|
| 138 | 
     | 
|
| 139 | 
    	if (details.initiator && !queried_url.startsWith(details.initiator)) {
   | 
|
| 140 | 
    	    console.warn(`Blocked suspicious query of '${url}' by '${details.initiator}'. This might be the result of page fingerprinting the browser.`);
   | 
|
| 141 | 
    	    return {cancel: true};
   | 
|
| 142 | 
    }  | 
|
| 143 | 
     | 
|
| 144 | 
    policy = decide_policy_for_url(storage, policy_observable, queried_url);  | 
|
| 145 | 
        } catch (e) {
   | 
|
| 146 | 
    	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.`);
   | 
|
| 147 | 
    }  | 
|
| 148 | 
     | 
|
| 149 | 
    const encoded_policy = encodeURIComponent(JSON.stringify(policy));  | 
|
| 150 | 
     | 
|
| 151 | 
        return {redirectUrl: redirect_url_template + encoded_policy};
   | 
|
| 96 | 152 | 
    }  | 
| 97 | 153 | 
     | 
| 98 | 154 | 
    const all_types = [  | 
| ... | ... | |
| 110 | 166 | 
    	extra_opts.push("extraHeaders");
   | 
| 111 | 167 | 
     | 
| 112 | 168 | 
    browser.webRequest.onHeadersReceived.addListener(  | 
| 113 | 
    	on_headers_received,
   | 
|
| 169 | 
    	sanitize_web_page,
   | 
|
| 114 | 170 | 
    	{urls: ["<all_urls>"], types: ["main_frame", "sub_frame"]},
   | 
| 115 | 171 | 
    	extra_opts.concat("responseHeaders")
   | 
| 116 | 172 | 
    );  | 
| 117 | 173 | 
     | 
| 118 | 
    browser.webRequest.onBeforeSendHeaders.addListener(  | 
|
| 119 | 
    on_before_send_headers,  | 
|
| 120 | 
    	{urls: ["<all_urls>"], types: all_types},
   | 
|
| 121 | 
    	extra_opts.concat("requestHeaders")
   | 
|
| 174 | 
        const dummy_url_pattern = browser.runtime.getURL("dummy") + "?url=*";
   | 
|
| 175 | 
    browser.webRequest.onBeforeRequest.addListener(  | 
|
| 176 | 
    synchronously_smuggle_policy,  | 
|
| 177 | 
    	{urls: [dummy_url_pattern], types: ["xmlhttprequest"]},
   | 
|
| 178 | 
    extra_opts  | 
|
| 122 | 179 | 
    );  | 
| 123 | 180 | 
     | 
| 124 | 181 | 
        policy_observable = await light_storage.observe_var("default_allow");
   | 
| 125 | 182 | 
    }  | 
| 126 | 183 | 
     | 
| 127 | 184 | 
    start_webRequest_operations();  | 
| 185 | 
     | 
|
| 186 | 
    const code = `\  | 
|
| 187 | 
    console.warn("Hi, I'm Mr Dynamic!");
   | 
|
| 188 | 
     | 
|
| 189 | 
    console.debug("let's see how window.killtheweb looks like now");
   | 
|
| 190 | 
     | 
|
| 191 | 
    console.log("killtheweb", window.killtheweb);
   | 
|
| 192 | 
    `  | 
|
| 193 | 
     | 
|
| 194 | 
    async function test_dynamic_content_scripts()  | 
|
| 195 | 
    {
   | 
|
| 196 | 
        browser.contentScripts.register({
   | 
|
| 197 | 
    	"js": [{code}],
   | 
|
| 198 | 
    "matches": ["<all_urls>"],  | 
|
| 199 | 
    "allFrames": true,  | 
|
| 200 | 
    "runAt": "document_start"  | 
|
| 201 | 
    });  | 
|
| 202 | 
    }  | 
|
| 203 | 
     | 
|
| 204 | 
    if (is_mozilla)  | 
|
| 205 | 
    test_dynamic_content_scripts();  | 
|
| background/page_actions_server.js | ||
|---|---|---|
| 16 | 16 | 
    * IMPORT browser  | 
| 17 | 17 | 
    * IMPORT listen_for_connection  | 
| 18 | 18 | 
    * IMPORT sha256  | 
| 19 | 
    * IMPORT query_best  | 
|
| 20 | 19 | 
    * IMPORT make_ajax_request  | 
| 21 | 20 | 
    * IMPORTS_END  | 
| 22 | 21 | 
    */  | 
| 23 | 22 | 
     | 
| 24 | 23 | 
    var storage;  | 
| 25 | 24 | 
    var handler;  | 
| 26 | 
    let policy_observable;  | 
|
| 27 | 
     | 
|
| 28 | 
    function send_actions(url, port)  | 
|
| 29 | 
    {
   | 
|
| 30 | 
    const [pattern, queried_settings] = query_best(storage, url);  | 
|
| 31 | 
     | 
|
| 32 | 
        const settings = {allow: policy_observable && policy_observable.value};
   | 
|
| 33 | 
    Object.assign(settings, queried_settings);  | 
|
| 34 | 
    if (settings.components)  | 
|
| 35 | 
    settings.allow = false;  | 
|
| 36 | 
     | 
|
| 37 | 
    const repos = storage.get_all(TYPE_PREFIX.REPO);  | 
|
| 38 | 
     | 
|
| 39 | 
    port.postMessage(["settings", [pattern, settings, repos]]);  | 
|
| 40 | 
     | 
|
| 41 | 
    const components = settings.components;  | 
|
| 42 | 
    const processed_bags = new Set();  | 
|
| 43 | 
     | 
|
| 44 | 
    if (components !== undefined)  | 
|
| 45 | 
    send_scripts([components], port, processed_bags);  | 
|
| 46 | 
    }  | 
|
| 47 | 25 | 
     | 
| 48 | 26 | 
    // TODO: parallelize script fetching  | 
| 49 | 27 | 
    async function send_scripts(components, port, processed_bags)  | 
| ... | ... | |
| 116 | 94 | 
    function handle_message(port, message, handler)  | 
| 117 | 95 | 
    {
   | 
| 118 | 96 | 
    port.onMessage.removeListener(handler[0]);  | 
| 119 | 
    let url = message.url;  | 
|
| 120 | 
        console.log({url});
   | 
|
| 121 | 
    send_actions(url, port);  | 
|
| 97 | 
        console.debug(`Loading payload '${message.payload}'.`);
   | 
|
| 98 | 
     | 
|
| 99 | 
    const processed_bags = new Set();  | 
|
| 100 | 
     | 
|
| 101 | 
    send_scripts([message.payload], port, processed_bags);  | 
|
| 122 | 102 | 
    }  | 
| 123 | 103 | 
     | 
| 124 | 104 | 
    function new_connection(port)  | 
| ... | ... | |
| 134 | 114 | 
    storage = await get_storage();  | 
| 135 | 115 | 
     | 
| 136 | 116 | 
    listen_for_connection(CONNECTION_TYPE.PAGE_ACTIONS, new_connection);  | 
| 137 | 
     | 
|
| 138 | 
        policy_observable = await light_storage.observe_var("default_allow");
   | 
|
| 139 | 117 | 
    }  | 
| 140 | 118 | 
     | 
| 141 | 119 | 
    /*  | 
| background/policy_injector.js | ||
|---|---|---|
| 10 | 10 | 
     | 
| 11 | 11 | 
    /*  | 
| 12 | 12 | 
    * IMPORTS_START  | 
| 13 | 
    * IMPORT sign_data  | 
|
| 14 | 
    * IMPORT extract_signed  | 
|
| 15 | 13 | 
    * IMPORT make_csp_rule  | 
| 16 | 14 | 
    * IMPORT csp_header_regex  | 
| 15 | 
    * Re-enable the import below once nonce stuff here is ready  | 
|
| 16 | 
    * !mport gen_nonce  | 
|
| 17 | 17 | 
    * IMPORTS_END  | 
| 18 | 18 | 
    */  | 
| 19 | 19 | 
     | 
| 20 | 20 | 
    function inject_csp_headers(headers, policy)  | 
| 21 | 21 | 
    {
   | 
| 22 | 22 | 
    let csp_headers;  | 
| 23 | 
    let old_signature;  | 
|
| 24 | 
    let haketilo_header;  | 
|
| 25 | 23 | 
     | 
| 26 | 
        for (const header of headers.filter(h => h.name === "x-haketilo")) {
   | 
|
| 27 | 
    /* x-haketilo header has format: <signature>_0_<data> */  | 
|
| 28 | 
    const match = /^([^_]+)_(0_.*)$/.exec(header.value);  | 
|
| 29 | 
    if (!match)  | 
|
| 30 | 
    continue;  | 
|
| 24 | 
        if (policy.payload) {
   | 
|
| 25 | 
    headers = headers.filter(h => !csp_header_regex.test(h.name));  | 
|
| 31 | 26 | 
     | 
| 32 | 
    	const result = extract_signed(...match.slice(1, 3));
   | 
|
| 33 | 
    	if (result.fail)
   | 
|
| 34 | 
    	    continue;
   | 
|
| 27 | 
    	// TODO: make CSP rules with nonces and facilitate passing them to
   | 
|
| 28 | 
    	// content scripts via dynamic content script registration or
   | 
|
| 29 | 
    	// synchronous XHRs
   | 
|
| 35 | 30 | 
     | 
| 36 | 
    /* This should succeed - it's our self-produced valid JSON. */  | 
|
| 37 | 
    const old_data = JSON.parse(decodeURIComponent(result.data));  | 
|
| 38 | 
     | 
|
| 39 | 
    /* Confirmed- it's the originals, smuggled in! */  | 
|
| 40 | 
    csp_headers = old_data.csp_headers;  | 
|
| 41 | 
    old_signature = old_data.policy_sig;  | 
|
| 42 | 
     | 
|
| 43 | 
    haketilo_header = header;  | 
|
| 44 | 
    break;  | 
|
| 31 | 
    // policy.nonce = gen_nonce();  | 
|
| 45 | 32 | 
    }  | 
| 46 | 33 | 
     | 
| 47 | 
        if (policy.has_payload) {
   | 
|
| 48 | 
    csp_headers = [];  | 
|
| 49 | 
    const non_csp_headers = [];  | 
|
| 50 | 
    const header_list =  | 
|
| 51 | 
    h => csp_header_regex.test(h) ? csp_headers : non_csp_headers;  | 
|
| 52 | 
    headers.forEach(h => header_list(h.name).push(h));  | 
|
| 53 | 
    headers = non_csp_headers;  | 
|
| 54 | 
        } else {
   | 
|
| 55 | 
    headers.push(...csp_headers || []);  | 
|
| 56 | 
    }  | 
|
| 57 | 
     | 
|
| 58 | 
        if (!haketilo_header) {
   | 
|
| 59 | 
    	haketilo_header = {name: "x-haketilo"};
   | 
|
| 60 | 
    headers.push(haketilo_header);  | 
|
| 61 | 
    }  | 
|
| 62 | 
     | 
|
| 63 | 
    if (old_signature)  | 
|
| 64 | 
    headers = headers.filter(h => h.value.search(old_signature) === -1);  | 
|
| 65 | 
     | 
|
| 66 | 
    const policy_str = encodeURIComponent(JSON.stringify(policy));  | 
|
| 67 | 
    const signed_policy = sign_data(policy_str, new Date().getTime());  | 
|
| 68 | 
    const later_30sec = new Date(new Date().getTime() + 30000).toGMTString();  | 
|
| 69 | 
        headers.push({
   | 
|
| 70 | 
    name: "Set-Cookie",  | 
|
| 71 | 
    	value: `haketilo-${signed_policy.join("=")}; Expires=${later_30sec};`
   | 
|
| 72 | 
    });  | 
|
| 73 | 
     | 
|
| 74 | 
    /*  | 
|
| 75 | 
    * Smuggle in the signature and the original CSP headers for future use.  | 
|
| 76 | 
    * These are signed with a time of 0, as it's not clear there is a limit on  | 
|
| 77 | 
    * how long Firefox might retain headers in the cache.  | 
|
| 78 | 
    */  | 
|
| 79 | 
        let haketilo_data = {csp_headers, policy_sig: signed_policy[0]};
   | 
|
| 80 | 
    haketilo_data = encodeURIComponent(JSON.stringify(haketilo_data));  | 
|
| 81 | 
        haketilo_header.value = sign_data(haketilo_data, 0).join("_");
   | 
|
| 82 | 
     | 
|
| 83 | 
        if (!policy.allow) {
   | 
|
| 34 | 
        if (!policy.allow && (policy.nonce || !policy.payload)) {
   | 
|
| 84 | 35 | 
    	headers.push({
   | 
| 85 | 36 | 
    name: "content-security-policy",  | 
| 86 | 37 | 
    value: make_csp_rule(policy)  | 
| background/stream_filter.js | ||
|---|---|---|
| 174 | 174 | 
    * as harmless anyway).  | 
| 175 | 175 | 
    */  | 
| 176 | 176 | 
     | 
| 177 | 
    const dummy_script =  | 
|
| 178 | 
    	      `<script data-haketilo-deleteme="${properties.policy.nonce}" nonce="${properties.policy.nonce}">null</script>`;
   | 
|
| 177 | 
    const dummy_script = `<script>null</script>`;  | 
|
| 179 | 178 | 
    const doctype_decl = /^(\s*<!doctype[^<>"']*>)?/i.exec(decoded)[0];  | 
| 180 | 179 | 
    decoded = doctype_decl + dummy_script +  | 
| 181 | 180 | 
    decoded.substring(doctype_decl.length);  | 
| ... | ... | |
| 189 | 188 | 
     | 
| 190 | 189 | 
    function apply_stream_filter(details, headers, policy)  | 
| 191 | 190 | 
    {
   | 
| 192 | 
        if (!policy.has_payload)
   | 
|
| 191 | 
    if (!policy.payload)  | 
|
| 193 | 192 | 
    return headers;  | 
| 194 | 193 | 
     | 
| 195 | 194 | 
    const properties = properties_from_headers(headers);  | 
| 196 | 
    properties.policy = policy;  | 
|
| 197 | 195 | 
     | 
| 198 | 196 | 
    properties.filter =  | 
| 199 | 197 | 
    browser.webRequest.filterResponseData(details.requestId);  | 
| build.sh | ||
|---|---|---|
| 180 | 180 | 
    mkdir -p "$BUILDDIR"/$DIR  | 
| 181 | 181 | 
    done  | 
| 182 | 182 | 
     | 
| 183 | 
    CHROMIUM_KEY=''  | 
|
| 184 | 183 | 
    CHROMIUM_UPDATE_URL=''  | 
| 185 | 184 | 
    GECKO_APPLICATIONS=''  | 
| 186 | 185 | 
     | 
| ... | ... | |
| 189 | 188 | 
    fi  | 
| 190 | 189 | 
     | 
| 191 | 190 | 
    if [ "$BROWSER" = "chromium" ]; then  | 
| 192 | 
    CHROMIUM_KEY="$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64)"  | 
|
| 193 | 
    CHROMIUM_KEY=$(echo chromium-key-dummy-file-$CHROMIUM_KEY | tr / -)  | 
|
| 194 | 
    touch "$BUILDDIR"/$CHROMIUM_KEY  | 
|
| 195 | 
     | 
|
| 196 | 191 | 
    CHROMIUM_UPDATE_URL="$UPDATE_URL"  | 
| 197 | 
     | 
|
| 198 | 
    CHROMIUM_KEY="\n\  | 
|
| 199 | 
    // WARNING!!!\n\  | 
|
| 200 | 
    // EACH USER SHOULD REPLACE DUMMY FILE's VALUE WITH A UNIQUE ONE!!!\n\  | 
|
| 201 | 
    // OTHERWISE, SECURITY CAN BE TRIVIALLY COMPROMISED!\n\  | 
|
| 202 | 
    // Only relevant to users of chrome-based browsers.\n\  | 
|
| 203 | 
    // Users of Firefox forks are safe.\n\  | 
|
| 204 | 
    \"$CHROMIUM_KEY\"\  | 
|
| 205 | 
    "  | 
|
| 206 | 192 | 
    else  | 
| 207 | 193 | 
    GECKO_APPLICATIONS="\n\  | 
| 208 | 194 | 
        \"applications\": {\n\
   | 
| ... | ... | |
| 215 | 201 | 
     | 
| 216 | 202 | 
    sed "\  | 
| 217 | 203 | 
    s^_GECKO_APPLICATIONS_^$GECKO_APPLICATIONS^  | 
| 218 | 
    s^_CHROMIUM_KEY_^$CHROMIUM_KEY^  | 
|
| 219 | 204 | 
    s^_CHROMIUM_UPDATE_URL_^$CHROMIUM_UPDATE_URL^  | 
| 220 | 205 | 
    s^_BGSCRIPTS_^$BGSCRIPTS^  | 
| 221 | 206 | 
    s^_CONTENTSCRIPTS_^$CONTENTSCRIPTS^" \  | 
| ... | ... | |
| 279 | 264 | 
    fi  | 
| 280 | 265 | 
     | 
| 281 | 266 | 
    cp -r copyright licenses/ "$BUILDDIR"  | 
| 267 | 
    cp dummy "$BUILDDIR"  | 
|
| 282 | 268 | 
    cp html/*.css "$BUILDDIR"/html  | 
| 283 | 269 | 
    mkdir "$BUILDDIR"/icons  | 
| 284 | 270 | 
    cp icons/*.png "$BUILDDIR"/icons  | 
| common/misc.js | ||
|---|---|---|
| 49 | 49 | 
    function make_csp_rule(policy)  | 
| 50 | 50 | 
    {
   | 
| 51 | 51 | 
    let rule = "prefetch-src 'none'; script-src-attr 'none';";  | 
| 52 | 
        const script_src = policy.has_payload ?
   | 
|
| 52 | 
        const script_src = policy.nonce !== undefined ?
   | 
|
| 53 | 53 | 
    	  `'nonce-${policy.nonce}'` : "'none'";
   | 
| 54 | 54 | 
        rule += ` script-src ${script_src}; script-src-elem ${script_src};`;
   | 
| 55 | 55 | 
    return rule;  | 
| common/signing.js | ||
|---|---|---|
| 1 | 
    /**  | 
|
| 2 | 
    * This file is part of Haketilo.  | 
|
| 3 | 
    *  | 
|
| 4 | 
    * Functions: Operations related to "signing" of data.  | 
|
| 5 | 
    *  | 
|
| 6 | 
    * Copyright (C) 2021 Wojtek Kosior  | 
|
| 7 | 
    * Redistribution terms are gathered in the `copyright' file.  | 
|
| 8 | 
    */  | 
|
| 9 | 
     | 
|
| 10 | 
    /*  | 
|
| 11 | 
    * IMPORTS_START  | 
|
| 12 | 
    * IMPORT sha256  | 
|
| 13 | 
    * IMPORT browser  | 
|
| 14 | 
    * IMPORT is_mozilla  | 
|
| 15 | 
    * IMPORTS_END  | 
|
| 16 | 
    */  | 
|
| 17 | 
     | 
|
| 18 | 
    /*  | 
|
| 19 | 
    * In order to make certain data synchronously accessible in certain contexts,  | 
|
| 20 | 
    * Haketilo smuggles it in string form in places like cookies, URLs and headers.  | 
|
| 21 | 
    * When using the smuggled data, we first need to make sure it isn't spoofed.  | 
|
| 22 | 
    * For that, we use this pseudo-signing mechanism.  | 
|
| 23 | 
    *  | 
|
| 24 | 
    * Despite what name suggests, no assymetric cryptography is involved, as it  | 
|
| 25 | 
    * would bring no additional benefits and would incur bigger performance  | 
|
| 26 | 
    * overhead. Instead, we hash the string data together with some secret value  | 
|
| 27 | 
    * that is supposed to be known only by this browser instance. Resulting hash  | 
|
| 28 | 
    * sum plays the role of the signature. In the hash we also include current  | 
|
| 29 | 
    * time. This way, even if signed data leaks (which shouldn't happen in the  | 
|
| 30 | 
    * first place), an attacker won't be able to re-use it indefinitely.  | 
|
| 31 | 
    *  | 
|
| 32 | 
    * The secret shared between execution contexts has to be available  | 
|
| 33 | 
    * synchronously. Under Mozilla, this is the extension's per-session id. Under  | 
|
| 34 | 
    * Chromium, this is a dummy web-accessible-resource name that resides in the  | 
|
| 35 | 
    * manifest and is supposed to be constructed by each user using a unique value  | 
|
| 36 | 
    * (this is done automatically by `build.sh').  | 
|
| 37 | 
    */  | 
|
| 38 | 
     | 
|
| 39 | 
    function get_secret()  | 
|
| 40 | 
    {
   | 
|
| 41 | 
    if (is_mozilla)  | 
|
| 42 | 
    	return browser.runtime.getURL("dummy");
   | 
|
| 43 | 
     | 
|
| 44 | 
    return chrome.runtime.getManifest().web_accessible_resources  | 
|
| 45 | 
    .map(r => /^chromium-key-dummy-file-(.*)/.exec(r)).filter(r => r)[0][1];  | 
|
| 46 | 
    }  | 
|
| 47 | 
     | 
|
| 48 | 
    function extract_signed(signature, signed_data)  | 
|
| 49 | 
    {
   | 
|
| 50 | 
        const match = /^([1-9][0-9]{12}|0)_(.*)$/.exec(signed_data);
   | 
|
| 51 | 
    if (!match)  | 
|
| 52 | 
    	return {fail: "bad format"};
   | 
|
| 53 | 
     | 
|
| 54 | 
        const result = {time: parseInt(match[1]), data: match[2]};
   | 
|
| 55 | 
    if (sign_data(result.data, result.time)[0] !== signature)  | 
|
| 56 | 
    result.fail = "bad signature";  | 
|
| 57 | 
     | 
|
| 58 | 
    return result;  | 
|
| 59 | 
    }  | 
|
| 60 | 
     | 
|
| 61 | 
    /*  | 
|
| 62 | 
    * Sign a given string for a given time. Time should be either 0 or in the range  | 
|
| 63 | 
    * 10^12 <= time < 10^13.  | 
|
| 64 | 
    */  | 
|
| 65 | 
    function sign_data(data, time) {
   | 
|
| 66 | 
        return [sha256(get_secret() + time + data), `${time}_${data}`];
   | 
|
| 67 | 
    }  | 
|
| 68 | 
     | 
|
| 69 | 
    /*  | 
|
| 70 | 
    * EXPORTS_START  | 
|
| 71 | 
    * EXPORT extract_signed  | 
|
| 72 | 
    * EXPORT sign_data  | 
|
| 73 | 
    * EXPORTS_END  | 
|
| 74 | 
    */  | 
|
| content/activity_info_server.js | ||
|---|---|---|
| 42 | 42 | 
     | 
| 43 | 43 | 
    function report_settings(settings)  | 
| 44 | 44 | 
    {
   | 
| 45 | 
        report_activity("settings", settings);
   | 
|
| 45 | 
        const settings_clone = {};
   | 
|
| 46 | 
    Object.assign(settings_clone, settings)  | 
|
| 47 | 
        report_activity("settings", settings_clone);
   | 
|
| 46 | 48 | 
    }  | 
| 47 | 49 | 
     | 
| 48 | 50 | 
    function report_document_type(is_html)  | 
| content/main.js | ||
|---|---|---|
| 11 | 11 | 
    /*  | 
| 12 | 12 | 
    * IMPORTS_START  | 
| 13 | 13 | 
    * IMPORT handle_page_actions  | 
| 14 | 
    * IMPORT extract_signed  | 
|
| 15 | 
    * IMPORT sign_data  | 
|
| 16 | 14 | 
    * IMPORT gen_nonce  | 
| 17 | 15 | 
    * IMPORT is_privileged_url  | 
| 16 | 
    * IMPORT browser  | 
|
| 18 | 17 | 
    * IMPORT is_chrome  | 
| 19 | 18 | 
    * IMPORT is_mozilla  | 
| 20 | 19 | 
    * IMPORT start_activity_info_server  | 
| 21 | 20 | 
    * IMPORT make_csp_rule  | 
| 22 | 21 | 
    * IMPORT csp_header_regex  | 
| 22 | 
    * IMPORT report_settings  | 
|
| 23 | 23 | 
    * IMPORTS_END  | 
| 24 | 24 | 
    */  | 
| 25 | 25 | 
     | 
| ... | ... | |
| 29 | 29 | 
     | 
| 30 | 30 | 
    wait_loaded(document).then(() => document.content_loaded = true);  | 
| 31 | 31 | 
     | 
| 32 | 
    function extract_cookie_policy(cookie, min_time)  | 
|
| 33 | 
    {
   | 
|
| 34 | 
        let best_result = {time: -1};
   | 
|
| 35 | 
    let policy = null;  | 
|
| 36 | 
    const extracted_signatures = [];  | 
|
| 37 | 
     | 
|
| 38 | 
        for (const match of cookie.matchAll(/haketilo-(\w*)=([^;]*)/g)) {
   | 
|
| 39 | 
    const new_result = extract_signed(...match.slice(1, 3));  | 
|
| 40 | 
    if (new_result.fail)  | 
|
| 41 | 
    continue;  | 
|
| 42 | 
     | 
|
| 43 | 
    extracted_signatures.push(match[1]);  | 
|
| 44 | 
     | 
|
| 45 | 
    if (new_result.time < Math.max(min_time, best_result.time))  | 
|
| 46 | 
    continue;  | 
|
| 47 | 
     | 
|
| 48 | 
    /* This should succeed - it's our self-produced valid JSON. */  | 
|
| 49 | 
    const new_policy = JSON.parse(decodeURIComponent(new_result.data));  | 
|
| 50 | 
    if (new_policy.url !== document.URL)  | 
|
| 51 | 
    continue;  | 
|
| 52 | 
     | 
|
| 53 | 
    best_result = new_result;  | 
|
| 54 | 
    policy = new_policy;  | 
|
| 55 | 
    }  | 
|
| 56 | 
     | 
|
| 57 | 
    return [policy, extracted_signatures];  | 
|
| 58 | 
    }  | 
|
| 59 | 
     | 
|
| 60 | 
    function extract_url_policy(url, min_time)  | 
|
| 61 | 
    {
   | 
|
| 62 | 
    const [base_url, payload, anchor] =  | 
|
| 63 | 
    /^([^#]*)#?([^#]*)(#?.*)$/.exec(url).splice(1, 4);  | 
|
| 64 | 
     | 
|
| 65 | 
    const match = /^haketilo_([^_]+)_(.*)$/.exec(payload);  | 
|
| 66 | 
    if (!match)  | 
|
| 67 | 
    return [null, url];  | 
|
| 68 | 
     | 
|
| 69 | 
    const result = extract_signed(...match.slice(1, 3));  | 
|
| 70 | 
    if (result.fail)  | 
|
| 71 | 
    return [null, url];  | 
|
| 72 | 
     | 
|
| 73 | 
    const original_url = base_url + anchor;  | 
|
| 74 | 
    const policy = result.time < min_time ? null :  | 
|
| 75 | 
    JSON.parse(decodeURIComponent(result.data));  | 
|
| 76 | 
     | 
|
| 77 | 
    return [policy.url === original_url ? policy : null, original_url];  | 
|
| 78 | 
    }  | 
|
| 79 | 
     | 
|
| 80 | 
    function employ_nonhttp_policy(policy)  | 
|
| 81 | 
    {
   | 
|
| 82 | 
    if (!policy.allow)  | 
|
| 83 | 
    return;  | 
|
| 84 | 
     | 
|
| 85 | 
    policy.nonce = gen_nonce();  | 
|
| 86 | 
    const [base_url, target] = /^([^#]*)(#?.*)$/.exec(policy.url).slice(1, 3);  | 
|
| 87 | 
    const encoded_policy = encodeURIComponent(JSON.stringify(policy));  | 
|
| 88 | 
    const payload = "haketilo_" +  | 
|
| 89 | 
    	  sign_data(encoded_policy, new Date().getTime()).join("_");
   | 
|
| 90 | 
        const resulting_url = `${base_url}#${payload}${target}`;
   | 
|
| 91 | 
    location.href = resulting_url;  | 
|
| 92 | 
    location.reload();  | 
|
| 93 | 
    }  | 
|
| 94 | 
     | 
|
| 95 | 32 | 
    /*  | 
| 96 | 33 | 
    * In the case of HTML documents:  | 
| 97 | 34 | 
    * 1. When injecting some payload we need to sanitize <meta> CSP tags before  | 
| ... | ... | |
| 306 | 243 | 
    start_data_urls_sanitizing(doc);  | 
| 307 | 244 | 
    }  | 
| 308 | 245 | 
     | 
| 309 | 
    async function disable_service_workers()  | 
|
| 246 | 
    async function _disable_service_workers()
   | 
|
| 310 | 247 | 
    {
   | 
| 311 | 248 | 
    if (!navigator.serviceWorker)  | 
| 312 | 249 | 
    return;  | 
| ... | ... | |
| 315 | 252 | 
    if (registrations.length === 0)  | 
| 316 | 253 | 
    return;  | 
| 317 | 254 | 
     | 
| 318 | 
        console.warn("Service Workers detected on this page! Unregistering and reloading");
   | 
|
| 255 | 
        console.warn("Service Workers detected on this page! Unregistering and reloading.");
   | 
|
| 319 | 256 | 
     | 
| 320 | 257 | 
        try {
   | 
| 321 | 258 | 
    await Promise.all(registrations.map(r => r.unregister()));  | 
| ... | ... | |
| 327 | 264 | 
    return new Promise(() => 0);  | 
| 328 | 265 | 
    }  | 
| 329 | 266 | 
     | 
| 330 | 
    if (!is_privileged_url(document.URL)) {
   | 
|
| 331 | 
    let policy_received_callback = () => undefined;  | 
|
| 332 | 
    let policy;  | 
|
| 333 | 
     | 
|
| 334 | 
    /* Signature valid for half an hour. */  | 
|
| 335 | 
    const min_time = new Date().getTime() - 1800 * 1000;  | 
|
| 336 | 
     | 
|
| 337 | 
        if (/^https?:/.test(document.URL)) {
   | 
|
| 338 | 
    let signatures;  | 
|
| 339 | 
    [policy, signatures] = extract_cookie_policy(document.cookie, min_time);  | 
|
| 340 | 
    for (const signature of signatures)  | 
|
| 341 | 
    	    document.cookie = `haketilo-${signature}=; Max-Age=-1;`;
   | 
|
| 342 | 
        } else {
   | 
|
| 343 | 
    const scheme = /^([^:]*)/.exec(document.URL)[1];  | 
|
| 344 | 
    const known_scheme = ["file", "ftp"].includes(scheme);  | 
|
| 345 | 
     | 
|
| 346 | 
    if (!known_scheme)  | 
|
| 347 | 
    	    console.warn(`Unknown url scheme: \`${scheme}'!`);
   | 
|
| 348 | 
     | 
|
| 349 | 
    let original_url;  | 
|
| 350 | 
    [policy, original_url] = extract_url_policy(document.URL, min_time);  | 
|
| 351 | 
    history.replaceState(null, "", original_url);  | 
|
| 352 | 
     | 
|
| 353 | 
    if (known_scheme && !policy)  | 
|
| 354 | 
    policy_received_callback = employ_nonhttp_policy;  | 
|
| 267 | 
    /*  | 
|
| 268 | 
    * Trying to use servce workers APIs might result in exceptions, for example  | 
|
| 269 | 
    * when in a non-HTML document. Because of this, we wrap the function that does  | 
|
| 270 | 
     * the actual work in a try {} block.
   | 
|
| 271 | 
    */  | 
|
| 272 | 
    async function disable_service_workers()  | 
|
| 273 | 
    {
   | 
|
| 274 | 
        try {
   | 
|
| 275 | 
    await _disable_service_workers()  | 
|
| 276 | 
        } catch (e) {
   | 
|
| 277 | 
    	console.debug("Exception thrown during an attempt to detect and disable service workers.", e);
   | 
|
| 355 | 278 | 
    }  | 
| 279 | 
    }  | 
|
| 356 | 280 | 
     | 
| 357 | 
        if (!policy) {
   | 
|
| 358 | 
    	console.debug("Using fallback policy!");
   | 
|
| 359 | 
    	policy = {allow: false, nonce: gen_nonce()};
   | 
|
| 281 | 
    function synchronously_get_policy(url)  | 
|
| 282 | 
    {
   | 
|
| 283 | 
    const encoded_url = encodeURIComponent(url);  | 
|
| 284 | 
        const request_url = `${browser.runtime.getURL("dummy")}?url=${encoded_url}`;
   | 
|
| 285 | 
     | 
|
| 286 | 
        try {
   | 
|
| 287 | 
    var xhttp = new XMLHttpRequest();  | 
|
| 288 | 
    	xhttp.open("GET", request_url, false);
   | 
|
| 289 | 
    xhttp.send();  | 
|
| 290 | 
        } catch(e) {
   | 
|
| 291 | 
    	console.error("Failure to synchronously fetch policy for url.", e);
   | 
|
| 292 | 
    	return {allow: false};
   | 
|
| 360 | 293 | 
    }  | 
| 361 | 294 | 
     | 
| 295 | 
    const policy = /^[^?]*\?settings=(.*)$/.exec(xhttp.responseURL)[1];  | 
|
| 296 | 
    return JSON.parse(decodeURIComponent(policy));  | 
|
| 297 | 
    }  | 
|
| 298 | 
     | 
|
| 299 | 
    if (!is_privileged_url(document.URL)) {
   | 
|
| 300 | 
    const policy = synchronously_get_policy(document.URL);  | 
|
| 301 | 
     | 
|
| 362 | 302 | 
    if (!(document instanceof HTMLDocument))  | 
| 363 | 
    	policy.has_payload = false;
   | 
|
| 303 | 
    	delete policy.payload;
   | 
|
| 364 | 304 | 
     | 
| 365 | 305 | 
        console.debug("current policy", policy);
   | 
| 366 | 306 | 
     | 
| 307 | 
    report_settings(policy);  | 
|
| 308 | 
     | 
|
| 309 | 
    policy.nonce = gen_nonce();  | 
|
| 310 | 
     | 
|
| 367 | 311 | 
    const doc_ready = Promise.all([  | 
| 368 | 312 | 
    policy.allow ? Promise.resolve() : sanitize_document(document, policy),  | 
| 369 | 313 | 
    policy.allow ? Promise.resolve() : disable_service_workers(),  | 
| 370 | 314 | 
    wait_loaded(document)  | 
| 371 | 315 | 
    ]);  | 
| 372 | 316 | 
     | 
| 373 | 
        handle_page_actions(policy.nonce, policy_received_callback, doc_ready);
   | 
|
| 317 | 
    handle_page_actions(policy, doc_ready);  | 
|
| 374 | 318 | 
     | 
| 375 | 319 | 
    start_activity_info_server();  | 
| 376 | 320 | 
    }  | 
| content/page_actions.js | ||
|---|---|---|
| 12 | 12 | 
    * IMPORT CONNECTION_TYPE  | 
| 13 | 13 | 
    * IMPORT browser  | 
| 14 | 14 | 
    * IMPORT report_script  | 
| 15 | 
    * IMPORT report_settings  | 
|
| 16 | 15 | 
    * IMPORT report_document_type  | 
| 17 | 16 | 
    * IMPORTS_END  | 
| 18 | 17 | 
    */  | 
| 19 | 18 | 
     | 
| 20 | 
    let policy_received_callback;
   | 
|
| 19 | 
    let policy;  | 
|
| 21 | 20 | 
    /* Snapshot url and content type early; these can be changed by other code. */  | 
| 22 | 21 | 
    let url;  | 
| 23 | 22 | 
    let is_html;  | 
| 24 | 23 | 
    let port;  | 
| 25 | 24 | 
    let loaded = false;  | 
| 26 | 25 | 
    let scripts_awaiting = [];  | 
| 27 | 
    let nonce;  | 
|
| 28 | 26 | 
     | 
| 29 | 27 | 
    function handle_message(message)  | 
| 30 | 28 | 
    {
   | 
| ... | ... | |
| 38 | 36 | 
    scripts_awaiting.push(script_text);  | 
| 39 | 37 | 
    }  | 
| 40 | 38 | 
    }  | 
| 41 | 
        if (action === "settings") {
   | 
|
| 42 | 
    report_settings(data);  | 
|
| 43 | 
    	policy_received_callback({url, allow: data[1].allow});
   | 
|
| 39 | 
        else {
   | 
|
| 40 | 
    	console.error(`Bad page action '${action}'.`);
   | 
|
| 44 | 41 | 
    }  | 
| 45 | 42 | 
    }  | 
| 46 | 43 | 
     | 
| ... | ... | |
| 61 | 58 | 
     | 
| 62 | 59 | 
        let script = document.createElement("script");
   | 
| 63 | 60 | 
    script.textContent = script_text;  | 
| 64 | 
        script.setAttribute("nonce", nonce);
   | 
|
| 61 | 
        script.setAttribute("nonce", policy.nonce);
   | 
|
| 65 | 62 | 
    script.haketilo_payload = true;  | 
| 66 | 63 | 
    document.body.appendChild(script);  | 
| 67 | 64 | 
     | 
| 68 | 65 | 
    report_script(script_text);  | 
| 69 | 66 | 
    }  | 
| 70 | 67 | 
     | 
| 71 | 
    function handle_page_actions(script_nonce, policy_received_cb,
   | 
|
| 72 | 
    			     doc_ready_promise) {
   | 
|
| 73 | 
    policy_received_callback = policy_received_cb;  | 
|
| 68 | 
    function handle_page_actions(_policy, doc_ready_promise) {
   | 
|
| 69 | 
        policy = _policy;
   | 
|
| 70 | 
     | 
|
| 74 | 71 | 
    url = document.URL;  | 
| 75 | 72 | 
    is_html = document instanceof HTMLDocument;  | 
| 76 | 73 | 
    report_document_type(is_html);  | 
| 77 | 74 | 
     | 
| 78 | 75 | 
    doc_ready_promise.then(document_ready);  | 
| 79 | 76 | 
     | 
| 80 | 
        port = browser.runtime.connect({name : CONNECTION_TYPE.PAGE_ACTIONS});
   | 
|
| 81 | 
        port.onMessage.addListener(handle_message);
   | 
|
| 82 | 
        port.postMessage({url});
   | 
|
| 83 | 
     | 
|
| 84 | 
        nonce = script_nonce;
   | 
|
| 77 | 
        if (policy.payload) {
   | 
|
| 78 | 
    	port = browser.runtime.connect({name : CONNECTION_TYPE.PAGE_ACTIONS});
   | 
|
| 79 | 
    	port.onMessage.addListener(handle_message);
   | 
|
| 80 | 
    	port.postMessage({payload: policy.payload});
   | 
|
| 81 | 
        }
   | 
|
| 85 | 82 | 
    }  | 
| 86 | 83 | 
     | 
| 87 | 84 | 
    /*  | 
| html/display-panel.js | ||
|---|---|---|
| 229 | 229 | 
    const [type, data] = message;  | 
| 230 | 230 | 
     | 
| 231 | 231 | 
        if (type === "settings") {
   | 
| 232 | 
    	let [pattern, settings] = data;
   | 
|
| 232 | 
    	const settings = data;
   | 
|
| 233 | 233 | 
     | 
| 234 | 234 | 
    blocked_span.textContent = settings.allow ? "no" : "yes";  | 
| 235 | 235 | 
     | 
| 236 | 
    	if (pattern) {
   | 
|
| 236 | 
    	if (settings.pattern) {
   | 
|
| 237 | 237 | 
    pattern_span.textContent = pattern;  | 
| 238 | 238 | 
    const settings_opener =  | 
| 239 | 
    () => open_in_settings(TYPE_PREFIX.PAGE, pattern);  | 
|
| 239 | 
    		  () => open_in_settings(TYPE_PREFIX.PAGE, settings.pattern);
   | 
|
| 240 | 240 | 
    	    view_pattern_but.classList.remove("hide");
   | 
| 241 | 241 | 
    	    view_pattern_but.addEventListener("click", settings_opener);
   | 
| 242 | 242 | 
    	} else {
   | 
| ... | ... | |
| 244 | 244 | 
    blocked_span.textContent = blocked_span.textContent + " (default)";  | 
| 245 | 245 | 
    }  | 
| 246 | 246 | 
     | 
| 247 | 
    const components = settings.components;  | 
|
| 248 | 
    	if (components) {
   | 
|
| 249 | 
    payload_span.textContent = nice_name(...components);  | 
|
| 247 | 
    	if (settings.payload) {
   | 
|
| 248 | 
    payload_span.textContent = nice_name(...settings.payload);  | 
|
| 250 | 249 | 
    	    payload_buttons_div.classList.remove("hide");
   | 
| 251 | 
    	    const settings_opener = () => open_in_settings(...components);
   | 
|
| 250 | 
    	    const settings_opener = () => open_in_settings(...settings.payload);
   | 
|
| 252 | 251 | 
    	    view_payload_but.addEventListener("click", settings_opener);
   | 
| 253 | 252 | 
    	} else {
   | 
| 254 | 253 | 
    payload_span.textContent = "none";  | 
| manifest.json | ||
|---|---|---|
| 44 | 44 | 
    "page": "html/options.html",  | 
| 45 | 45 | 
    "open_in_tab": true  | 
| 46 | 46 | 
    }_CHROMIUM_UPDATE_URL_,  | 
| 47 | 
    "web_accessible_resources": [_CHROMIUM_KEY_  | 
|
| 48 | 
    ],  | 
|
| 47 | 
    "web_accessible_resources": ["dummy"],  | 
|
| 49 | 48 | 
        "background": {
   | 
| 50 | 49 | 
    "persistent": true,  | 
| 51 | 50 | 
    "scripts": [_BGSCRIPTS_]  | 
Also available in: Unified diff
replace cookies with synchronous XmlHttpRequest as policy smuggling method.
Note: this breaks Mozilla port of Haketilo. Synchronous XmlHttpRequest doesn't work as well there. This will be fixed with dynamically-registered content scripts later.