Project

General

Profile

« Previous | Next » 

Revision 53837634

Added by koszko about 2 years ago

enable whitelisting of `file://' protocol\n\nThis commit additionally also changes the semantics of triple asterisk wildcard in URL path.

View differences:

common/misc.js
84 84
    window.open(url, "_blank");
85 85
}
86 86

  
87
/* Check if url corresponds to a browser's special page */
88
function is_privileged_url(url)
89
{
90
    return !!/^(chrome(-extension)?|moz-extension):\/\/|^about:/i.exec(url);
91
}
87
/*
88
 * Check if url corresponds to a browser's special page (or a directory index in
89
 * case of `file://' protocol).
90
 */
91
const privileged_reg =
92
      /^(chrome(-extension)?|moz-extension):\/\/|^about:|^file:\/\/.*\/$/;
93
const is_privileged_url = url => privileged_reg.test(url);
92 94

  
93 95
/* Parse a CSP header */
94 96
function parse_csp(csp) {
common/patterns.js
5 5
 * Redistribution terms are gathered in the `copyright' file.
6 6
 */
7 7

  
8
const proto_re = "[a-zA-Z]*:\/\/";
8
const proto_regex = /^(\w+):\/\/(.*)$/;
9

  
9 10
const domain_re = "[^/?#]+";
10
const segments_re = "/[^?#]*";
11
const query_re = "\\?[^#]*";
12

  
13
const url_regex = new RegExp(`\
14
^\
15
(${proto_re})\
16
(${domain_re})\
17
(${segments_re})?\
18
(${query_re})?\
19
#?.*\$\
20
`);
11
const path_re = "[^?#]*";
12
const query_re = "\\??[^#]*";
13

  
14
const http_regex = new RegExp(`^(${domain_re})(${path_re})(${query_re}).*`);
15

  
16
const file_regex = new RegExp(`^(${path_re}).*`);
21 17

  
22 18
function deconstruct_url(url)
23 19
{
24
    const regex_match = url_regex.exec(url);
25
    if (regex_match === null)
20
    const proto_match = proto_regex.exec(url);
21
    if (proto_match === null)
26 22
	return undefined;
27 23

  
28
    let [_, proto, domain, path, query] = regex_match;
24
    const deco = {proto: proto_match[1]};
29 25

  
30
    domain = domain.split(".");
31
    let path_trailing_dash =
32
	path && path[path.length - 1] === "/";
33
    path = (path || "").split("/").filter(s => s !== "");
34
    path.unshift("");
26
    if (deco.proto === "file") {
27
	deco.path = file_regex.exec(proto_match[2])[1];
28
    } else {
29
	const http_match = http_regex.exec(proto_match[2]);
30
	if (!http_match)
31
	    return undefined;
32
	[deco.domain, deco.path, deco.query] = http_match.slice(1, 4);
33
	deco.domain = deco.domain.split(".");
34
    }
35 35

  
36
    return {proto, domain, path, query, path_trailing_dash};
36
    const leading_dash = deco.path[0] === "/";
37
    deco.trailing_dash = deco.path[deco.path.length - 1] === "/";
38
    deco.path = deco.path.split("/").filter(s => s !== "");
39
    if (leading_dash || deco.path.length === 0)
40
	deco.path.unshift("");
41

  
42
    return deco;
37 43
}
38 44

  
39 45
/* Be sane: both arguments should be arrays of length >= 2 */
......
104 110
	return false
105 111
    }
106 112

  
107
    if (pattern_deco.proto !== url_deco.proto)
108
	return false;
109

  
110
    return domain_matches(url_deco.domain, pattern_deco.domain) &&
111
	path_matches(url_deco.path, url_deco.path_trailing_dash,
112
		     pattern_deco.path, pattern_deco.path_trailing_dash);
113
    return pattern_deco.proto === url_deco.proto &&
114
	!(pattern_deco.proto === "file" && pattern_deco.trailing_dash) &&
115
	!!url_deco.domain === !!pattern_deco.domain &&
116
	(!url_deco.domain ||
117
	 domain_matches(url_deco.domain, pattern_deco.domain)) &&
118
	path_matches(url_deco.path, url_deco.trailing_dash,
119
		     pattern_deco.path, pattern_deco.trailing_dash);
113 120
}
114 121

  
115
/*
116
 * Call callback for every possible pattern that matches url. Return when there
117
 * are no more patterns or callback returns false.
118
 */
119
function for_each_possible_pattern(url, callback)
122
function* each_domain_pattern(domain_segments)
120 123
{
121
    const deco = deconstruct_url(url);
122

  
123
    if (deco === undefined) {
124
	console.log("bad url format", url);
125
	return;
124
    for (let slice = 0; slice < domain_segments.length; slice++) {
125
	const domain_part = domain_segments.slice(slice).join(".");
126
	const domain_wildcards = [];
127
	if (slice === 0)
128
	    yield domain_part;
129
	if (slice === 1)
130
	    yield "*." + domain_part;
131
	if (slice > 1)
132
	    yield "**." + domain_part;
133
	yield "***." + domain_part;
126 134
    }
135
}
127 136

  
128
    for (let d_slice = 0; d_slice < deco.domain.length; d_slice++) {
129
	const domain_part = deco.domain.slice(d_slice).join(".");
130
	const domain_wildcards = [];
131
	if (d_slice === 0)
132
	    domain_wildcards.push("");
133
	if (d_slice === 1)
134
	    domain_wildcards.push("*.");
135
	if (d_slice > 0)
136
	    domain_wildcards.push("**.");
137
	domain_wildcards.push("***.");
138

  
139
	for (const domain_wildcard of domain_wildcards) {
140
	    const domain_pattern = domain_wildcard + domain_part;
141

  
142
	    for (let s_slice = deco.path.length; s_slice > 0; s_slice--) {
143
		const path_part = deco.path.slice(0, s_slice).join("/");
144
		const path_wildcards = [];
145
		if (s_slice === deco.path.length) {
146
		    if (deco.path_trailing_dash)
147
			path_wildcards.push("/");
148
		    path_wildcards.push("");
149
		}
150
		if (s_slice === deco.path.length - 1 &&
151
		    deco.path[s_slice] !== "*")
152
		    path_wildcards.push("/*");
153
		if (s_slice < deco.path.length &&
154
		    (deco.path[s_slice] !== "**" ||
155
		     s_slice < deco.path.length - 1))
156
		    path_wildcards.push("/**");
157
		if (deco.path[s_slice] !== "***" || s_slice < deco.path.length)
158
		    path_wildcards.push("/***");
159

  
160
		for (const path_wildcard of path_wildcards) {
161
		    const path_pattern = path_part + path_wildcard;
162

  
163
		    const pattern = deco.proto + domain_pattern + path_pattern;
164

  
165
		    if (callback(pattern) === false)
166
			return;
167
		}
168
	    }
137
function* each_path_pattern(path_segments, trailing_dash)
138
{
139
    for (let slice = path_segments.length; slice > 0; slice--) {
140
	const path_part = path_segments.slice(0, slice).join("/");
141
	const path_wildcards = [];
142
	if (slice === path_segments.length) {
143
	    if (trailing_dash)
144
		yield path_part + "/";
145
	    yield path_part;
169 146
	}
147
	if (slice === path_segments.length - 1 && path_segments[slice] !== "*")
148
	    yield path_part + "/*";
149
	if (slice < path_segments.length - 1)
150
	    yield path_part + "/**";
151
	if (slice < path_segments.length - 1 ||
152
	    path_segments[path_segments.length - 1] !== "***")
153
	    yield path_part + "/***";
170 154
    }
171 155
}
172 156

  
173
function possible_patterns(url)
157
/* Generate every possible pattern that matches url. */
158
function* each_url_pattern(url)
174 159
{
175
    const patterns = [];
176
    for_each_possible_pattern(url, patterns.push);
160
    const deco = deconstruct_url(url);
177 161

  
178
    return patterns;
162
    if (deco === undefined) {
163
	console.log("bad url format", url);
164
	return false;
165
    }
166

  
167
    const all_domains = deco.domain ? each_domain_pattern(deco.domain) : [""];
168
    for (const domain of all_domains) {
169
	for (const path of each_path_pattern(deco.path, deco.trailing_dash))
170
	    yield `${deco.proto}://${domain}${path}`;
171
    }
