Project

General

Profile

« Previous | Next » 

Revision 96068ada

Added by koszko almost 2 years ago

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.

View differences:

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