1
|
/**
|
2
|
* Hachette main content script run in all frames
|
3
|
*
|
4
|
* Copyright (C) 2021 Wojtek Kosior
|
5
|
* Copyright (C) 2021 jahoti
|
6
|
* Redistribution terms are gathered in the `copyright' file.
|
7
|
*/
|
8
|
|
9
|
/*
|
10
|
* IMPORTS_START
|
11
|
* IMPORT handle_page_actions
|
12
|
* IMPORT extract_signed
|
13
|
* IMPORT gen_nonce
|
14
|
* IMPORT is_privileged_url
|
15
|
* IMPORT mozilla_suppress_scripts
|
16
|
* IMPORT is_chrome
|
17
|
* IMPORT is_mozilla
|
18
|
* IMPORT start_activity_info_server
|
19
|
* IMPORT csp_rule
|
20
|
* IMPORT is_csp_header_name
|
21
|
* IMPORT sanitize_csp_header
|
22
|
* IMPORTS_END
|
23
|
*/
|
24
|
|
25
|
function accept_node(node, parent)
|
26
|
{
|
27
|
const clone = document.importNode(node, false);
|
28
|
node.hachette_corresponding = clone;
|
29
|
/*
|
30
|
* TODO: Stop page's own issues like "Error parsing a meta element's
|
31
|
* content:" from appearing as extension's errors.
|
32
|
*/
|
33
|
parent.hachette_corresponding.appendChild(clone);
|
34
|
}
|
35
|
|
36
|
/*
|
37
|
* 1. When injecting some payload we need to sanitize <meta> CSP tags before
|
38
|
* they reach the document.
|
39
|
* 2. Only <meta> tags inside <head> are considered valid by the browser and
|
40
|
* need to be considered.
|
41
|
* 3. We want to detach <html> from document, wait until its <head> completes
|
42
|
* loading, sanitize it and re-attach <html>.
|
43
|
* 4. Browsers are eager to add <meta>'s that appear after `</head>' but before
|
44
|
* `<body>'. Due to this behavior the `DOMContentLoaded' event is considered
|
45
|
* unreliable (although it could still work properly, it is just problematic
|
46
|
* to verify).
|
47
|
* 5. We shall wait for anything to appear in or after <body> and take that as
|
48
|
* a sign <head> has _really_ finished loading.
|
49
|
*/
|
50
|
|
51
|
function make_body_start_observer(DOM_element, waiting)
|
52
|
{
|
53
|
const observer = new MutationObserver(() => try_body_started(waiting));
|
54
|
observer.observe(DOM_element, {childList: true});
|
55
|
return observer;
|
56
|
}
|
57
|
|
58
|
function try_body_started(waiting)
|
59
|
{
|
60
|
const body = waiting.detached_html.querySelector("body");
|
61
|
|
62
|
if ((body && (body.firstChild || body.nextSibling)) ||
|
63
|
waiting.doc.documentElement.nextSibling) {
|
64
|
finish_waiting(waiting);
|
65
|
return true;
|
66
|
}
|
67
|
|
68
|
if (body && waiting.observers.length < 2)
|
69
|
waiting.observers.push(make_body_start_observer(body, waiting));
|
70
|
}
|
71
|
|
72
|
function finish_waiting(waiting)
|
73
|
{
|
74
|
waiting.observers.forEach(observer => observer.disconnect());
|
75
|
waiting.doc.removeEventListener("DOMContentLoaded", waiting.loaded_cb);
|
76
|
setTimeout(waiting.callback, 0);
|
77
|
}
|
78
|
|
79
|
function _wait_for_head(doc, detached_html, callback)
|
80
|
{
|
81
|
const waiting = {doc, detached_html, callback, observers: []};
|
82
|
if (try_body_started(waiting))
|
83
|
return;
|
84
|
|
85
|
waiting.observers = [make_body_start_observer(detached_html, waiting)];
|
86
|
waiting.loaded_cb = () => finish_waiting(waiting);
|
87
|
doc.addEventListener("DOMContentLoaded", waiting.loaded_cb);
|
88
|
}
|
89
|
|
90
|
function wait_for_head(doc, detached_html)
|
91
|
{
|
92
|
return new Promise(cb => _wait_for_head(doc, detached_html, cb));
|
93
|
}
|
94
|
|
95
|
const blocked_str = "blocked";
|
96
|
|
97
|
function block_attribute(node, attr)
|
98
|
{
|
99
|
/*
|
100
|
* Disabling attributes this way allows them to still be relatively
|
101
|
* easily accessed in case they contain some useful data.
|
102
|
*/
|
103
|
const construct_name = [attr];
|
104
|
while (node.hasAttribute(construct_name.join("")))
|
105
|
construct_name.unshift(blocked_str);
|
106
|
|
107
|
while (construct_name.length > 1) {
|
108
|
construct_name.shift();
|
109
|
const name = construct_name.join("");
|
110
|
node.setAttribute(`${blocked_str}-${name}`, node.getAttribute(name));
|
111
|
}
|
112
|
|
113
|
node.removeAttribute(attr);
|
114
|
}
|
115
|
|
116
|
function sanitize_meta(meta, policy)
|
117
|
{
|
118
|
const http_equiv = meta.getAttribute("http-equiv");
|
119
|
const value = meta.content;
|
120
|
|
121
|
if (!value || !is_csp_header_name(http_equiv, true))
|
122
|
return;
|
123
|
|
124
|
block_attribute(meta, "content");
|
125
|
|
126
|
if (is_csp_header_name(http_equiv, false))
|
127
|
meta.content = sanitize_csp_header({value}, policy).value;
|
128
|
}
|
129
|
|
130
|
function apply_hachette_csp_rules(doc, policy)
|
131
|
{
|
132
|
const meta = doc.createElement("meta");
|
133
|
meta.setAttribute("http-equiv", "Content-Security-Policy");
|
134
|
meta.setAttribute("content", csp_rule(policy.nonce));
|
135
|
doc.head.append(meta);
|
136
|
/* CSP is already in effect, we can remove the <meta> now. */
|
137
|
meta.remove();
|
138
|
}
|
139
|
|
140
|
async function sanitize_document(doc, policy)
|
141
|
{
|
142
|
/*
|
143
|
* Ensure our CSP rules are employed from the beginning. This CSP injection
|
144
|
* method is, when possible, going to be applied together with CSP rules
|
145
|
* injected using webRequest.
|
146
|
*/
|
147
|
const has_own_head = doc.head;
|
148
|
if (!has_own_head)
|
149
|
doc.documentElement.prepend(doc.createElement("head"));
|
150
|
|
151
|
apply_hachette_csp_rules(doc, policy);
|
152
|
|
153
|
/* Probably not needed, but...: proceed with DOM in its initial state. */
|
154
|
if (!has_own_head)
|
155
|
doc.head.remove();
|
156
|
|
157
|
/*
|
158
|
* <html> node gets hijacked now, to be re-attached after <head> is loaded
|
159
|
* and sanitized.
|
160
|
*/
|
161
|
const old_html = doc.documentElement;
|
162
|
const new_html = doc.createElement("html");
|
163
|
old_html.replaceWith(new_html);
|
164
|
|
165
|
await wait_for_head(doc, old_html);
|
166
|
|
167
|
for (const meta of old_html.querySelectorAll("head meta"))
|
168
|
sanitize_meta(meta, policy);
|
169
|
|
170
|
new_html.replaceWith(old_html);
|
171
|
}
|
172
|
|
173
|
if (!is_privileged_url(document.URL)) {
|
174
|
const reductor =
|
175
|
(ac, [_, sig, pol]) => ac[0] && ac || [extract_signed(sig, pol), sig];
|
176
|
const matches = [...document.cookie.matchAll(/hachette-(\w*)=([^;]*)/g)];
|
177
|
let [policy, signature] = matches.reduce(reductor, []);
|
178
|
|
179
|
if (!policy || policy.url !== document.URL) {
|
180
|
console.log("WARNING! Using default policy!!!");
|
181
|
policy = {allow: false, nonce: gen_nonce()};
|
182
|
}
|
183
|
|
184
|
if (signature)
|
185
|
document.cookie = `hachette-${signature}=; Max-Age=-1;`;
|
186
|
|
187
|
if (!policy.allow)
|
188
|
sanitize_document(document, policy);
|
189
|
|
190
|
handle_page_actions(policy.nonce);
|
191
|
|
192
|
start_activity_info_server();
|
193
|
}
|