aboutsummaryrefslogtreecommitdiff
path: root/content/main.js
blob: 2a46c7e49769a1729551e52196351565b332f078 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
 * Myext main content script run in all frames
 *
 * Copyright (C) 2021 Wojtek Kosior
 * Redistribution terms are gathered in the `copyright' file.
 */

"use strict";

(() => {
    const handle_page_actions = window.handle_page_actions;
    const url_item = window.url_item;
    const gen_unique = window.gen_unique;
    const sanitize_attributes = window.sanitize_attributes;

    /*
     * Due to some technical limitations the chosen method of whitelisting sites
     * is to smuggle whitelist indicator in page's url as a "magical" string
     * after '#'. Right now this is not needed in HTTP(s) pages where native
     * script blocking happens through CSP header injection but is needed for
     * protocols like ftp:// and file://.
     *
     * The code that actually injects the magical string into ftp:// and file://
     * urls has not yet been added to the extension.
     */

    let url = url_item(document.URL);
    let unique = gen_unique(url);
    let nonce = unique.substring(1);
    
    const scriptSuppressor = window.scriptSuppressor(nonce);

    function needs_blocking()
    {
	if (url.startsWith("https://") || url.startsWith("http://"))
	    return false;

	let url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/;
	let match = url_re.exec(document.URL);
	let base_url = match[1];
	let first_target = match[3];
	let second_target = match[4];

	if (first_target !== undefined &&
	    first_target === unique) {
	    if (second_target !== undefined)
		window.location.href = base_url + second_target;
	    else
		history.replaceState(null, "", base_url);

	    console.log(["allowing whitelisted", document.URL]);
	    return false;
	}

	console.log(["disallowing", document.URL]);
	return true;
    }

    function handle_mutation(mutations, observer)
    {
	if (document.readyState === 'complete') {
	    console.log("complete");
	    observer.disconnect();
	    return;
	}
	for (let mutation of mutations) {
	    for (let node of mutation.addedNodes) {
		/*
		 * Modifying <script> element doesn't always prevent its
		 * execution in some Mozilla browsers. Additional blocking
		 * through CSP meta tag injection is required.
		 */
		if (node.tagName === "SCRIPT") {
		    block_script(node);
		    continue;
		}

		sanitize_attributes(node);

		if (node.tagName === "HEAD")
		    inject_csp(node);
	    }
	}
    }

    function block_script(node)
    {
	console.log(node);

	/*
	 * Disabling scripts this way allows them to still be relatively
	 * easily accessed in case they contain some useful data.
	 */
	if (node.hasAttribute("type"))
	    node.setAttribute("blocked-type", node.getAttribute("type"));
	node.setAttribute("type", "application/json");
    }

    function inject_csp(node)
    {
	console.log('injecting CSP');
	let meta = document.createElement("meta");
	meta.setAttribute("http-equiv", "Content-Security-Policy");
	meta.setAttribute("content", `\
script-src 'nonce-${nonce}'; \
script-src-elem 'nonce-${nonce}';\
`);
	node.appendChild(meta);
    }

    if (needs_blocking()) {
	// Script blocking for Gecko
	addEventListener('beforescriptexecute', scriptSuppressor, true);
	
	var observer = new MutationObserver(handle_mutation);
	observer.observe(document.documentElement, {
	    attributes: true,
	    childList: true,
	    subtree: true
	});
    }

    handle_page_actions(nonce);
})();