Skip to content

Commit 267dd5e

Browse files
committed
Avoid synchronous policy fetching whenever possible.
1 parent 80f6c8c commit 267dd5e

2 files changed

Lines changed: 72 additions & 86 deletions

File tree

src/content/staticNS.js

Lines changed: 72 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
let listenersMap = new Map();
44
let backlog = new Set();
55
let documentCSP = new DocumentCSP(document);
6-
documentCSP.removeEventAttributes();
6+
77
let ns = {
88
debug: true, // DEV_ONLY
99
get embeddingDocument() {
@@ -37,114 +37,107 @@
3737
fetchPolicy() {
3838
let url = document.URL;
3939

40-
let syncFetch = callback => {
41-
browser.runtime.sendSyncMessage(
42-
{id: "fetchPolicy", url, contextUrl: url},
43-
callback);
44-
};
45-
4640
debug(`Fetching policy from document %s, readyState %s`,
4741
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
5043
);
5144

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...
5348
if (/^(javascript|about):/.test(url)) {
5449
url = document.readyState === "loading"
5550
? document.baseURI
5651
: `${window.isSecureContext ? "https" : "http"}://${document.domain}`;
5752
debug("Fetching policy for actual URL %s (was %s)", url, document.URL);
5853
}
59-
(async () => {
60-
let policy;
54+
let asyncFetch = async () => {
6155
try {
6256
policy = await Messages.send("fetchChildPolicy", {url, contextUrl: url});
6357
} catch (e) {
64-
console.error("Error while fetching policy", e);
58+
error(e, "Error while fetching policy");
6559
}
6660
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);
6964
return;
7065
}
7166
this.setup(policy);
72-
})();
67+
}
68+
asyncFetch();
7369
return;
7470
}
7571

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);
10088
}
89+
}
10190

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+
}
122114
}
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);
127116
}
128-
debug("Stopping synchronous load to fetch and apply localPolicy...");
117+
129118
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);
134135
}
135136

136137
let setup = policy => {
137138
debug("Fetched %o, readyState %s", policy, document.readyState); // DEV_ONLY
138139
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();
148141
}
149142

150143
for (let attempts = 3; attempts-- > 0;) {
@@ -160,6 +153,7 @@
160153
}
161154
}
162155

156+
dequeueEarlyScripts();
163157
},
164158

165159
setup(policy) {
@@ -178,7 +172,6 @@
178172
this.capabilities = new Set(perms.capabilities);
179173
documentCSP.apply(this.capabilities, this.embeddingDocument);
180174
}
181-
documentCSP.restoreEventAttributes();
182175
this.canScript = this.allows("script");
183176
this.fire("capabilities");
184177
},
@@ -188,10 +181,6 @@
188181
allows(cap) {
189182
return this.capabilities && this.capabilities.has(cap);
190183
},
191-
192-
getWindowName() {
193-
return window.name;
194-
}
195184
};
196185

197186
if (this.ns) {

src/lib/SyncMessage.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,15 +240,12 @@
240240
console.debug("sendSyncMessage resume #%s/%s - %sms", id, suspended, Date.now() - startTime); // DEV_ONLY
241241
};
242242

243-
244-
245243
let domSuspender = new MutationObserver(records => {
246244
console.debug("sendSyncMessage suspending on ", records)
247245
suspend();
248246
});
249247
domSuspender.observe(document.documentElement, {childList: true});
250248

251-
252249
let finalize = () => {
253250
console.debug("sendSyncMessage finalizing");
254251
domSuspender.disconnect();

0 commit comments

Comments
 (0)