Avoid synchronous policy fetching whenever possible.

This commit is contained in:
hackademix 2020-08-22 17:24:13 +02:00
parent 80f6c8c8bc
commit 267dd5eb5d
2 changed files with 73 additions and 87 deletions

View File

@ -3,7 +3,7 @@
let listenersMap = new Map(); let listenersMap = new Map();
let backlog = new Set(); let backlog = new Set();
let documentCSP = new DocumentCSP(document); let documentCSP = new DocumentCSP(document);
documentCSP.removeEventAttributes();
let ns = { let ns = {
debug: true, // DEV_ONLY debug: true, // DEV_ONLY
get embeddingDocument() { get embeddingDocument() {
@ -37,114 +37,107 @@
fetchPolicy() { fetchPolicy() {
let url = document.URL; let url = document.URL;
let syncFetch = callback => {
browser.runtime.sendSyncMessage(
{id: "fetchPolicy", url, contextUrl: url},
callback);
};
debug(`Fetching policy from document %s, readyState %s`, debug(`Fetching policy from document %s, readyState %s`,
url, document.readyState url, document.readyState
, document.documentElement.outerHTML, // DEV_ONLY //, document.domain, document.baseURI, window.isSecureContext // DEV_ONLY
document.domain, document.baseURI, window.isSecureContext // DEV_ONLY
); );
if (!/^(?:file|ftp|https?):/i.test(url)) { let requireDocumentCSP = /^(?:ftp|file):/.test(url);
if (!requireDocumentCSP) {
// CSP headers have been already provided by webRequest, we are not in a hurry...
if (/^(javascript|about):/.test(url)) { if (/^(javascript|about):/.test(url)) {
url = document.readyState === "loading" url = document.readyState === "loading"
? document.baseURI ? document.baseURI
: `${window.isSecureContext ? "https" : "http"}://${document.domain}`; : `${window.isSecureContext ? "https" : "http"}://${document.domain}`;
debug("Fetching policy for actual URL %s (was %s)", url, document.URL); debug("Fetching policy for actual URL %s (was %s)", url, document.URL);
} }
(async () => { let asyncFetch = async () => {
let policy;
try { try {
policy = await Messages.send("fetchChildPolicy", {url, contextUrl: url}); policy = await Messages.send("fetchChildPolicy", {url, contextUrl: url});
} catch (e) { } catch (e) {
console.error("Error while fetching policy", e); error(e, "Error while fetching policy");
} }
if (policy === undefined) { if (policy === undefined) {
log("Policy was undefined, retrying in 1/2 sec..."); let delay = 300;
setTimeout(() => this.fetchPolicy(), 500); log(`Policy was undefined, retrying in ${delay}ms...`);
setTimeout(asyncFetch, delay);
return; return;
} }
this.setup(policy); this.setup(policy);
})(); }
asyncFetch();
return; return;
} }
let originalState = document.readyState; // Here we've got no CSP header yet (file: or ftp: URL), we need one
let syncLoad = UA.isMozilla && /^(?:ftp|file):/.test(url); // injected in the DOM as soon as possible.
let localPolicy; debug("No CSP yet for non-HTTP document load: fetching policy synchronously...");
if (syncLoad && originalState !== "complete") { documentCSP.removeEventAttributes();
localPolicy = {
key: `[${sha256(`ns.policy.${url}|${browser.runtime.getURL("")}`)}]`,
read(resetName = false) {
let [policy, name] =
window.name.includes(this.key) ? window.name.split(this.key) : [null, window.name];
this.policy = policy ? (policy = JSON.parse(policy)) : null;
if (resetName) window.name = name;
return {policy, name};
},
write(policy = this.policy, name = window.name) {
if (name.includes(this.key)) {
({name} = this.read());
}
let policyString = JSON.stringify(policy);
window.name = [policyString, name].join(this.key);
// verify
if (JSON.stringify(this.read().policy) !== policyString) {
throw new Error("Can't write localPolicy", policy, window.name);
}
}
}
try { let earlyScripts = [];
let {policy} = localPolicy.read(true); let dequeueEarlyScripts = (last = false) => {
if (policy) { if (!(ns.canScript && earlyScripts)) return;
debug("Applying localPolicy", policy); if (earlyScripts.length === 0) {
this.setup(policy); earlyScripts = null;
let onEarlyReload = e => {
// this fixes infinite reload loops if Firefox decides to reload the page immediately
// because it needs to be reparsed (e.g. broken / late charset declaration)
// see https://forums.informaction.com/viewtopic.php?p=102850
documentCSP.apply(new Set()); // block everything to prevent leaks from page's event handlers
try {
syncFetch(p => policy = p); // user might have changed the permissions in the meanwhile...
} catch (e) {
error(e);
}
addEventListener("pagehide", e => localPolicy.write(policy), false);
};
addEventListener("beforeunload", onEarlyReload, false);
addEventListener("DOMContentLoaded", e => removeEventListener("beforeunload", onEarlyReload, false), true);
return;
}
} catch(e) {
error(e, "Falling back: could not setup local policy", localPolicy.policy);
this.setup(null);
return; return;
} }
debug("Stopping synchronous load to fetch and apply localPolicy..."); for (let s; s = earlyScripts.shift(); ) {
debug("Restoring", s);
s.firstChild._replaced = true;
s._original.replaceWith(s);
}
}
let syncFetch = callback => {
browser.runtime.sendSyncMessage(
{id: "fetchPolicy", url, contextUrl: url},
callback);
};
if (UA.isMozilla && document.readyState !== "complete") {
// Mozilla has already parsed the <head> element, we must take extra steps...
debug("Early parsing: preemptively suppressing events and script execution.");
{
let eventTypes = [];
for (let p in document.documentElement) if (p.startsWith("on")) eventTypes.push(p.substring(2));
let eventSuppressor = e => {
if (!ns.canScript) {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
if (e.type === "load") debug(`Suppressing ${e.type} on `, e.target);
} else {
debug("Stopping suppression");
for (let et of eventTypes) document.removeEventListener(et, eventSuppressor, true);
}
}
for (let et of eventTypes) document.addEventListener(et, eventSuppressor, true);
}
addEventListener("beforescriptexecute", e => { addEventListener("beforescriptexecute", e => {
console.log("Blocking early script", e.target); debug(e.type, e.target);
e.preventDefault(); if (earlyScripts) {
}); let s = e.target;
stop(); if (s._replaced) {
debug("Replaced script found");
dequeueEarlyScripts(true);
return;
}
let replacement = document.createRange().createContextualFragment(s.outerHTML);
replacement._original = e.target;
earlyScripts.push(replacement);
e.preventDefault();
dequeueEarlyScripts(true);
debug("Blocked early script");
}
}, true);
} }
let setup = policy => { let setup = policy => {
debug("Fetched %o, readyState %s", policy, document.readyState); // DEV_ONLY debug("Fetched %o, readyState %s", policy, document.readyState); // DEV_ONLY
this.setup(policy); this.setup(policy);
if (localPolicy) { documentCSP.restoreEventAttributes();
try {
localPolicy.write(policy);
location.reload(false);
} catch (e) {
error(e, "Cannot write local policy, bailing out...")
}
return;
}
} }
for (let attempts = 3; attempts-- > 0;) { for (let attempts = 3; attempts-- > 0;) {
@ -160,6 +153,7 @@
} }
} }
dequeueEarlyScripts();
}, },
setup(policy) { setup(policy) {
@ -178,7 +172,6 @@
this.capabilities = new Set(perms.capabilities); this.capabilities = new Set(perms.capabilities);
documentCSP.apply(this.capabilities, this.embeddingDocument); documentCSP.apply(this.capabilities, this.embeddingDocument);
} }
documentCSP.restoreEventAttributes();
this.canScript = this.allows("script"); this.canScript = this.allows("script");
this.fire("capabilities"); this.fire("capabilities");
}, },
@ -188,10 +181,6 @@
allows(cap) { allows(cap) {
return this.capabilities && this.capabilities.has(cap); return this.capabilities && this.capabilities.has(cap);
}, },
getWindowName() {
return window.name;
}
}; };
if (this.ns) { if (this.ns) {

View File

@ -240,15 +240,12 @@
console.debug("sendSyncMessage resume #%s/%s - %sms", id, suspended, Date.now() - startTime); // DEV_ONLY console.debug("sendSyncMessage resume #%s/%s - %sms", id, suspended, Date.now() - startTime); // DEV_ONLY
}; };
let domSuspender = new MutationObserver(records => { let domSuspender = new MutationObserver(records => {
console.debug("sendSyncMessage suspending on ", records) console.debug("sendSyncMessage suspending on ", records)
suspend(); suspend();
}); });
domSuspender.observe(document.documentElement, {childList: true}); domSuspender.observe(document.documentElement, {childList: true});
let finalize = () => { let finalize = () => {
console.debug("sendSyncMessage finalizing"); console.debug("sendSyncMessage finalizing");
domSuspender.disconnect(); domSuspender.disconnect();