| 1 | /**
 | 
  
    | 2 |  * This file is part of Haketilo.
 | 
  
    | 3 |  *
 | 
  
    | 4 |  * Function: Determining what to do on a given web page.
 | 
  
    | 5 |  *
 | 
  
    | 6 |  * Copyright (C) 2021 Wojtek Kosior
 | 
  
    | 7 |  *
 | 
  
    | 8 |  * This program is free software: you can redistribute it and/or modify
 | 
  
    | 9 |  * it under the terms of the GNU General Public License as published by
 | 
  
    | 10 |  * the Free Software Foundation, either version 3 of the License, or
 | 
  
    | 11 |  * (at your option) any later version.
 | 
  
    | 12 |  *
 | 
  
    | 13 |  * This program is distributed in the hope that it will be useful,
 | 
  
    | 14 |  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
  
    | 15 |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
  
    | 16 |  * GNU General Public License for more details.
 | 
  
    | 17 |  *
 | 
  
    | 18 |  * As additional permission under GNU GPL version 3 section 7, you
 | 
  
    | 19 |  * may distribute forms of that code without the copy of the GNU
 | 
  
    | 20 |  * GPL normally required by section 4, provided you include this
 | 
  
    | 21 |  * license notice and, in case of non-source distribution, a URL
 | 
  
    | 22 |  * through which recipients can access the Corresponding Source.
 | 
  
    | 23 |  * If you modify file(s) with this exception, you may extend this
 | 
  
    | 24 |  * exception to your version of the file(s), but you are not
 | 
  
    | 25 |  * obligated to do so. If you do not wish to do so, delete this
 | 
  
    | 26 |  * exception statement from your version.
 | 
  
    | 27 |  *
 | 
  
    | 28 |  * As a special exception to the GPL, any HTML file which merely
 | 
  
    | 29 |  * makes function calls to this code, and for that purpose
 | 
  
    | 30 |  * includes it by reference shall be deemed a separate work for
 | 
  
    | 31 |  * copyright law purposes. If you modify this code, you may extend
 | 
  
    | 32 |  * this exception to your version of the code, but you are not
 | 
  
    | 33 |  * obligated to do so. If you do not wish to do so, delete this
 | 
  
    | 34 |  * exception statement from your version.
 | 
  
    | 35 |  *
 | 
  
    | 36 |  * You should have received a copy of the GNU General Public License
 | 
  
    | 37 |  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
  
    | 38 |  *
 | 
  
    | 39 |  * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
 | 
  
    | 40 |  * license. Although I request that you do not make use of this code in a
 | 
  
    | 41 |  * proprietary program, I am not going to enforce this in court.
 | 
  
    | 42 |  */
 | 
  
    | 43 | 
 | 
  
    | 44 | #IMPORT common/patterns_query_tree.js AS pqt
 | 
  
    | 45 | 
 | 
  
    | 46 | #FROM common/sha256.js IMPORT sha256
 | 
  
    | 47 | 
 | 
  
    | 48 | /*
 | 
  
    | 49 |  * CSP rule that either blocks all scripts or only allows scripts with specified
 | 
  
    | 50 |  * nonce attached.
 | 
  
    | 51 |  */
 | 
  
    | 52 | function make_csp(nonce) {
 | 
  
    | 53 |     const rule = nonce ? `'nonce-${nonce}'` : "'none'";
 | 
  
    | 54 |     const csp_list = [
 | 
  
    | 55 | 	["prefetch-src",    "'none'"],
 | 
  
    | 56 | 	["script-src-attr", "'none'"],
 | 
  
    | 57 | 	["script-src",      rule, "'unsafe-eval'"],
 | 
  
    | 58 | 	["script-src-elem", rule]
 | 
  
    | 59 |     ];
 | 
  
    | 60 |     return csp_list.map(words => `${words.join(" ")};`).join(" ");
 | 
  
    | 61 | }
 | 
  
    | 62 | 
 | 
  
    | 63 | function decide_policy(patterns_tree, url, default_allow, secret)
 | 
  
    | 64 | {
 | 
  
    | 65 |     const policy = {allow: default_allow};
 | 
  
    | 66 | 
 | 
  
    | 67 |     try {
 | 
  
    | 68 | 	var payloads = pqt.search(patterns_tree, url).next().value;
 | 
  
    | 69 |     } catch (e) {
 | 
  
    | 70 | 	console.error("Haketilo:", e);
 | 
  
    | 71 | 	policy.allow = false;
 | 
  
    | 72 | 	policy.error = {haketilo_error_type: "deciding_policy"};
 | 
  
    | 73 |     }
 | 
  
    | 74 | 
 | 
  
    | 75 |     if (payloads !== undefined) {
 | 
  
    | 76 | 	/*
 | 
  
    | 77 | 	 * mapping will be either the actual mapping identifier or "~allow" if
 | 
  
    | 78 | 	 * we matched a simple script block/allow rule.
 | 
  
    | 79 | 	 */
 | 
  
    | 80 | 	policy.mapping = Object.keys(payloads).sort()[0];
 | 
  
    | 81 | 	const payload = payloads[policy.mapping];
 | 
  
    | 82 | 	if (policy.mapping === "~allow") {
 | 
  
    | 83 | 	    /* Convert 0/1 back to false/true. */
 | 
  
    | 84 | 	    policy.allow = !!payload;
 | 
  
    | 85 | 	} else /* if (payload.identifier) */ {
 | 
  
    | 86 | 	    policy.allow = false;
 | 
  
    | 87 | 	    policy.payload = payload;
 | 
  
    | 88 | 	    /*
 | 
  
    | 89 | 	     * Hash a secret and other values into a string that's unpredictable
 | 
  
    | 90 | 	     * to someone who does not know these values. What we produce here
 | 
  
    | 91 | 	     * is not a true "nonce" because it might get produced multiple
 | 
  
    | 92 | 	     * times given the same url and mapping choice. Nevertheless, this
 | 
  
    | 93 | 	     * is reasonably good given the limitations WebExtension APIs and
 | 
  
    | 94 | 	     * environments give us. If we were using a true nonce, we'd have no
 | 
  
    | 95 | 	     * reliable way of passing it to our content scripts.
 | 
  
    | 96 | 	     */
 | 
  
    | 97 | 	    const nonce_source = [
 | 
  
    | 98 | 		policy.mapping,
 | 
  
    | 99 | 		policy.payload.identifier,
 | 
  
    | 100 | 		url,
 | 
  
    | 101 | 		secret
 | 
  
    | 102 | 	    ];
 | 
  
    | 103 | 	    policy.nonce = sha256(nonce_source.join(":"));
 | 
  
    | 104 | 	}
 | 
  
    | 105 |     }
 | 
  
    | 106 | 
 | 
  
    | 107 |     if (!policy.allow)
 | 
  
    | 108 | 	policy.csp = make_csp(policy.nonce);
 | 
  
    | 109 | 
 | 
  
    | 110 |     return policy;
 | 
  
    | 111 | }
 | 
  
    | 112 | #EXPORT decide_policy
 | 
  
    | 113 | 
 | 
  
    | 114 | #EXPORT  () => ({allow: false, csp: make_csp()})  AS fallback_policy
 | 
  
    | 115 | 
 | 
  
    | 116 | #IF NEVER
 | 
  
    | 117 | 
 | 
  
    | 118 | /*
 | 
  
    | 119 |  * Note: the functions below were overeagerly written and are not used now but
 | 
  
    | 120 |  * might prove useful to once we add more functionalities and are hence kept...
 | 
  
    | 121 |  */
 | 
  
    | 122 | 
 | 
  
    | 123 | function relaxed_csp_eval(csp) {
 | 
  
    | 124 |     const new_csp_list = [];
 | 
  
    | 125 | 
 | 
  
    | 126 |     for (const directive of csp.split(";")) {
 | 
  
    | 127 | 	const directive_words = directive.trim().split(" ");
 | 
  
    | 128 | 	if (directive_words[0] === "script-src")
 | 
  
    | 129 | 	    directive_words.push("'unsafe-eval'");
 | 
  
    | 130 | 
 | 
  
    | 131 | 	new_csp_list.push(directive_words);
 | 
  
    | 132 |     }
 | 
  
    | 133 | 
 | 
  
    | 134 |     new_policy.csp = new_csp_list.map(d => `${d.join(" ")}';`).join(" ");
 | 
  
    | 135 | }
 | 
  
    | 136 | 
 | 
  
    | 137 | function relax_policy_eval(policy) {
 | 
  
    | 138 |     const new_policy = Object.assign({}, policy);
 | 
  
    | 139 | 
 | 
  
    | 140 |     return Object.assign(new_policy, {csp: relaxed_csp_eval(policy.csp)});
 | 
  
    | 141 | }
 | 
  
    | 142 | #EXPORT relax_policy_eval
 | 
  
    | 143 | 
 | 
  
    | 144 | #ENDIF
 |