Revision 4c6a2323
Added by koszko over 1 year ago
Makefile.in | ||
---|---|---|
69 | 69 |
openssl req -x509 -new -nodes -key $< -days 1024 -out $@ \ |
70 | 70 |
-subj "/CN=Haketilo Test" |
71 | 71 |
|
72 |
test: test/certs/rootCA.pem test/certs/site.key |
|
72 |
test: test/certs/rootCA.pem test/certs/site.key $(default_target)-build.zip
|
|
73 | 73 |
MOZ_HEADLESS=whatever pytest |
74 | 74 |
|
75 | 75 |
test-environment: test/certs/rootCA.pem test/certs/site.key |
background/background.js | ||
---|---|---|
1 |
/** |
|
2 |
* This file is part of Haketilo. |
|
3 |
* |
|
4 |
* Function: Background scripts - main script. |
|
5 |
* |
|
6 |
* Copyright (C) 2022 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 background/patterns_query_manager.js |
|
45 |
#IMPORT background/webrequest.js |
|
46 |
#IMPORT background/CORS_bypass_server.js |
|
47 |
#IMPORT background/broadcast_broker.js |
|
48 |
#IMPORT background/indexeddb_files_server.js |
|
49 |
|
|
50 |
#FROM common/misc.js IMPORT gen_nonce |
|
51 |
|
|
52 |
function main() { |
|
53 |
const secret = gen_nonce(); |
|
54 |
|
|
55 |
/* |
|
56 |
* Some other services depend on IndexedDB which depende on broadcast |
|
57 |
* broker, hence we start it first. |
|
58 |
*/ |
|
59 |
broadcast_broker.start(); |
|
60 |
CORS_bypass_server.start(); |
|
61 |
indexeddb_files_server.start(); |
|
62 |
|
|
63 |
patterns_query_manager.start(secret); |
|
64 |
webrequest.start(secret); |
|
65 |
} |
|
66 |
|
|
67 |
main(); |
background/broadcast_broker.js | ||
---|---|---|
42 | 42 |
* proprietary program, I am not going to enforce this in court. |
43 | 43 |
*/ |
44 | 44 |
|
45 |
#IMPORT common/connection_types.js AS CONNECTION_TYPE |
|
46 |
|
|
47 | 45 |
#FROM common/message_server.js IMPORT listen_for_connection |
48 | 46 |
|
49 | 47 |
let next_id = 1; |
... | ... | |
169 | 167 |
|
170 | 168 |
function start() |
171 | 169 |
{ |
172 |
listen_for_connection(CONNECTION_TYPE.BROADCAST_SEND, new_broadcast_sender); |
|
173 |
listen_for_connection(CONNECTION_TYPE.BROADCAST_LISTEN, |
|
174 |
new_broadcast_listener); |
|
170 |
listen_for_connection("broadcast_send", new_broadcast_sender); |
|
171 |
listen_for_connection("broadcast_listen", new_broadcast_listener); |
|
175 | 172 |
} |
176 | 173 |
#EXPORT start |
background/main.js | ||
---|---|---|
1 |
/** |
|
2 |
* This file is part of Haketilo. |
|
3 |
* |
|
4 |
* Function: Main background script. |
|
5 |
* |
|
6 |
* Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org> |
|
7 |
* Copyright (C) 2021 Jahoti <jahoti@envs.net> |
|
8 |
* |
|
9 |
* This program is free software: you can redistribute it and/or modify |
|
10 |
* it under the terms of the GNU General Public License as published by |
|
11 |
* the Free Software Foundation, either version 3 of the License, or |
|
12 |
* (at your option) any later version. |
|
13 |
* |
|
14 |
* This program is distributed in the hope that it will be useful, |
|
15 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
16 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
17 |
* GNU General Public License for more details. |
|
18 |
* |
|
19 |
* As additional permission under GNU GPL version 3 section 7, you |
|
20 |
* may distribute forms of that code without the copy of the GNU |
|
21 |
* GPL normally required by section 4, provided you include this |
|
22 |
* license notice and, in case of non-source distribution, a URL |
|
23 |
* through which recipients can access the Corresponding Source. |
|
24 |
* If you modify file(s) with this exception, you may extend this |
|
25 |
* exception to your version of the file(s), but you are not |
|
26 |
* obligated to do so. If you do not wish to do so, delete this |
|
27 |
* exception statement from your version. |
|
28 |
* |
|
29 |
* As a special exception to the GPL, any HTML file which merely |
|
30 |
* makes function calls to this code, and for that purpose |
|
31 |
* includes it by reference shall be deemed a separate work for |
|
32 |
* copyright law purposes. If you modify this code, you may extend |
|
33 |
* this exception to your version of the code, but you are not |
|
34 |
* obligated to do so. If you do not wish to do so, delete this |
|
35 |
* exception statement from your version. |
|
36 |
* |
|
37 |
* You should have received a copy of the GNU General Public License |
|
38 |
* along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
39 |
* |
|
40 |
* I, Wojtek Kosior, thereby promise not to sue for violation of this file's |
|
41 |
* license. Although I request that you do not make use of this code in a |
|
42 |
* proprietary program, I am not going to enforce this in court. |
|
43 |
*/ |
|
44 |
|
|
45 |
#IMPORT common/storage_light.js AS light_storage |
|
46 |
|
|
47 |
#IMPORT background/storage_server.js |
|
48 |
#IMPORT background/page_actions_server.js |
|
49 |
#IMPORT background/stream_filter.js |
|
50 |
|
|
51 |
#FROM common/browser.js IMPORT browser |
|
52 |
#FROM common/stored_types.js IMPORT TYPE_PREFIX |
|
53 |
#FROM background/storage.js IMPORT get_storage |
|
54 |
#FROM common/misc.js IMPORT is_privileged_url |
|
55 |
#FROM common/settings_query.js IMPORT query_best |
|
56 |
#FROM background/policy_injector.js IMPORT inject_csp_headers |
|
57 |
|
|
58 |
const initial_data = ( |
|
59 |
#INCLUDE_VERBATIM default_settings.json |
|
60 |
); |
|
61 |
|
|
62 |
storage_server.start(); |
|
63 |
page_actions_server.start(); |
|
64 |
|
|
65 |
async function init_ext(install_details) |
|
66 |
{ |
|
67 |
if (install_details.reason != "install") |
|
68 |
return; |
|
69 |
|
|
70 |
let storage = await get_storage(); |
|
71 |
|
|
72 |
await storage.clear(); |
|
73 |
|
|
74 |
/* Below we add sample settings to the extension. */ |
|
75 |
for (let setting of initial_data) { |
|
76 |
let [key, value] = Object.entries(setting)[0]; |
|
77 |
storage.set(key[0], key.substring(1), value); |
|
78 |
} |
|
79 |
} |
|
80 |
|
|
81 |
browser.runtime.onInstalled.addListener(init_ext); |
|
82 |
|
|
83 |
/* |
|
84 |
* The function below implements a more practical interface for what it does by |
|
85 |
* wrapping the old query_best() function. |
|
86 |
*/ |
|
87 |
function decide_policy_for_url(storage, policy_observable, url) |
|
88 |
{ |
|
89 |
if (storage === undefined) |
|
90 |
return {allow: false}; |
|
91 |
|
|
92 |
const settings = |
|
93 |
{allow: policy_observable !== undefined && policy_observable.value}; |
|
94 |
|
|
95 |
const [pattern, queried_settings] = query_best(storage, url); |
|
96 |
|
|
97 |
if (queried_settings) { |
|
98 |
settings.payload = queried_settings.components; |
|
99 |
settings.allow = !!queried_settings.allow && !settings.payload; |
|
100 |
settings.pattern = pattern; |
|
101 |
} |
|
102 |
|
|
103 |
return settings; |
|
104 |
} |
|
105 |
|
|
106 |
let storage; |
|
107 |
let policy_observable = {}; |
|
108 |
|
|
109 |
function sanitize_web_page(details) |
|
110 |
{ |
|
111 |
const url = details.url; |
|
112 |
if (is_privileged_url(details.url)) |
|
113 |
return; |
|
114 |
|
|
115 |
const policy = |
|
116 |
decide_policy_for_url(storage, policy_observable, details.url); |
|
117 |
|
|
118 |
let headers = details.responseHeaders; |
|
119 |
|
|
120 |
headers = inject_csp_headers(headers, policy); |
|
121 |
|
|
122 |
let skip = false; |
|
123 |
for (const header of headers) { |
|
124 |
if ((header.name.toLowerCase().trim() === "content-disposition" && |
|
125 |
/^\s*attachment\s*(;.*)$/i.test(header.value))) |
|
126 |
skip = true; |
|
127 |
} |
|
128 |
skip = skip || (details.statusCode >= 300 && details.statusCode < 400); |
|
129 |
|
|
130 |
if (!skip) { |
|
131 |
/* Check for API availability. */ |
|
132 |
if (browser.webRequest.filterResponseData) |
|
133 |
headers = stream_filter.apply(details, headers, policy); |
|
134 |
} |
|
135 |
|
|
136 |
return {responseHeaders: headers}; |
|
137 |
} |
|
138 |
|
|
139 |
const request_url_regex = /^[^?]*\?url=(.*)$/; |
|
140 |
const redirect_url_template = browser.runtime.getURL("dummy") + "?settings="; |
|
141 |
|
|
142 |
function synchronously_smuggle_policy(details) |
|
143 |
{ |
|
144 |
/* |
|
145 |
* Content script will make a synchronous XmlHttpRequest to extension's |
|
146 |
* `dummy` file to query settings for given URL. We smuggle that |
|
147 |
* information in query parameter of the URL we redirect to. |
|
148 |
* A risk of fingerprinting arises if a page with script execution allowed |
|
149 |
* guesses the dummy file URL and makes an AJAX call to it. It is currently |
|
150 |
* a problem in ManifestV2 Chromium-family port of Haketilo because Chromium |
|
151 |
* uses predictable URLs for web-accessible resources. We plan to fix it in |
|
152 |
* the future ManifestV3 port. |
|
153 |
*/ |
|
154 |
if (details.type !== "xmlhttprequest") |
|
155 |
return {cancel: true}; |
|
156 |
|
|
157 |
console.debug(`Settings queried using XHR for '${details.url}'.`); |
|
158 |
|
|
159 |
let policy = {allow: false}; |
|
160 |
|
|
161 |
try { |
|
162 |
/* |
|
163 |
* request_url should be of the following format: |
|
164 |
* <url_for_extension's_dummy_file>?url=<valid_urlencoded_url> |
|
165 |
*/ |
|
166 |
const match = request_url_regex.exec(details.url); |
|
167 |
const queried_url = decodeURIComponent(match[1]); |
|
168 |
|
|
169 |
if (details.initiator && !queried_url.startsWith(details.initiator)) { |
|
170 |
console.warn(`Blocked suspicious query of '${url}' by '${details.initiator}'. This might be the result of page fingerprinting the browser.`); |
|
171 |
return {cancel: true}; |
|
172 |
} |
|
173 |
|
|
174 |
policy = decide_policy_for_url(storage, policy_observable, queried_url); |
|
175 |
} catch (e) { |
|
176 |
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.`); |
|
177 |
} |
|
178 |
|
|
179 |
const encoded_policy = encodeURIComponent(JSON.stringify(policy)); |
|
180 |
|
|
181 |
return {redirectUrl: redirect_url_template + encoded_policy}; |
|
182 |
} |
|
183 |
|
|
184 |
const all_types = [ |
|
185 |
"main_frame", "sub_frame", "stylesheet", "script", "image", "font", |
|
186 |
"object", "xmlhttprequest", "ping", "csp_report", "media", "websocket", |
|
187 |
"other", "main_frame", "sub_frame" |
|
188 |
]; |
|
189 |
|
|
190 |
async function start_webRequest_operations() |
|
191 |
{ |
|
192 |
storage = await get_storage(); |
|
193 |
|
|
194 |
#IF CHROMIUM |
|
195 |
const extra_opts = ["blocking", "extraHeaders"]; |
|
196 |
#ELSE |
|
197 |
const extra_opts = ["blocking"]; |
|
198 |
#ENDIF |
|
199 |
|
|
200 |
browser.webRequest.onHeadersReceived.addListener( |
|
201 |
sanitize_web_page, |
|
202 |
{urls: ["<all_urls>"], types: ["main_frame", "sub_frame"]}, |
|
203 |
extra_opts.concat("responseHeaders") |
|
204 |
); |
|
205 |
|
|
206 |
const dummy_url_pattern = browser.runtime.getURL("dummy") + "?url=*"; |
|
207 |
browser.webRequest.onBeforeRequest.addListener( |
|
208 |
synchronously_smuggle_policy, |
|
209 |
{urls: [dummy_url_pattern], types: ["xmlhttprequest"]}, |
|
210 |
extra_opts |
|
211 |
); |
|
212 |
|
|
213 |
policy_observable = await light_storage.observe_var("default_allow"); |
|
214 |
} |
|
215 |
|
|
216 |
start_webRequest_operations(); |
|
217 |
|
|
218 |
#IF MOZILLA |
|
219 |
const code = `\ |
|
220 |
console.warn("Hi, I'm Mr Dynamic!"); |
|
221 |
|
|
222 |
console.debug("let's see how window.haketilo_exports looks like now"); |
|
223 |
|
|
224 |
console.log("haketilo_exports", window.haketilo_exports); |
|
225 |
` |
|
226 |
|
|
227 |
async function test_dynamic_content_scripts() |
|
228 |
{ |
|
229 |
browser.contentScripts.register({ |
|
230 |
"js": [{code}], |
|
231 |
"matches": ["<all_urls>"], |
|
232 |
"allFrames": true, |
|
233 |
"runAt": "document_start" |
|
234 |
}); |
|
235 |
} |
|
236 |
|
|
237 |
test_dynamic_content_scripts(); |
|
238 |
#ENDIF |
background/page_actions_server.js | ||
---|---|---|
1 |
/** |
|
2 |
* This file is part of Haketilo. |
|
3 |
* |
|
4 |
* Function: Serving page actions to content scripts. |
|
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/storage_light.js AS light_storage |
|
45 |
#IMPORT common/connection_types.js AS CONNECTION_TYPE |
|
46 |
|
|
47 |
#FROM common/browser.js IMPORT browser |
|
48 |
#FROM common/message_server.js IMPORT listen_for_connection |
|
49 |
#FROM background/storage.js IMPORT get_storage |
|
50 |
#FROM common/stored_types.js IMPORT TYPE_PREFIX |
|
51 |
#FROM common/sha256.js IMPORT sha256 |
|
52 |
#FROM common/ajax.js IMPORT make_ajax_request |
|
53 |
|
|
54 |
var storage; |
|
55 |
var handler; |
|
56 |
|
|
57 |
// TODO: parallelize script fetching |
|
58 |
async function send_scripts(components, port, processed_bags) |
|
59 |
{ |
|
60 |
for (let [prefix, name] of components) { |
|
61 |
if (prefix === TYPE_PREFIX.BAG) { |
|
62 |
if (processed_bags.has(name)) { |
|
63 |
console.log(`preventing recursive inclusion of bag ${name}`); |
|
64 |
continue; |
|
65 |
} |
|
66 |
|
|
67 |
var bag = storage.get(TYPE_PREFIX.BAG, name); |
|
68 |
|
|
69 |
if (bag === undefined) { |
|
70 |
console.log(`no bag in storage for key ${name}`); |
|
71 |
continue; |
|
72 |
} |
|
73 |
|
|
74 |
processed_bags.add(name); |
|
75 |
await send_scripts(bag, port, processed_bags); |
|
76 |
|
|
77 |
processed_bags.delete(name); |
|
78 |
} else { |
|
79 |
let script_text = await get_script_text(name); |
|
80 |
if (script_text === undefined) |
|
81 |
continue; |
|
82 |
|
|
83 |
port.postMessage(["inject", [script_text]]); |
|
84 |
} |
|
85 |
} |
|
86 |
} |
|
87 |
|
|
88 |
async function get_script_text(script_name) |
|
89 |
{ |
|
90 |
try { |
|
91 |
let script_data = storage.get(TYPE_PREFIX.SCRIPT, script_name); |
|
92 |
if (script_data === undefined) { |
|
93 |
console.log(`missing data for ${script_name}`); |
|
94 |
return; |
|
95 |
} |
|
96 |
let script_text = script_data.text; |
|
97 |
if (!script_text) |
|
98 |
script_text = await fetch_remote_script(script_data); |
|
99 |
return script_text; |
|
100 |
} catch (e) { |
|
101 |
console.log(e); |
|
102 |
} |
|
103 |
} |
|
104 |
|
|
105 |
async function fetch_remote_script(script_data) |
|
106 |
{ |
|
107 |
try { |
|
108 |
let xhttp = await make_ajax_request("GET", script_data.url); |
|
109 |
if (xhttp.status === 200) { |
|
110 |
let computed_hash = sha256(xhttp.responseText); |
|
111 |
if (computed_hash !== script_data.hash) { |
|
112 |
console.log(`Bad hash for ${script_data.url}\n got ${computed_hash} instead of ${script_data.hash}`); |
|
113 |
return; |
|
114 |
} |
|
115 |
return xhttp.responseText; |
|
116 |
} else { |
|
117 |
console.log("script not fetched: " + script_data.url); |
|
118 |
return; |
|
119 |
} |
|
120 |
} catch (e) { |
|
121 |
console.log(e); |
|
122 |
} |
|
123 |
} |
|
124 |
|
|
125 |
function handle_message(port, message, handler) |
|
126 |
{ |
|
127 |
port.onMessage.removeListener(handler[0]); |
|
128 |
console.debug(`Loading payload '${message.payload}'.`); |
|
129 |
|
|
130 |
const processed_bags = new Set(); |
|
131 |
|
|
132 |
send_scripts([message.payload], port, processed_bags); |
|
133 |
} |
|
134 |
|
|
135 |
function new_connection(port) |
|
136 |
{ |
|
137 |
console.log("new page actions connection!"); |
|
138 |
let handler = []; |
|
139 |
handler.push(m => handle_message(port, m, handler)); |
|
140 |
port.onMessage.addListener(handler[0]); |
|
141 |
} |
|
142 |
|
|
143 |
async function start() |
|
144 |
{ |
|
145 |
storage = await get_storage(); |
|
146 |
|
|
147 |
listen_for_connection(CONNECTION_TYPE.PAGE_ACTIONS, new_connection); |
|
148 |
} |
|
149 |
#EXPORT start |
background/patterns_query_manager.js | ||
---|---|---|
64 | 64 |
let script_update_occuring = false; |
65 | 65 |
let script_update_needed; |
66 | 66 |
|
67 |
async function update_content_script() |
|
68 |
{ |
|
67 |
async function update_content_script() { |
|
69 | 68 |
if (script_update_occuring) |
70 | 69 |
return; |
71 | 70 |
|
... | ... | |
98 | 97 |
} |
99 | 98 |
#ENDIF |
100 | 99 |
|
101 |
function register(kind, object) |
|
102 |
{ |
|
100 |
function register(kind, object) { |
|
103 | 101 |
if (kind === "mappings") { |
104 | 102 |
for (const [pattern, resource] of Object.entries(object.payloads)) |
105 | 103 |
pqt.register(tree, pattern, object.identifier, resource); |
... | ... | |
118 | 116 |
#ENDIF |
119 | 117 |
} |
120 | 118 |
|
121 |
function changed(kind, change) |
|
122 |
{ |
|
119 |
function changed(kind, change) { |
|
123 | 120 |
const old_version = currently_registered.get(change.key); |
124 | 121 |
if (old_version !== undefined) { |
125 | 122 |
if (kind === "mappings") { |
... | ... | |
143 | 140 |
#ENDIF |
144 | 141 |
} |
145 | 142 |
|
146 |
async function start(secret_) |
|
147 |
{ |
|
143 |
function setting_changed(change) { |
|
144 |
if (change.key !== "default_allow") |
|
145 |
return; |
|
146 |
|
|
147 |
default_allow.value = (change.new_val || {}).value; |
|
148 |
|
|
149 |
#IF MOZILLA || MV3 |
|
150 |
script_update_needed = true; |
|
151 |
setTimeout(update_content_script, 0); |
|
152 |
#ENDIF |
|
153 |
} |
|
154 |
|
|
155 |
async function start(secret_) { |
|
148 | 156 |
secret = secret_; |
149 | 157 |
|
150 | 158 |
const [mapping_tracking, initial_mappings] = |
... | ... | |
155 | 163 |
initial_mappings.forEach(m => register("mappings", m)); |
156 | 164 |
initial_blocking.forEach(b => register("blocking", b)); |
157 | 165 |
|
158 |
const set_allow_val = ch => default_allow.value = (ch.new_val || {}).value; |
|
159 | 166 |
const [setting_tracking, initial_settings] = |
160 |
await haketilodb.track.settings(set_allow_val); |
|
167 |
await haketilodb.track.settings(setting_changed); |
|
168 |
|
|
161 | 169 |
for (const setting of initial_settings) { |
162 | 170 |
if (setting.name === "default_allow") |
163 | 171 |
Object.assign(default_allow, setting); |
background/policy_injector.js | ||
---|---|---|
1 |
/** |
|
2 |
* This file is part of Haketilo. |
|
3 |
* |
|
4 |
* Function: Injecting policy to page by modifying HTTP headers. |
|
5 |
* |
|
6 |
* Copyright (C) 2021, Wojtek Kosior |
|
7 |
* Copyright (C) 2021, jahoti |
|
8 |
* |
|
9 |
* This program is free software: you can redistribute it and/or modify |
|
10 |
* it under the terms of the GNU General Public License as published by |
|
11 |
* the Free Software Foundation, either version 3 of the License, or |
|
12 |
* (at your option) any later version. |
|
13 |
* |
|
14 |
* This program is distributed in the hope that it will be useful, |
|
15 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
16 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
17 |
* GNU General Public License for more details. |
|
18 |
* |
|
19 |
* As additional permission under GNU GPL version 3 section 7, you |
|
20 |
* may distribute forms of that code without the copy of the GNU |
|
21 |
* GPL normally required by section 4, provided you include this |
|
22 |
* license notice and, in case of non-source distribution, a URL |
|
23 |
* through which recipients can access the Corresponding Source. |
|
24 |
* If you modify file(s) with this exception, you may extend this |
|
25 |
* exception to your version of the file(s), but you are not |
|
26 |
* obligated to do so. If you do not wish to do so, delete this |
|
27 |
* exception statement from your version. |
|
28 |
* |
|
29 |
* As a special exception to the GPL, any HTML file which merely |
|
30 |
* makes function calls to this code, and for that purpose |
|
31 |
* includes it by reference shall be deemed a separate work for |
|
32 |
* copyright law purposes. If you modify this code, you may extend |
|
33 |
* this exception to your version of the code, but you are not |
|
34 |
* obligated to do so. If you do not wish to do so, delete this |
|
35 |
* exception statement from your version. |
|
36 |
* |
|
37 |
* You should have received a copy of the GNU General Public License |
|
38 |
* along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
39 |
* |
|
40 |
* |
|
41 |
* I, Wojtek Kosior, thereby promise not to sue for violation of this file's |
|
42 |
* license. Although I request that you do not make use of this code in a |
|
43 |
* proprietary program, I am not going to enforce this in court. |
|
44 |
*/ |
|
45 |
|
|
46 |
#FROM common/misc.js IMPORT csp_header_regex |
|
47 |
|
|
48 |
/* Re-enable the import below once nonce stuff here is ready */ |
|
49 |
#IF NEVER |
|
50 |
#FROM common/misc.js IMPORT gen_nonce |
|
51 |
#ENDIF |
|
52 |
|
|
53 |
/* CSP rule that blocks scripts according to policy's needs. */ |
|
54 |
function make_csp_rule(policy) |
|
55 |
{ |
|
56 |
let rule = "prefetch-src 'none'; script-src-attr 'none';"; |
|
57 |
const script_src = policy.nonce !== undefined ? |
|
58 |
`'nonce-${policy.nonce}'` : "'none'"; |
|
59 |
rule += ` script-src ${script_src}; script-src-elem ${script_src};`; |
|
60 |
return rule; |
|
61 |
} |
|
62 |
|
|
63 |
function inject_csp_headers(headers, policy) |
|
64 |
{ |
|
65 |
let csp_headers; |
|
66 |
|
|
67 |
if (policy.payload) { |
|
68 |
headers = headers.filter(h => !csp_header_regex.test(h.name)); |
|
69 |
|
|
70 |
// TODO: make CSP rules with nonces and facilitate passing them to |
|
71 |
// content scripts via dynamic content script registration or |
|
72 |
// synchronous XHRs |
|
73 |
|
|
74 |
// policy.nonce = gen_nonce(); |
|
75 |
} |
|
76 |
|
|
77 |
if (!policy.allow && (policy.nonce || !policy.payload)) { |
|
78 |
headers.push({ |
|
79 |
name: "content-security-policy", |
|
80 |
value: make_csp_rule(policy) |
|
81 |
}); |
|
82 |
} |
|
83 |
|
|
84 |
return headers; |
|
85 |
} |
|
86 |
|
|
87 |
#EXPORT inject_csp_headers |
background/storage.js | ||
---|---|---|
1 |
/** |
|
2 |
* This file is part of Haketilo. |
|
3 |
* |
|
4 |
* Function: Storage manager. |
|
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/storage_raw.js AS raw_storage |
|
45 |
#IMPORT common/observables.js |
|
46 |
|
|
47 |
#FROM common/stored_types.js IMPORT list_prefixes, TYPE_NAME |
|
48 |
#FROM common/lock.js IMPORT lock, unlock, make_lock |
|
49 |
#FROM common/once.js IMPORT make_once |
|
50 |
#FROM common/browser.js IMPORT browser |
|
51 |
|
|
52 |
var exports = {}; |
|
53 |
|
|
54 |
/* A special case of persisted variable is one that contains list of items. */ |
|
55 |
|
|
56 |
async function get_list_var(name) |
|
57 |
{ |
|
58 |
let list = await raw_storage.get_var(name); |
|
59 |
|
|
60 |
return list === undefined ? [] : list; |
|
61 |
} |
|
62 |
|
|
63 |
/* We maintain in-memory copies of some stored lists. */ |
|
64 |
|
|
65 |
async function list(prefix) |
|
66 |
{ |
|
67 |
let name = TYPE_NAME[prefix] + "s"; /* Make plural. */ |
|
68 |
let map = new Map(); |
|
69 |
|
|
70 |
for (let item of await get_list_var(name)) |
|
71 |
map.set(item, await raw_storage.get(prefix + item)); |
|
72 |
|
|
73 |
return {map, prefix, name, observable: observables.make(), |
|
74 |
lock: make_lock()}; |
|
75 |
} |
|
76 |
|
|
77 |
var list_by_prefix = {}; |
|
78 |
|
|
79 |
async function init() |
|
80 |
{ |
|
81 |
for (let prefix of list_prefixes) |
|
82 |
list_by_prefix[prefix] = await list(prefix); |
|
83 |
|
|
84 |
return exports; |
|
85 |
} |
|
86 |
|
|
87 |
/* |
|
88 |
* Facilitate listening to changes |
|
89 |
*/ |
|
90 |
|
|
91 |
exports.add_change_listener = function (cb, prefixes=list_prefixes) |
|
92 |
{ |
|
93 |
if (typeof(prefixes) === "string") |
|
94 |
prefixes = [prefixes]; |
|
95 |
|
|
96 |
for (let prefix of prefixes) |
|
97 |
observables.subscribe(list_by_prefix[prefix].observable, cb); |
|
98 |
} |
|
99 |
|
|
100 |
exports.remove_change_listener = function (cb, prefixes=list_prefixes) |
|
101 |
{ |
|
102 |
if (typeof(prefixes) === "string") |
|
103 |
prefixes = [prefixes]; |
|
104 |
|
|
105 |
for (let prefix of prefixes) |
|
106 |
observables.unsubscribe(list_by_prefix[prefix].observable, cb); |
|
107 |
} |
|
108 |
|
|
109 |
/* Prepare some hepler functions to get elements of a list */ |
|
110 |
|
|
111 |
function list_items_it(list, with_values=false) |
|
112 |
{ |
|
113 |
return with_values ? list.map.entries() : list.map.keys(); |
|
114 |
} |
|
115 |
|
|
116 |
function list_entries_it(list) |
|
117 |
{ |
|
118 |
return list_items_it(list, true); |
|
119 |
} |
|
120 |
|
|
121 |
function list_items(list, with_values=false) |
|
122 |
{ |
|
123 |
let array = []; |
|
124 |
|
|
125 |
for (let item of list_items_it(list, with_values)) |
|
126 |
array.push(item); |
|
127 |
|
|
128 |
return array; |
|
129 |
} |
|
130 |
|
|
131 |
function list_entries(list) |
|
132 |
{ |
|
133 |
return list_items(list, true); |
|
134 |
} |
|
135 |
|
|
136 |
/* |
|
137 |
* Below we make additional effort to update map of given kind of items |
|
138 |
* every time an item is added/removed to keep everything coherent. |
|
139 |
*/ |
|
140 |
async function set_item(item, value, list) |
|
141 |
{ |
|
142 |
await lock(list.lock); |
|
143 |
let result = await _set_item(...arguments); |
|
144 |
unlock(list.lock) |
|
145 |
return result; |
|
146 |
} |
|
147 |
async function _set_item(item, value, list) |
|
148 |
{ |
|
149 |
const key = list.prefix + item; |
|
150 |
const old_val = list.map.get(item); |
|
151 |
const set_obj = {[key]: value}; |
|
152 |
if (old_val === undefined) { |
|
153 |
const items = list_items(list); |
|
154 |
items.push(item); |
|
155 |
set_obj["_" + list.name] = items; |
|
156 |
} |
|
157 |
|
|
158 |
await raw_storage.set(set_obj); |
|
159 |
list.map.set(item, value); |
|
160 |
|
|
161 |
const change = { |
|
162 |
prefix : list.prefix, |
|
163 |
item, |
|
164 |
old_val, |
|
165 |
new_val : value |
|
166 |
}; |
|
167 |
|
|
168 |
observables.broadcast(list.observable, change); |
|
169 |
|
|
170 |
return old_val; |
|
171 |
} |
|
172 |
|
|
173 |
// TODO: The actual idea to set value to undefined is good - this way we can |
|
174 |
// also set a new list of items in the same API call. But such key |
|
175 |
// is still stored in the storage. We need to somehow remove it later. |
|
176 |
// For that, we're going to have to store 1 more list of each kind. |
|
177 |
async function remove_item(item, list) |
|
178 |
{ |
|
179 |
await lock(list.lock); |
|
180 |
let result = await _remove_item(...arguments); |
|
181 |
unlock(list.lock) |
|
182 |
return result; |
|
183 |
} |
|
184 |
async function _remove_item(item, list) |
|
185 |
{ |
|
186 |
const old_val = list.map.get(item); |
|
187 |
if (old_val === undefined) |
|
188 |
return; |
|
189 |
|
|
190 |
const items = list_items(list); |
|
191 |
const index = items.indexOf(item); |
|
192 |
items.splice(index, 1); |
|
193 |
|
|
194 |
await raw_storage.set({ |
|
195 |
[list.prefix + item]: undefined, |
|
196 |
["_" + list.name]: items |
|
197 |
}); |
|
198 |
list.map.delete(item); |
|
199 |
|
|
200 |
const change = { |
|
201 |
prefix : list.prefix, |
|
202 |
item, |
|
203 |
old_val, |
|
204 |
new_val : undefined |
|
205 |
}; |
|
206 |
|
|
207 |
observables.broadcast(list.observable, change); |
|
208 |
|
|
209 |
return old_val; |
|
210 |
} |
|
211 |
|
|
212 |
// TODO: same as above applies here |
|
213 |
async function replace_item(old_item, new_item, list, new_val=undefined) |
|
214 |
{ |
|
215 |
await lock(list.lock); |
|
216 |
let result = await _replace_item(...arguments); |
|
217 |
unlock(list.lock) |
|
218 |
return result; |
|
219 |
} |
|
220 |
async function _replace_item(old_item, new_item, list, new_val=undefined) |
|
221 |
{ |
|
222 |
const old_val = list.map.get(old_item); |
|
223 |
if (new_val === undefined) { |
|
224 |
if (old_val === undefined) |
|
225 |
return; |
|
226 |
new_val = old_val; |
|
227 |
} else if (new_val === old_val && new_item === old_item) { |
|
228 |
return old_val; |
|
229 |
} |
|
230 |
|
|
231 |
if (old_item === new_item || old_val === undefined) { |
|
232 |
await _set_item(new_item, new_val, list); |
|
233 |
return old_val; |
|
234 |
} |
|
235 |
|
|
236 |
const items = list_items(list); |
|
237 |
const index = items.indexOf(old_item); |
|
238 |
items[index] = new_item; |
|
239 |
|
|
240 |
await raw_storage.set({ |
|
241 |
[list.prefix + old_item]: undefined, |
|
242 |
[list.prefix + new_item]: new_val, |
|
243 |
["_" + list.name]: items |
|
244 |
}); |
|
245 |
list.map.delete(old_item); |
|
246 |
|
|
247 |
const change = { |
|
248 |
prefix : list.prefix, |
|
249 |
item : old_item, |
|
250 |
old_val, |
|
251 |
new_val : undefined |
|
252 |
}; |
|
253 |
|
|
254 |
observables.broadcast(list.observable, change); |
|
255 |
|
|
256 |
list.map.set(new_item, new_val); |
|
257 |
|
|
258 |
change.item = new_item; |
|
259 |
change.old_val = undefined; |
|
260 |
change.new_val = new_val; |
|
261 |
|
|
262 |
observables.broadcast(list.observable, change); |
|
263 |
|
|
264 |
return old_val; |
|
265 |
} |
|
266 |
|
|
267 |
/* |
|
268 |
* For scripts, item name is chosen by user, data should be |
|
269 |
* an object containing: |
|
270 |
* - script's url and hash or |
|
271 |
* - script's text or |
|
272 |
* - all three |
|
273 |
*/ |
|
274 |
|
|
275 |
/* |
|
276 |
* For bags, item name is chosen by user, data is an array of 2-element |
|
277 |
* arrays with type prefix and script/bag names. |
|
278 |
*/ |
|
279 |
|
|
280 |
/* |
|
281 |
* For pages data argument is an object with properties `allow' |
|
282 |
* and `components'. Item name is url. |
|
283 |
*/ |
|
284 |
|
|
285 |
exports.set = async function (prefix, item, data) |
|
286 |
{ |
|
287 |
return set_item(item, data, list_by_prefix[prefix]); |
|
288 |
} |
|
289 |
|
|
290 |
exports.get = function (prefix, item) |
|
291 |
{ |
|
292 |
return list_by_prefix[prefix].map.get(item); |
|
293 |
} |
|
294 |
|
|
295 |
exports.remove = async function (prefix, item) |
|
296 |
{ |
|
297 |
return remove_item(item, list_by_prefix[prefix]); |
|
298 |
} |
|
299 |
|
|
300 |
exports.replace = async function (prefix, old_item, new_item, |
|
301 |
new_data=undefined) |
|
302 |
{ |
|
303 |
return replace_item(old_item, new_item, list_by_prefix[prefix], |
|
304 |
new_data); |
|
305 |
} |
|
306 |
|
|
307 |
exports.get_all_names = function (prefix) |
|
308 |
{ |
|
309 |
return list_items(list_by_prefix[prefix]); |
|
310 |
} |
|
311 |
|
|
312 |
exports.get_all_names_it = function (prefix) |
|
313 |
{ |
|
314 |
return list_items_it(list_by_prefix[prefix]); |
|
315 |
} |
|
316 |
|
|
317 |
exports.get_all = function (prefix) |
|
318 |
{ |
|
319 |
return list_entries(list_by_prefix[prefix]); |
|
320 |
} |
|
321 |
|
|
322 |
exports.get_all_it = function (prefix) |
|
323 |
{ |
|
324 |
return list_entries_it(list_by_prefix[prefix]); |
|
325 |
} |
|
326 |
|
|
327 |
/* Finally, a quick way to wipe all the data. */ |
|
328 |
// TODO: maybe delete items in such order that none of them ever references |
|
329 |
// an already-deleted one? |
|
330 |
exports.clear = async function () |
|
331 |
{ |
|
332 |
let lists = list_prefixes.map((p) => list_by_prefix[p]); |
|
333 |
|
|
334 |
for (let list of lists) |
|
335 |
await lock(list.lock); |
|
336 |
|
|
337 |
for (let list of lists) { |
|
338 |
|
|
339 |
let change = { |
|
340 |
prefix : list.prefix, |
|
341 |
new_val : undefined |
|
342 |
}; |
|
343 |
|
|
344 |
for (let [item, val] of list_entries_it(list)) { |
|
345 |
change.item = item; |
|
346 |
change.old_val = val; |
|
347 |
observables.broadcast(list.observable, change); |
|
348 |
} |
|
349 |
|
|
350 |
list.map = new Map(); |
|
351 |
} |
|
352 |
|
|
353 |
await browser.storage.local.clear(); |
|
354 |
|
|
355 |
for (let list of lists) |
|
356 |
unlock(list.lock); |
|
357 |
} |
|
358 |
|
|
359 |
#EXPORT make_once(init) AS get_storage |
background/storage_server.js | ||
---|---|---|
1 |
/** |
|
2 |
* This file is part of Haketilo. |
|
3 |
* |
|
4 |
* Function: Storage through messages (server side). |
|
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/connection_types.js AS CONNECTION_TYPE |
|
45 |
|
|
46 |
#FROM common/message_server.js IMPORT listen_for_connection |
|
47 |
#FROM background/storage.js IMPORT get_storage |
|
48 |
#FROM common/stored_types.js IMPORT list_prefixes |
|
49 |
|
|
50 |
var storage; |
|
51 |
|
|
52 |
async function handle_remote_call(port, message) |
|
53 |
{ |
|
54 |
let [call_id, func, args] = message; |
|
55 |
|
|
56 |
try { |
|
57 |
let result = await Promise.resolve(storage[func](...args)); |
|
58 |
port.postMessage({call_id, result}); |
|
59 |
} catch (error) { |
|
60 |
error = error + ''; |
|
61 |
port.postMessage({call_id, error}); |
|
62 |
} |
|
63 |
} |
|
64 |
|
|
65 |
function remove_storage_listener(cb) |
|
66 |
{ |
|
67 |
storage.remove_change_listener(cb); |
|
68 |
} |
|
69 |
|
|
70 |
function new_connection(port) |
|
71 |
{ |
|
72 |
console.log("new remote storage connection!"); |
|
73 |
|
|
74 |
const message = {}; |
|
75 |
for (const prefix of list_prefixes) |
|
76 |
message[prefix] = storage.get_all(prefix); |
|
77 |
|
|
78 |
port.postMessage(message); |
|
79 |
|
|
80 |
let handle_change = change => port.postMessage(change); |
|
81 |
|
|
82 |
storage.add_change_listener(handle_change); |
|
83 |
|
|
84 |
port.onMessage.addListener(m => handle_remote_call(port, m)); |
|
85 |
port.onDisconnect.addListener(() => |
|
86 |
remove_storage_listener(handle_change)); |
|
87 |
} |
|
88 |
|
|
89 |
async function start() |
|
90 |
{ |
|
91 |
storage = await get_storage(); |
|
92 |
|
|
93 |
listen_for_connection(CONNECTION_TYPE.REMOTE_STORAGE, new_connection); |
|
94 |
} |
|
95 |
#EXPORT start |
background/webrequest.js | ||
---|---|---|
172 | 172 |
extra_opts |
173 | 173 |
); |
174 | 174 |
#ENDIF |
175 |
|
|
176 |
await track_default_allow(); |
|
177 | 175 |
} |
178 | 176 |
#EXPORT start |
common/ajax.js | ||
---|---|---|
1 |
/** |
|
2 |
* This file is part of Haketilo. |
|
3 |
* |
|
4 |
* Function: Wrapping XMLHttpRequest into a Promise. |
|
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 |
function ajax_callback() |
|
45 |
{ |
|
46 |
if (this.readyState == 4) |
|
47 |
this.resolve_callback(this); |
|
48 |
} |
|
49 |
|
|
50 |
function initiate_ajax_request(resolve, reject, method, url) |
|
51 |
{ |
|
52 |
const xhttp = new XMLHttpRequest(); |
|
53 |
xhttp.resolve_callback = resolve; |
|
54 |
xhttp.onreadystatechange = ajax_callback; |
|
55 |
xhttp.open(method, url, true); |
|
56 |
try { |
|
57 |
xhttp.send(); |
|
58 |
} catch(e) { |
|
59 |
console.log(e); |
|
60 |
setTimeout(reject, 0); |
|
61 |
} |
|
62 |
} |
|
63 |
|
|
64 |
function make_ajax_request(method, url) |
|
65 |
{ |
|
66 |
return new Promise((resolve, reject) => |
|
67 |
initiate_ajax_request(resolve, reject, method, url)); |
|
68 |
} |
|
69 |
|
|
70 |
#EXPORT make_ajax_request |
common/broadcast.js | ||
---|---|---|
41 | 41 |
* proprietary program, I am not going to enforce this in court. |
42 | 42 |
*/ |
43 | 43 |
|
44 |
#IMPORT common/connection_types.js AS CONNECTION_TYPE |
|
45 |
|
|
46 | 44 |
#FROM common/message_server.js IMPORT connect_to_background |
47 | 45 |
|
48 | 46 |
function sender_connection() |
49 | 47 |
{ |
50 | 48 |
return { |
51 |
port: connect_to_background(CONNECTION_TYPE.BROADCAST_SEND)
|
|
49 |
port: connect_to_background("broadcast_send")
|
|
52 | 50 |
}; |
53 | 51 |
} |
54 | 52 |
#EXPORT sender_connection |
... | ... | |
94 | 92 |
function listener_connection(cb) |
95 | 93 |
{ |
96 | 94 |
const conn = { |
97 |
port: connect_to_background(CONNECTION_TYPE.BROADCAST_LISTEN)
|
|
95 |
port: connect_to_background("broadcast_listen")
|
|
98 | 96 |
}; |
99 | 97 |
|
100 | 98 |
conn.port.onMessage.addListener(cb); |
common/connection_types.js | ||
---|---|---|
1 |
/** |
|
2 |
* This file is part of Haketilo. |
|
3 |
* |
|
4 |
* Function: Define an "enum" of message connection types. |
|
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 |
/* |
|
45 |
* Those need to be strings so they can be used as 'name' parameter |
|
46 |
* to browser.runtime.connect() |
|
47 |
*/ |
|
48 |
|
|
49 |
#EXPORT "0" AS REMOTE_STORAGE |
|
50 |
#EXPORT "1" AS PAGE_ACTIONS |
|
51 |
#EXPORT "2" AS ACTIVITY_INFO |
|
52 |
#EXPORT "3" AS BROADCAST_SEND |
|
53 |
#EXPORT "4" AS BROADCAST_LISTEN |
common/indexeddb.js | ||
---|---|---|
48 | 48 |
#IF UNIT_TEST |
49 | 49 |
{} |
50 | 50 |
#ELSE |
51 |
#INCLUDE_VERBATIM default_settings.json
|
|
51 |
#INCLUDE default_settings.json |
|
52 | 52 |
#ENDIF |
53 | 53 |
); |
54 | 54 |
|
common/lock.js | ||
---|---|---|
1 |
/** |
|
2 |
* This file is part of Haketilo. |
|
3 |
* |
|
4 |
* Function: Implement a lock (aka binary semaphore aka mutex). |
|
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 |
/* |
|
45 |
* Javascript runs single-threaded, with an event loop. Because of that, |
|
46 |
* explicit synchronization is often not needed. An exception is when we use |
|
47 |
* an API function that must wait. Ajax is an example. Callback passed to ajax |
|
48 |
* call doesn't get called immediately, but after some time. In the meantime |
|
49 |
* some other piece of code might get to execute and modify some variables. |
|
50 |
* Access to WebExtension local storage is another situation where this problem |
|
51 |
* can occur. |
|
52 |
* |
|
53 |
* This is a solution. A lock object, that can be used to delay execution of |
|
54 |
* some code until other code finishes its critical work. Locking is wrapped |
|
55 |
* in a promise. |
|
56 |
*/ |
|
57 |
|
|
58 |
#EXPORT () => ({free: true, queue: []}) AS make_lock |
|
59 |
|
|
60 |
function _lock(lock, cb) { |
|
61 |
if (lock.free) { |
|
62 |
lock.free = false; |
|
63 |
setTimeout(cb); |
|
64 |
} else { |
|
65 |
lock.queue.push(cb); |
|
66 |
} |
|
67 |
} |
|
68 |
|
|
69 |
#EXPORT lock => new Promise(resolve => _lock(lock, resolve)) AS lock |
|
70 |
|
|
71 |
function try_lock(lock) |
|
72 |
{ |
|
73 |
if (lock.free) { |
|
74 |
lock.free = false; |
|
75 |
return true; |
|
76 |
} |
|
77 |
|
|
78 |
return false; |
|
79 |
} |
|
80 |
#EXPORT try_lock |
|
81 |
|
|
82 |
function unlock(lock) { |
|
83 |
if (lock.free) |
|
84 |
throw new Exception("Attempting to release a free lock"); |
|
85 |
|
|
86 |
if (lock.queue.length === 0) { |
|
87 |
lock.free = true; |
|
88 |
} else { |
|
89 |
let cb = lock.queue[0]; |
|
90 |
lock.queue.splice(0, 1); |
|
91 |
setTimeout(cb); |
|
92 |
} |
|
93 |
} |
|
94 |
#EXPORT unlock |
common/message_server.js | ||
---|---|---|
97 | 97 |
return browser.runtime.connect({name: magic}); |
98 | 98 |
|
99 | 99 |
if (!(magic in listeners)) |
100 |
throw `no listener for '${magic}'` |
|
100 |
throw `no listener for '${magic}'`;
|
|
101 | 101 |
|
102 | 102 |
const ports = [new Port(magic), new Port(magic)]; |
103 | 103 |
ports[0].other = ports[1]; |
common/misc.js | ||
---|---|---|
42 | 42 |
* proprietary program, I am not going to enforce this in court. |
43 | 43 |
*/ |
44 | 44 |
|
45 |
#FROM common/browser.js IMPORT browser |
|
46 |
#FROM common/stored_types.js IMPORT TYPE_NAME, TYPE_PREFIX |
|
47 |
|
|
48 | 45 |
/* uint8_to_hex is a separate function used in cryptographic functions. */ |
49 | 46 |
const uint8_to_hex = |
50 | 47 |
array => [...array].map(b => ("0" + b.toString(16)).slice(-2)).join(""); |
... | ... | |
83 | 80 |
*/ |
84 | 81 |
#EXPORT (prefix, name) => `${name} (${TYPE_NAME[prefix]})` AS nice_name |
85 | 82 |
|
86 |
/* Open settings tab with given item's editing already on. */ |
|
87 |
function open_in_settings(prefix, name) |
|
88 |
{ |
|
89 |
name = encodeURIComponent(name); |
|
90 |
const url = browser.runtime.getURL("html/options.html#" + prefix + name); |
|
91 |
window.open(url, "_blank"); |
|
92 |
} |
|
93 |
#EXPORT open_in_settings |
|
94 |
|
|
95 | 83 |
/* |
96 | 84 |
* Check if url corresponds to a browser's special page (or a directory index in |
97 | 85 |
* case of `file://' protocol). |
... | ... | |
102 | 90 |
const priv_reg = /^chrome(-extension)?:\/\/|^about:|^file:\/\/[^?#]*\/([?#]|$)/; |
103 | 91 |
#ENDIF |
104 | 92 |
#EXPORT url => priv_reg.test(url) AS is_privileged_url |
105 |
|
|
106 |
/* Parse a CSP header */ |
|
107 |
function parse_csp(csp) { |
|
108 |
let directive, directive_array; |
|
109 |
let directives = {}; |
|
110 |
for (directive of csp.split(';')) { |
|
111 |
directive = directive.trim(); |
|
112 |
if (directive === '') |
|
113 |
continue; |
|
114 |
|
|
115 |
directive_array = directive.split(/\s+/); |
|
116 |
directive = directive_array.shift(); |
|
117 |
/* The "true" case should never occur; nevertheless... */ |
|
118 |
directives[directive] = directive in directives ? |
|
119 |
directives[directive].concat(directive_array) : |
|
120 |
directive_array; |
|
121 |
} |
|
122 |
return directives; |
|
123 |
} |
|
124 |
|
|
125 |
/* Regexes and objects to use as/in schemas for parse_json_with_schema(). */ |
Also available in: Unified diff
make Haketilo buildable again (for Mozilla)
How cool it is to throw away 5755 lines of code...