179 172
}
180 173

  
181 174
/*
182 175
 * EXPORTS_START
183 176
 * EXPORT url_matches
184
 * EXPORT for_each_possible_pattern
185
 * EXPORT possible_patterns
177
 * EXPORT each_url_pattern
186 178
 * EXPORTS_END
187 179
 */
common/settings_query.js
8 8
/*
9 9
 * IMPORTS_START
10 10
 * IMPORT TYPE_PREFIX
11
 * IMPORT for_each_possible_pattern
11
 * IMPORT each_url_pattern
12 12
 * IMPORTS_END
13 13
 */
14 14

  
15
function check_pattern(storage, pattern, multiple, matched)
16
{
17
    const settings = storage.get(TYPE_PREFIX.PAGE, pattern);
18

  
19
    if (settings === undefined)
20
	return;
21

  
22
    matched.push([pattern, settings]);
23

  
24
    if (!multiple)
25
	return false;
26
}
27

  
28 15
function query(storage, url, multiple)
29 16
{
30 17
    const matched = [];
31 18
    const cb = p => check_pattern(storage, p, multiple, matched);
32
    for_each_possible_pattern(url, cb);
19
    for (const pattern of each_url_pattern(url)) {
20
	const result = [pattern, storage.get(TYPE_PREFIX.PAGE, pattern)];
21
	if (result[1] === undefined)
22
	    continue;
23

  
24
	if (!multiple)
25
	    return result;
26
	matched.push(result);
27
    }
33 28

  
34
    return multiple ? matched : (matched[0] || [undefined, undefined]);
29
    return multiple ? matched : [undefined, undefined];
35 30
}
36 31

  
37 32
function query_best(storage, url)
content/freezer.js
49 49
	console.log('Script suppressor has detached.');
