diff options
-rw-r--r-- | src/lib/SyncMessage.js | 150 | ||||
-rw-r--r-- | src/manifest.json | 2 |
2 files changed, 152 insertions, 0 deletions
diff --git a/src/lib/SyncMessage.js b/src/lib/SyncMessage.js new file mode 100644 index 0000000..6d48b23 --- /dev/null +++ b/src/lib/SyncMessage.js @@ -0,0 +1,150 @@ +(() => { + let ENDPOINT_PREFIX = `https://sync-messages.invalid/${browser.extension.getURL("")}?`; + let MOZILLA = "mozSystem" in XMLHttpRequest.prototype; + + if (browser.webRequest) { + if (typeof browser.runtime.onSyncMessage !== "object") { + // Background Script side + + // cache of senders from unprivileged requests to track tab ids in Firefox + let pending = new Map(); + let tabUrlCache = new Map(); + let tabRemovalListener = null; + let CANCEL = {cancel: true}; + let {TAB_ID_NONE} = browser.tabs; + + let obrListener = request => { + let {url, tabId} = request; + let params = new URLSearchParams(url.split("?")[1]); + let msgId = params.get("id"); + let msg = params.get("msg"); + let documentUrl = params.get("url"); + let sender; + if (tabId === TAB_ID_NONE) { + // 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)) { + sender = pending.get(msgId); + pending.delete(msgId); + } else { + throw new Error(`sendSyncMessage: cannot correlate sender info for ${msgId}.`); + } + } else { + let {frameAncestors, frameId} = request; + let isTop = frameId === 0 || !!params.get("top"); + let tabUrl = frameAncestors && frameAncestors.length + && frameAncestors[frameAncestors.length - 1].url; + + if (!tabUrl) { + if (isTop) { + tabUrlCache.set(tabId, tabUrl = documentUrl); + if (!tabRemovalListener) { + browser.tabs.onRemoved.addListener(tabRemovalListener = tab => { + tabUrlCache.delete(tab.id); + }); + } + } else { + tabUrl = tabUrlCache.get(tabId); + } + } + sender = { + tab: { + id: tabId, + url: tabUrl + }, + frameId, + url: documentUrl, + timeStamp: Date.now() + }; + if (msg === null) { + // this was the unprivileged, messageless preliminary request + // to set tabId and frameId + pending.set(msgId, sender); + } + } + if (!(msg !== null && sender && url.startsWith(ENDPOINT_PREFIX))) { + return CANCEL; + } + // 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). + let result; + for (let l of listeners) { + try { + if ((result = l(JSON.parse(msg), sender)) !== undefined) break; + } catch (e) { + console.error(e); + } + } + return { + redirectUrl: `data:application/json,${JSON.stringify(result)}` + }; + }; + + let listeners = new Set(); + browser.runtime.onSyncMessage = { + ENDPOINT_PREFIX, + addListener(l) { + listeners.add(l); + if (listeners.size === 1) { + browser.webRequest.onBeforeRequest.addListener(obrListener, + {urls: [`${ENDPOINT_PREFIX}*`, `*://*/${ENDPOINT_PREFIX}*`], + types: ["xmlhttprequest"]}, + ["blocking"] + ); + } + }, + removeListener(l) { + listeners.remove(l); + if (listeners.size === 0) { + browser.webRequest.onBeforeRequest.removeListener(obrListener); + } + }, + hasListener(l) { + return listeners.has(l); + } + }; + } + } else if (typeof browser.runtime.sendSyncMessage !== "function") { + // Content Script side + if (typeof uuid !== "function") { + let uuid = () => (Math.random() * Date.now()).toString(16); + } + let docUrl = document.URL; + browser.runtime.sendSyncMessage = sendSyncMessage = msg => { + let msgId = `id=${encodeURIComponent(`${uuid()},${docUrl}`)}`; + let url = `${ENDPOINT_PREFIX}${msgId}` + + `&url=${encodeURIComponent(docUrl)}`; + if (window.top === window) { + // we add top URL information because Chromium doesn't know anything + // about frameAncestors + url += "&top=true"; + } + + if (MOZILLA) try { + // on Firefox first we send an unprivileged XHR to notify the listener + // about the tab ID, which is not sent in privileged XHR + let r = new content.XMLHttpRequest(); + let unprivilegedUrl = docUrl.startsWith("http") + ? `${document.location.origin}/${url}` : url; + r.open("GET", unprivilegedUrl, false); + r.send(null); + } catch (e) { + // we ignore the likely CORS error + } + // adding the payload + url += `&msg=${encodeURIComponent(JSON.stringify(msg))}`; + try { + // 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 + let r = new XMLHttpRequest(); + r.open("GET", url, false); + r.send(null); + return JSON.parse(r.responseText); + } catch(e) { + console.error(`syncMessage error in ${document.URL}: ${e.message}`); + } + return null; + }; + } +})(); diff --git a/src/manifest.json b/src/manifest.json index e0e5c71..0bd9338 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -38,6 +38,7 @@ "lib/UA.js", "lib/browser-polyfill.js", "lib/uuid.js", + "lib/SyncMessage.js", "lib/log.js", "lib/include.js", "lib/punycode.js", @@ -83,6 +84,7 @@ "lib/browser-polyfill.js", "lib/log.js", "lib/uuid.js", + "lib/SyncMessage.js", "lib/sha256.js", "lib/Messages.js", "lib/CSP.js", |