Project

General

Profile

Download (5.59 KB) Statistics
| Branch: | Tag: | Revision:

haketilo / background / main.js @ 96068ada

1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Main background script.
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 * Redistribution terms are gathered in the `copyright' file.
8
 */
9

    
10
/*
11
 * IMPORTS_START
12
 * IMPORT TYPE_PREFIX
13
 * IMPORT get_storage
14
 * IMPORT light_storage
15
 * IMPORT start_storage_server
16
 * IMPORT start_page_actions_server
17
 * IMPORT browser
18
 * IMPORT is_privileged_url
19
 * IMPORT query_best
20
 * IMPORT inject_csp_headers
21
 * IMPORT apply_stream_filter
22
 * IMPORT is_chrome
23
 * IMPORT is_mozilla
24
 * IMPORTS_END
25
 */
26

    
27
start_storage_server();
28
start_page_actions_server();
29

    
30
async function init_ext(install_details)
31
{
32
    if (install_details.reason != "install")
33
	return;
34

    
35
    let storage = await get_storage();
36

    
37
    await storage.clear();
38

    
39
    /*
40
     * Below we add sample settings to the extension.
41
     */
42

    
43
    for (let setting of // The next line is replaced with the contents of /default_settings.json by the build script
44
        `DEFAULT SETTINGS`
45
    ) {
46
	let [key, value] = Object.entries(setting)[0];
47
	storage.set(key[0], key.substring(1), value);
48
    }
49
}
50

    
51
browser.runtime.onInstalled.addListener(init_ext);
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
}
75

    
76
let storage;
77
let policy_observable = {};
78

    
79
function sanitize_web_page(details)
80
{
81
    const url = details.url;
82
    if (is_privileged_url(details.url))
83
	return;
84

    
85
    const policy =
86
	  decide_policy_for_url(storage, policy_observable, details.url);
87

    
88
    let headers = details.responseHeaders;
89

    
90
    headers = inject_csp_headers(headers, policy);
91

    
92
    let skip = false;
93
    for (const header of headers) {
94
	if ((header.name.toLowerCase().trim() === "content-disposition" &&
95
	     /^\s*attachment\s*(;.*)$/i.test(header.value)))
96
	    skip = true;
97
    }
98
    skip = skip || (details.statusCode >= 300 && details.statusCode < 400);
99

    
100
    if (!skip) {
101
	/* Check for API availability. */
102
	if (browser.webRequest.filterResponseData)
103
	    headers = apply_stream_filter(details, headers, policy);
104
    }
105

    
106
    return {responseHeaders: headers};
107
}
108

    
109
const request_url_regex = /^[^?]*\?url=(.*)$/;
110
const redirect_url_template = browser.runtime.getURL("dummy") + "?settings=";
111

    
112
function synchronously_smuggle_policy(details)
113
{
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};
152
}
153

    
154
const all_types = [
155
    "main_frame", "sub_frame", "stylesheet", "script", "image", "font",
156
    "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket",
157
    "other", "main_frame", "sub_frame"
158
];
159

    
160
async function start_webRequest_operations()
161
{
162
    storage = await get_storage();
163

    
164
    const extra_opts = ["blocking"];
165
    if (is_chrome)
166
	extra_opts.push("extraHeaders");
167

    
168
    browser.webRequest.onHeadersReceived.addListener(
169
	sanitize_web_page,
170
	{urls: ["<all_urls>"], types: ["main_frame", "sub_frame"]},
171
	extra_opts.concat("responseHeaders")
172
    );
173

    
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
179
    );
180

    
181
    policy_observable = await light_storage.observe_var("default_allow");
182
}
183

    
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();
(1-1/6)