Use asyncrhonous messages to deliver SyncMessage payloads on Firefox.

This commit is contained in:
hackademix 2019-10-22 09:46:31 +02:00
parent 534ab54c28
commit d196982cd5
1 changed files with 115 additions and 86 deletions

View File

@ -7,14 +7,33 @@
if (typeof browser.runtime.onSyncMessage !== "object") { if (typeof browser.runtime.onSyncMessage !== "object") {
// Background Script side // Background Script side
// cache of senders from early async messages to track tab ids in Firefox
let pending = new Map(); let pending = new Map();
if (MOZILLA) { if (MOZILLA) {
// we don't care this is async, as long as it get called before the // we don't care this is async, as long as it get called before the
// sync XHR (we are not interested in the response on the content side) // sync XHR (we are not interested in the response on the content side)
browser.runtime.onMessage.addListener((m, sender) => { browser.runtime.onMessage.addListener((m, sender) => {
if (!m.___syncMessageId) return; let wrapper = m.__syncMessage__;
pending.set(m.___syncMessageId, sender); if (!wrapper) return;
let {id} = wrapper;
pending.set(id, wrapper);
let result;
let unsuspend = result => {
pending.delete(id);
if (wrapper.unsuspend) {
setTimeout(wrapper.unsuspend, 0);
}
return result;
}
try {
result = notifyListeners(JSON.stringify(wrapper.payload), sender);
} catch(e) {
unsuspend();
throw e;
}
console.debug("sendSyncMessage: returning", result);
return (result instanceof Promise ? result
: new Promise(resolve => resolve(result))
).then(result => unsuspend(result));
}); });
} }
@ -26,6 +45,7 @@
let obrListener = request => { let obrListener = request => {
try {
let {url, tabId} = request; let {url, tabId} = request;
let params = new URLSearchParams(url.split("?")[1]); let params = new URLSearchParams(url.split("?")[1]);
let msgId = params.get("id"); let msgId = params.get("id");
@ -33,29 +53,23 @@
return asyncRet(msgId); return asyncRet(msgId);
} }
let msg = params.get("msg"); let msg = params.get("msg");
let documentUrl = params.get("url");
let suspension = !!params.get("suspend"); if (MOZILLA || tabId === TAB_ID_NONE) {
let sender; // this shoud be a mozilla suspension request
if (tabId === TAB_ID_NONE || suspension) { return params.get("suspend") ? new Promise(resolve => {
// Firefox sends privileged content script XHR without valid tab ids
// so we cache sender info from unprivileged XHR correlated by msgId
if (pending.has(msgId)) { if (pending.has(msgId)) {
sender = pending.get(msgId); let wrapper = pending.get(msgId);
if (suspension) { // we hold any script execution / DOM modification on this promise if (!wrapper.unsuspend) {
return new Promise(resolve => { wrapper.unsuspend = resolve;
sender.unsuspend = resolve; return;
});
} }
if (sender.unsuspend) {
let {unsuspend} = sender;
delete sender.unsuspend;
setTimeout(unsuspend(ret("unsuspend")), 0);
} }
pending.delete(msgId); resolve();
} else { }).then(() => ret("go on"))
throw new Error(`sendSyncMessage: cannot correlate sender info for ${msgId}.`); : CANCEL; // otherwise, bail
} }
} else { // CHROME from now on
let documentUrl = params.get("url");
let {frameAncestors, frameId} = request; let {frameAncestors, frameId} = request;
let isTop = frameId === 0 || !!params.get("top"); let isTop = frameId === 0 || !!params.get("top");
let tabUrl = frameAncestors && frameAncestors.length let tabUrl = frameAncestors && frameAncestors.length
@ -73,7 +87,7 @@
tabUrl = tabUrlCache.get(tabId); tabUrl = tabUrlCache.get(tabId);
} }
} }
sender = { let sender = {
tab: { tab: {
id: tabId, id: tabId,
url: tabUrl url: tabUrl
@ -82,27 +96,13 @@
url: documentUrl, url: documentUrl,
timeStamp: Date.now() timeStamp: Date.now()
}; };
}
if (!(msg !== null && sender)) { if (!(msg !== null && sender)) {
return CANCEL; return CANCEL;
} }
// Just like in the async runtime.sendMessage() API, let result = notifyListeners(msg, sender);
// we process the listeners in order until we find a not undefined
// result, then we return it (or undefined if none returns anything).
let result;
for (let l of listeners) {
try {
if ((result = l(JSON.parse(msg), sender)) !== undefined) break;
} catch (e) {
console.error("%o processing message %o from %o", e, msg, sender);
}
}
if (result instanceof Promise) { if (result instanceof Promise) {
if (MOZILLA) {
// Firefox supports asynchronous webRequest listeners, so we can
// just defer the return
return (async () => ret(await result))();
} else {
// On Chromium, if the promise is not resolved yet, // On Chromium, if the promise is not resolved yet,
// we redirect the XHR to the same URL (hence same msgId) // we redirect the XHR to the same URL (hence same msgId)
// while the result get cached for asynchronous retrieval // while the result get cached for asynchronous retrieval
@ -115,9 +115,11 @@
/&redirects=(\d+)|$/, // redirects count to avoid loop detection /&redirects=(\d+)|$/, // redirects count to avoid loop detection
(all, count) => `&redirects=${parseInt(count) + 1 || 1}`)}; (all, count) => `&redirects=${parseInt(count) + 1 || 1}`)};
} }
}
return ret(result); return ret(result);
}; } catch(e) {
console.error(e);
return CANCEL;
} };
let ret = r => ({redirectUrl: `data:application/json,${JSON.stringify(r)}`}) let ret = r => ({redirectUrl: `data:application/json,${JSON.stringify(r)}`})
let asyncRet = msgId => { let asyncRet = msgId => {
@ -127,6 +129,19 @@
} }
let listeners = new Set(); let listeners = new Set();
function notifyListeners(msg, sender) {
// Just like in the async runtime.sendMessage() API,
// we process the listeners in order until we find a not undefined
// result, then we return it (or undefined if none returns anything).
for (let l of listeners) {
try {
let result = l(JSON.parse(msg), sender);
if (result !== undefined) return result;
} catch (e) {
console.error("%o processing message %o from %o", e, msg, sender);
}
}
}
browser.runtime.onSyncMessage = { browser.runtime.onSyncMessage = {
ENDPOINT_PREFIX, ENDPOINT_PREFIX,
addListener(l) { addListener(l) {
@ -163,30 +178,46 @@
// about frameAncestors // about frameAncestors
url += "&top=true"; url += "&top=true";
} }
let finalizers = [];
if (MOZILLA) { if (MOZILLA) {
// on Firefox we first need to send an async message telling the // on Firefox we first need to send an async message telling the
// background script about the tab ID, which does not get sent // background script about the tab ID, which does not get sent
// with "privileged" XHR // with "privileged" XHR
browser.runtime.sendMessage({___syncMessageId: msgId}); let result;
browser.runtime.sendMessage(
{__syncMessage__: {id: msgId, payload: msg}}
).then(r => {
result = r;
}).catch(e => {
throw e;
});
// In order to cope with inconsistencies in XHR synchronicity, // In order to cope with inconsistencies in XHR synchronicity,
// allowing DOM element to be inserted and script to be executed // allowing DOM element to be inserted and script to be executed
// (seen with file:// and ftp:// loads) we additionally suspend on // (seen with file:// and ftp:// loads) we additionally suspend on
// Mutation notifications and beforescriptexecute events // Mutation notifications and beforescriptexecute events
let suspendURL = url + "&suspend"; let suspendURL = url + "&suspend=true";
let suspended = false;
let suspend = () => { let suspend = () => {
if (result || suspended) return;
suspended = true;
try {
let r = new XMLHttpRequest(); let r = new XMLHttpRequest();
r.open("GET", url, false); r.open("GET", suspendURL, false);
r.send(null); r.send(null);
} finally {
suspended = false;
}
}; };
let domSuspender = new MutationObserver(suspend); let domSuspender = new MutationObserver(suspend);
domSuspender.observe(document.documentElement, {childList: true}); domSuspender.observe(document.documentElement, {childList: true});
addEventListener("beforescriptexecute", suspend, true); addEventListener("beforescriptexecute", suspend, true);
finalizers.push(() => { try {
suspend();
} finally {
removeEventListener("beforescriptexecute", suspend, true); removeEventListener("beforescriptexecute", suspend, true);
domSuspender.disconnect(); domSuspender.disconnect();
}); }
return result;
} }
// then we send the payload using a privileged XHR, which is not subject // then we send the payload using a privileged XHR, which is not subject
// to CORS but unfortunately doesn't carry any tab id except on Chromium // to CORS but unfortunately doesn't carry any tab id except on Chromium
@ -199,8 +230,6 @@
return JSON.parse(r.responseText); return JSON.parse(r.responseText);
} catch(e) { } catch(e) {
console.error(`syncMessage error in ${document.URL}: ${e.message} (response ${r.responseText})`); console.error(`syncMessage error in ${document.URL}: ${e.message} (response ${r.responseText})`);
} finally {
for (let f of finalizers) try { f(); } catch(e) { console.error(e); }
} }
return null; return null;
}; };