|
3 | 3 | let listenersMap = new Map(); |
4 | 4 | let backlog = new Set(); |
5 | 5 | let documentCSP = new DocumentCSP(document); |
6 | | - documentCSP.removeEventAttributes(); |
| 6 | + |
7 | 7 | let ns = { |
8 | 8 | debug: true, // DEV_ONLY |
9 | 9 | get embeddingDocument() { |
|
37 | 37 | fetchPolicy() { |
38 | 38 | let url = document.URL; |
39 | 39 |
|
40 | | - let syncFetch = callback => { |
41 | | - browser.runtime.sendSyncMessage( |
42 | | - {id: "fetchPolicy", url, contextUrl: url}, |
43 | | - callback); |
44 | | - }; |
45 | | - |
46 | 40 | debug(`Fetching policy from document %s, readyState %s`, |
47 | 41 | url, document.readyState |
48 | | - , document.documentElement.outerHTML, // DEV_ONLY |
49 | | - document.domain, document.baseURI, window.isSecureContext // DEV_ONLY |
| 42 | + //, document.domain, document.baseURI, window.isSecureContext // DEV_ONLY |
50 | 43 | ); |
51 | 44 |
|
52 | | - if (!/^(?:file|ftp|https?):/i.test(url)) { |
| 45 | + let requireDocumentCSP = /^(?:ftp|file):/.test(url); |
| 46 | + if (!requireDocumentCSP) { |
| 47 | + // CSP headers have been already provided by webRequest, we are not in a hurry... |
53 | 48 | if (/^(javascript|about):/.test(url)) { |
54 | 49 | url = document.readyState === "loading" |
55 | 50 | ? document.baseURI |
56 | 51 | : `${window.isSecureContext ? "https" : "http"}://${document.domain}`; |
57 | 52 | debug("Fetching policy for actual URL %s (was %s)", url, document.URL); |
58 | 53 | } |
59 | | - (async () => { |
60 | | - let policy; |
| 54 | + let asyncFetch = async () => { |
61 | 55 | try { |
62 | 56 | policy = await Messages.send("fetchChildPolicy", {url, contextUrl: url}); |
63 | 57 | } catch (e) { |
64 | | - console.error("Error while fetching policy", e); |
| 58 | + error(e, "Error while fetching policy"); |
65 | 59 | } |
66 | 60 | if (policy === undefined) { |
67 | | - log("Policy was undefined, retrying in 1/2 sec..."); |
68 | | - setTimeout(() => this.fetchPolicy(), 500); |
| 61 | + let delay = 300; |
| 62 | + log(`Policy was undefined, retrying in ${delay}ms...`); |
| 63 | + setTimeout(asyncFetch, delay); |
69 | 64 | return; |
70 | 65 | } |
71 | 66 | this.setup(policy); |
72 | | - })(); |
| 67 | + } |
| 68 | + asyncFetch(); |
73 | 69 | return; |
74 | 70 | } |
75 | 71 |
|
76 | | - let originalState = document.readyState; |
77 | | - let syncLoad = UA.isMozilla && /^(?:ftp|file):/.test(url); |
78 | | - let localPolicy; |
79 | | - if (syncLoad && originalState !== "complete") { |
80 | | - localPolicy = { |
81 | | - key: `[${sha256(`ns.policy.${url}|${browser.runtime.getURL("")}`)}]`, |
82 | | - read(resetName = false) { |
83 | | - let [policy, name] = |
84 | | - window.name.includes(this.key) ? window.name.split(this.key) : [null, window.name]; |
85 | | - this.policy = policy ? (policy = JSON.parse(policy)) : null; |
86 | | - if (resetName) window.name = name; |
87 | | - return {policy, name}; |
88 | | - }, |
89 | | - write(policy = this.policy, name = window.name) { |
90 | | - if (name.includes(this.key)) { |
91 | | - ({name} = this.read()); |
92 | | - } |
93 | | - let policyString = JSON.stringify(policy); |
94 | | - window.name = [policyString, name].join(this.key); |
95 | | - // verify |
96 | | - if (JSON.stringify(this.read().policy) !== policyString) { |
97 | | - throw new Error("Can't write localPolicy", policy, window.name); |
98 | | - } |
99 | | - } |
| 72 | + // Here we've got no CSP header yet (file: or ftp: URL), we need one |
| 73 | + // injected in the DOM as soon as possible. |
| 74 | + debug("No CSP yet for non-HTTP document load: fetching policy synchronously..."); |
| 75 | + documentCSP.removeEventAttributes(); |
| 76 | + |
| 77 | + let earlyScripts = []; |
| 78 | + let dequeueEarlyScripts = (last = false) => { |
| 79 | + if (!(ns.canScript && earlyScripts)) return; |
| 80 | + if (earlyScripts.length === 0) { |
| 81 | + earlyScripts = null; |
| 82 | + return; |
| 83 | + } |
| 84 | + for (let s; s = earlyScripts.shift(); ) { |
| 85 | + debug("Restoring", s); |
| 86 | + s.firstChild._replaced = true; |
| 87 | + s._original.replaceWith(s); |
100 | 88 | } |
| 89 | + } |
101 | 90 |
|
102 | | - try { |
103 | | - let {policy} = localPolicy.read(true); |
104 | | - if (policy) { |
105 | | - debug("Applying localPolicy", policy); |
106 | | - this.setup(policy); |
107 | | - let onEarlyReload = e => { |
108 | | - // this fixes infinite reload loops if Firefox decides to reload the page immediately |
109 | | - // because it needs to be reparsed (e.g. broken / late charset declaration) |
110 | | - // see https://forums.informaction.com/viewtopic.php?p=102850 |
111 | | - documentCSP.apply(new Set()); // block everything to prevent leaks from page's event handlers |
112 | | - try { |
113 | | - syncFetch(p => policy = p); // user might have changed the permissions in the meanwhile... |
114 | | - } catch (e) { |
115 | | - error(e); |
116 | | - } |
117 | | - addEventListener("pagehide", e => localPolicy.write(policy), false); |
118 | | - }; |
119 | | - addEventListener("beforeunload", onEarlyReload, false); |
120 | | - addEventListener("DOMContentLoaded", e => removeEventListener("beforeunload", onEarlyReload, false), true); |
121 | | - return; |
| 91 | + let syncFetch = callback => { |
| 92 | + browser.runtime.sendSyncMessage( |
| 93 | + {id: "fetchPolicy", url, contextUrl: url}, |
| 94 | + callback); |
| 95 | + }; |
| 96 | + |
| 97 | + if (UA.isMozilla && document.readyState !== "complete") { |
| 98 | + // Mozilla has already parsed the <head> element, we must take extra steps... |
| 99 | + |
| 100 | + debug("Early parsing: preemptively suppressing events and script execution."); |
| 101 | + { |
| 102 | + let eventTypes = []; |
| 103 | + for (let p in document.documentElement) if (p.startsWith("on")) eventTypes.push(p.substring(2)); |
| 104 | + let eventSuppressor = e => { |
| 105 | + if (!ns.canScript) { |
| 106 | + e.preventDefault(); |
| 107 | + e.stopImmediatePropagation(); |
| 108 | + e.stopPropagation(); |
| 109 | + if (e.type === "load") debug(`Suppressing ${e.type} on `, e.target); |
| 110 | + } else { |
| 111 | + debug("Stopping suppression"); |
| 112 | + for (let et of eventTypes) document.removeEventListener(et, eventSuppressor, true); |
| 113 | + } |
122 | 114 | } |
123 | | - } catch(e) { |
124 | | - error(e, "Falling back: could not setup local policy", localPolicy.policy); |
125 | | - this.setup(null); |
126 | | - return; |
| 115 | + for (let et of eventTypes) document.addEventListener(et, eventSuppressor, true); |
127 | 116 | } |
128 | | - debug("Stopping synchronous load to fetch and apply localPolicy..."); |
| 117 | + |
129 | 118 | addEventListener("beforescriptexecute", e => { |
130 | | - console.log("Blocking early script", e.target); |
131 | | - e.preventDefault(); |
132 | | - }); |
133 | | - stop(); |
| 119 | + debug(e.type, e.target); |
| 120 | + if (earlyScripts) { |
| 121 | + let s = e.target; |
| 122 | + if (s._replaced) { |
| 123 | + debug("Replaced script found"); |
| 124 | + dequeueEarlyScripts(true); |
| 125 | + return; |
| 126 | + } |
| 127 | + let replacement = document.createRange().createContextualFragment(s.outerHTML); |
| 128 | + replacement._original = e.target; |
| 129 | + earlyScripts.push(replacement); |
| 130 | + e.preventDefault(); |
| 131 | + dequeueEarlyScripts(true); |
| 132 | + debug("Blocked early script"); |
| 133 | + } |
| 134 | + }, true); |
134 | 135 | } |
135 | 136 |
|
136 | 137 | let setup = policy => { |
137 | 138 | debug("Fetched %o, readyState %s", policy, document.readyState); // DEV_ONLY |
138 | 139 | this.setup(policy); |
139 | | - if (localPolicy) { |
140 | | - try { |
141 | | - localPolicy.write(policy); |
142 | | - location.reload(false); |
143 | | - } catch (e) { |
144 | | - error(e, "Cannot write local policy, bailing out...") |
145 | | - } |
146 | | - return; |
147 | | - } |
| 140 | + documentCSP.restoreEventAttributes(); |
148 | 141 | } |
149 | 142 |
|
150 | 143 | for (let attempts = 3; attempts-- > 0;) { |
|
160 | 153 | } |
161 | 154 | } |
162 | 155 |
|
| 156 | + dequeueEarlyScripts(); |
163 | 157 | }, |
164 | 158 |
|
165 | 159 | setup(policy) { |
|
178 | 172 | this.capabilities = new Set(perms.capabilities); |
179 | 173 | documentCSP.apply(this.capabilities, this.embeddingDocument); |
180 | 174 | } |
181 | | - documentCSP.restoreEventAttributes(); |
182 | 175 | this.canScript = this.allows("script"); |
183 | 176 | this.fire("capabilities"); |
184 | 177 | }, |
|
188 | 181 | allows(cap) { |
189 | 182 | return this.capabilities && this.capabilities.has(cap); |
190 | 183 | }, |
191 | | - |
192 | | - getWindowName() { |
193 | | - return window.name; |
194 | | - } |
195 | 184 | }; |
196 | 185 |
|
197 | 186 | if (this.ns) { |
|
0 commit comments