Avoid synchronous policy fetching whenever possible.
This commit is contained in:
parent
80f6c8c8bc
commit
267dd5eb5d
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue