/** * Hachette injecting policy to page using webRequest * * Copyright (C) 2021 Wojtek Kosior * Copyright (C) 2021 jahoti * Redistribution terms are gathered in the `copyright' file. */ /* * IMPORTS_START * IMPORT get_storage * IMPORT browser * IMPORT is_chrome * IMPORT gen_nonce * IMPORT is_privileged_url * IMPORT sign_data * IMPORT extract_signed * IMPORT query_best * IMPORT sanitize_csp_header * IMPORT csp_rule * IMPORT is_csp_header_name * IMPORTS_END */ var storage; function headers_inject(details) { const url = details.url; if (is_privileged_url(url)) return; const [pattern, settings] = query_best(storage, url); const allow = !!(settings && settings.allow); const nonce = gen_nonce(); let orig_csp_headers; let old_signature; let hachette_header; let headers = details.responseHeaders; for (const header of headers.filter(h => h.name === "x-hachette")) { const match = /^([^%])(%.*)$/.exec(header.value); if (!match) continue; const old_data = extract_signed(...match.splice(1, 2), [[0]]); if (!old_data || old_data.url !== url) continue; /* Confirmed- it's the originals, smuggled in! */ orig_csp_headers = old_data.csp_headers; old_signature = old_data.policy_signature; hachette_header = header; break; } if (!hachette_header) { hachette_header = {name: "x-hachette"}; headers.push(hachette_header); } orig_csp_headers = orig_csp_headers || headers.filter(h => is_csp_header_name(h.name)); /* When blocking remove report-only CSP headers that snitch on us. */ headers = headers.filter(h => !is_csp_header_name(h.name, !allow)); if (old_signature) headers = headers.filter(h => h.name.search(old_signature) === -1); const policy_object = {allow, nonce, url}; const sanitizer = h => sanitize_csp_header(h, policy_object); headers.push(...orig_csp_headers.map(sanitizer)); const policy = encodeURIComponent(JSON.stringify(policy_object)); const policy_signature = sign_data(policy, new Date()); const later_30sec = new Date(new Date().getTime() + 30000).toGMTString(); headers.push({ name: "Set-Cookie", value: `hachette-${policy_signature}=${policy}; Expires=${later_30sec};` }); /* * Smuggle in the signature and the original CSP headers for future use. * These are signed with a time of 0, as it's not clear there is a limit on * how long Firefox might retain headers in the cache. */ let hachette_data = {csp_headers: orig_csp_headers, policy_signature, url}; hachette_data = encodeURIComponent(JSON.stringify(hachette_data)); hachette_header.value = sign_data(hachette_data, 0) + hachette_data; /* To ensure there is a CSP header if required */ if (!allow) headers.push({name: "content-security-policy", value: csp_rule(nonce)}); return {responseHeaders: headers}; } async function start_policy_injector() { storage = await get_storage(); let extra_opts = ["blocking", "responseHeaders"]; if (is_chrome) extra_opts.push("extraHeaders"); browser.webRequest.onHeadersReceived.addListener( headers_inject, { urls: [""], types: ["main_frame", "sub_frame"] }, extra_opts ); } /* * EXPORTS_START * EXPORT start_policy_injector * EXPORTS_END */