50 50
	return;
51 51
    }
52
    console.log("script event", e);
52 53
    if (e.isTrusted && !e.target._hachette_payload) {
53 54
	e.preventDefault();
54 55
	console.log('Suppressed script', e.target);
content/main.js
10 10
 * IMPORTS_START
11 11
 * IMPORT handle_page_actions
12 12
 * IMPORT extract_signed
13
 * IMPORT sign_data
13 14
 * IMPORT gen_nonce
14 15
 * IMPORT is_privileged_url
15 16
 * IMPORT mozilla_suppress_scripts
......
31 32
    parent.hachette_corresponding.appendChild(clone);
32 33
}
33 34

  
34
if (!is_privileged_url(document.URL)) {
35
    /* Signature valid for half an hour. */
36
    const min_time = new Date().getTime() - 1800 * 1000;
35
function extract_cookie_policy(cookie, min_time)
36
{
37 37
    let best_result = {time: -1};
38 38
    let policy = null;
39 39
    const extracted_signatures = [];
40
    for (const match of document.cookie.matchAll(/hachette-(\w*)=([^;]*)/g)) {
40

  
41
    for (const match of cookie.matchAll(/hachette-(\w*)=([^;]*)/g)) {
41 42
	const new_result = extract_signed(...match.slice(1, 3));
42 43
	if (new_result.fail)
43 44
	    continue;
......
56 57
	policy = new_policy;
57 58
    }
58 59

  
60
    return [policy, extracted_signatures];
61
}
62

  
63
function extract_url_policy(url, min_time)
64
{
65
    const [base_url, payload, anchor] =
66
	  /^([^#]*)#?([^#]*)(#?.*)$/.exec(url).splice(1, 4);
67

  
68
    const match = /^hachette_([^_]+)_(.*)$/.exec(payload);
69
    if (!match)
70
	return [null, url];
71

  
72
    const result = extract_signed(...match.slice(1, 3));
73
    if (result.fail)
74
	return [null, url];
75

  
76
    const original_url = base_url + anchor;
77
    const policy = result.time < min_time ? null :
78
	  JSON.parse(decodeURIComponent(result.data));
79

  
80
    return [policy.url === original_url ? policy : null, original_url];
81
}
82

  
83
function employ_nonhttp_policy(policy)
84
{
85
    if (!policy.allow)
86
	return;
87

  
88
    policy.nonce = gen_nonce();
89
    const [base_url, target] = /^([^#]*)(#?.*)$/.exec(policy.url).slice(1, 3);
90
    const encoded_policy = encodeURIComponent(JSON.stringify(policy));
91
    const payload = "hachette_" +
92
	  sign_data(encoded_policy, new Date().getTime()).join("_");
93
    const resulting_url = `${base_url}#${payload}${target}`;
94
    location.href = resulting_url;
95
    location.reload();
96
}
97

  
98
if (!is_privileged_url(document.URL)) {
99
    let policy_received_callback = () => undefined;
100
    let policy;
101

  
102
    /* Signature valid for half an hour. */
103
    const min_time = new Date().getTime() - 1800 * 1000;
104

  
105
    if (/^https?:/.test(document.URL)) {
106
	let signatures;
107
	[policy, signatures] = extract_cookie_policy(document.cookie, min_time);
108
	for (const signature of signatures)
109
	    document.cookie = `hachette-${signature}=; Max-Age=-1;`;
110
    } else {
111
	const scheme = /^([^:]*)/.exec(document.URL)[1];
112
	const known_scheme = ["file"].includes(scheme);
113

  
114
	if (!known_scheme)
115
	    console.warn(`Unknown url scheme: \`${scheme}'!`);
116

  
117
	let original_url;
118
	[policy, original_url] = extract_url_policy(document.URL, min_time);
119
	history.replaceState(null, "", original_url);
120

  
121
	if (known_scheme && !policy)
122
	    policy_received_callback = employ_nonhttp_policy;
123
    }
124

  
59 125
    if (!policy) {
60
	console.warn("WARNING! Using default policy!!!");
126
	console.warn("Using default policy!");
61 127
	policy = {allow: false, nonce: gen_nonce()};
62 128
    }
63 129

  
64
    for (const signature of extracted_signatures)
65
	document.cookie = `hachette-${signature}=; Max-Age=-1;`;
66

  
67
    handle_page_actions(policy.nonce);
130
    handle_page_actions(policy.nonce, policy_received_callback);
68 131

  
69 132
    if (!policy.allow) {
133
	if (is_mozilla) {
134
	    const script = document.querySelector("script");
135
	    if (script)
136
		script.textContent = "throw 'blocked';\n" + script.textContent;
137
	}
70 138
	const old_html = document.documentElement;
71 139
	const new_html = document.createElement("html");
72 140
	old_html.replaceWith(new_html);
content/page_actions.js
14 14
 * IMPORTS_END
15 15
 */
16 16

  
17
var port;
18
var loaded = false;
19
var scripts_awaiting = [];
20
var nonce;
17
let policy_received_callback;
18
/* Snapshot url early because document.URL can be changed by other code. */
19
let url;
20
let port;
21
let loaded = false;
22
let scripts_awaiting = [];
23
let nonce;
21 24

  
22 25
function handle_message(message)
23 26
{
......
31 34
		scripts_awaiting.push(script_text);
32 35
	}
33 36
    }
34
    if (action === "settings")
37
    if (action === "settings") {
35 38
	report_settings(data);
39
	policy_received_callback({url, allow: !!data[1] && data[1].allow});
40
    }
36 41
}
37 42

  
38 43
function document_loaded(event)
......
56 61
    report_script(script_text);
57 62
}
58 63

  
59
function handle_page_actions(script_nonce) {
64
function handle_page_actions(script_nonce, policy_received_cb) {
65
    policy_received_callback = policy_received_cb;
66
    url = document.URL;
67

  
60 68
    document.addEventListener("DOMContentLoaded", document_loaded);
61 69
    port = browser.runtime.connect({name : CONNECTION_TYPE.PAGE_ACTIONS});
62 70
    port.onMessage.addListener(handle_message);
63
    port.postMessage({url: document.URL});
71
    port.postMessage({url});
64 72

  
65 73
    nonce = script_nonce;
66 74
}
html/display-panel.js
20 20
 * IMPORT TYPE_PREFIX
21 21
 * IMPORT nice_name
22 22
 * IMPORT open_in_settings
23
 * IMPORT for_each_possible_pattern
23
 * IMPORT each_url_pattern
24 24
 * IMPORT by_id
25 25
 * IMPORT clone_template
26 26
 * IMPORTS_END
......
127 127

  
128 128
function populate_possible_patterns_list(url)
129 129
{
130
    for_each_possible_pattern(url, add_pattern_to_list);
130
    for (const pattern of each_url_pattern(url))
131
	add_pattern_to_list(pattern);
131 132

  
132 133
    for (const [pattern, settings] of query_all(storage, url)) {
133 134
	set_pattern_li_button_text(ensure_pattern_exists(pattern),

Also available in: Unified diff