· 6 years ago · Apr 05, 2020, 08:14 PM
1"use strict";
2(() => {
3 let ENDPOINT_ORIGIN = "https://255.255.255.255";
4 let ENDPOINT_PREFIX = `${ENDPOINT_ORIGIN}/${browser.extension.getURL("")}?`;
5 let MOZILLA = "mozSystem" in XMLHttpRequest.prototype;
6
7 if (browser.webRequest) {
8 if (typeof browser.runtime.onSyncMessage !== "object") {
9 // Background Script side
10
11 let pending = new Map();
12 if (MOZILLA) {
13 // we don't care this is async, as long as it get called before the
14 // sync XHR (we are not interested in the response on the content side)
15 browser.runtime.onMessage.addListener((m, sender) => {
16 let wrapper = m.__syncMessage__;
17 if (!wrapper) return;
18 let {id} = wrapper;
19 pending.set(id, wrapper);
20 let result;
21 let unsuspend = result => {
22 pending.delete(id);
23 if (wrapper.unsuspend) {
24 setTimeout(wrapper.unsuspend, 0);
25 }
26 return result;
27 }
28 try {
29 result = notifyListeners(JSON.stringify(wrapper.payload), sender);
30 } catch(e) {
31 unsuspend();
32 throw e;
33 }
34 console.debug("sendSyncMessage: returning", result);
35 return (result instanceof Promise ? result
36 : new Promise(resolve => resolve(result))
37 ).then(result => unsuspend(result));
38 });
39 }
40
41 let tabUrlCache = new Map();
42 let asyncResults = new Map();
43 let tabRemovalListener = null;
44 let CANCEL = {cancel: true};
45 let {TAB_ID_NONE} = browser.tabs;
46
47
48 let onBeforeRequest = request => { try {
49 let {url, tabId} = request;
50 let params = new URLSearchParams(url.split("?")[1]);
51 let msgId = params.get("id");
52 if (asyncResults.has(msgId)) {
53 return asyncRet(msgId);
54 }
55 let msg = params.get("msg");
56
57 if (MOZILLA || tabId === TAB_ID_NONE) {
58 // this shoud be a mozilla suspension request
59 return params.get("suspend") ? new Promise(resolve => {
60 if (pending.has(msgId)) {
61 let wrapper = pending.get(msgId);
62 if (!wrapper.unsuspend) {
63 wrapper.unsuspend = resolve;
64 return;
65 }
66 }
67 resolve();
68 }).then(() => ret("go on"))
69 : CANCEL; // otherwise, bail
70 }
71 // CHROME from now on
72 let documentUrl = params.get("url");
73 let {frameAncestors, frameId} = request;
74 let isTop = frameId === 0 || !!params.get("top");
75 let tabUrl = frameAncestors && frameAncestors.length
76 && frameAncestors[frameAncestors.length - 1].url;
77
78 if (!tabUrl) {
79 if (isTop) {
80 tabUrlCache.set(tabId, tabUrl = documentUrl);
81 if (!tabRemovalListener) {
82 browser.tabs.onRemoved.addListener(tabRemovalListener = tab => {
83 tabUrlCache.delete(tab.id);
84 });
85 }
86 } else {
87 tabUrl = tabUrlCache.get(tabId);
88 }
89 }
90 let sender = {
91 tab: {
92 id: tabId,
93 url: tabUrl
94 },
95 frameId,
96 url: documentUrl,
97 timeStamp: Date.now()
98 };
99
100 if (!(msg !== null && sender)) {
101 return CANCEL;
102 }
103 let result = notifyListeners(msg, sender);
104 if (result instanceof Promise) {
105
106 // On Chromium, if the promise is not resolved yet,
107 // we redirect the XHR to the same URL (hence same msgId)
108 // while the result get cached for asynchronous retrieval
109 result.then(r => {
110 asyncResults.set(msgId, result = r);
111 });
112 return asyncResults.has(msgId)
113 ? asyncRet(msgId) // promise was already resolved
114 : {redirectUrl: url.replace(
115 /&redirects=(\d+)|$/, // redirects count to avoid loop detection
116 (all, count) => `&redirects=${parseInt(count) + 1 || 1}`)};
117 }
118 return ret(result);
119 } catch(e) {
120 console.error(e);
121 return CANCEL;
122 } };
123
124 let onHeaderReceived = request => {
125 let replaced = "";
126 let {responseHeaders} = request;
127 for (let h of request.responseHeaders) {
128 if (h.name === "feature-policy") {
129 h.value = h.value.replace(/\b(sync-xhr\s+)([^*][^;]*)/g,
130 (all, m1, m2) => replaced =
131 `${m1}${m2.replace(/'none'/, '')} 'self'`
132 );
133 }
134 }
135 return replaced ? {responseHeaders} : null;
136 };
137
138 let ret = r => ({redirectUrl: `data:application/json,${JSON.stringify(r)}`})
139 let asyncRet = msgId => {
140 let result = asyncResults.get(msgId);
141 asyncResults.delete(msgId);
142 return ret(result);
143 }
144
145 let listeners = new Set();
146 function notifyListeners(msg, sender) {
147 // Just like in the async runtime.sendMessage() API,
148 // we process the listeners in order until we find a not undefined
149 // result, then we return it (or undefined if none returns anything).
150 for (let l of listeners) {
151 try {
152 let result = l(JSON.parse(msg), sender);
153 if (result !== undefined) return result;
154 } catch (e) {
155 console.error("%o processing message %o from %o", e, msg, sender);
156 }
157 }
158 }
159 browser.runtime.onSyncMessage = {
160 ENDPOINT_PREFIX,
161 addListener(l) {
162 listeners.add(l);
163 if (listeners.size === 1) {
164 browser.webRequest.onBeforeRequest.addListener(onBeforeRequest,
165 {
166 urls: [`${ENDPOINT_PREFIX}*`],
167 types: ["xmlhttprequest"]
168 },
169 ["blocking"]
170 );
171 browser.webRequest.onHeadersReceived.addListener(onHeaderReceived,
172 {
173 urls: ["<all_urls>"],
174 types: ["main_frame", "sub_frame"]
175 },
176 ["blocking", "responseHeaders"]
177 );
178 }
179 },
180 removeListener(l) {
181 listeners.remove(l);
182 if (listeners.size === 0) {
183 browser.webRequest.onBeforeRequest.removeListener(onBeforeRequest);
184 browser.webRequest.onHeadersReceived.removeListener(onHeadersReceived);
185 }
186 },
187 hasListener(l) {
188 return listeners.has(l);
189 }
190 };
191 }
192 } else if (typeof browser.runtime.sendSyncMessage !== "function") {
193 // Content Script side
194 let uuid = () => (Math.random() * Date.now()).toString(16);
195 let docUrl = document.URL;
196 browser.runtime.sendSyncMessage = (msg, callback) => {
197 let msgId = `${uuid()},${docUrl}`;
198 let url = `${ENDPOINT_PREFIX}id=${encodeURIComponent(msgId)}` +
199 `&url=${encodeURIComponent(docUrl)}`;
200 if (window.top === window) {
201 // we add top URL information because Chromium doesn't know anything
202 // about frameAncestors
203 url += "&top=true";
204 }
205 /*
206 if (document.documentElement instanceof HTMLElement && !document.head) {
207 // let's insert a head element to let userscripts work
208 document.documentElement.appendChild(document.createElement("head"));
209 }*/
210
211 if (MOZILLA) {
212 // on Firefox we first need to send an async message telling the
213 // background script about the tab ID, which does not get sent
214 // with "privileged" XHR
215 let result;
216 browser.runtime.sendMessage(
217 {__syncMessage__: {id: msgId, payload: msg}}
218 ).then(r => {
219 result = r;
220 if (callback) callback(r);
221 }).catch(e => {
222 throw e;
223 });
224
225 // In order to cope with inconsistencies in XHR synchronicity,
226 // allowing DOM element to be inserted and script to be executed
227 // (seen with file:// and ftp:// loads) we additionally suspend on
228 // Mutation notifications and beforescriptexecute events
229 let suspendURL = url + "&suspend=true";
230 let suspended = false;
231 let suspend = () => {
232 if (suspended) return;
233 suspended = true;
234 try {
235 let r = new XMLHttpRequest();
236 r.open("GET", suspendURL, false);
237 r.send(null);
238 } catch (e) {
239 console.error(e);
240 }
241 suspended = false;
242 };
243 let domSuspender = new MutationObserver(records => {
244 suspend();
245 });
246 domSuspender.observe(document.documentElement, {childList: true});
247 addEventListener("beforescriptexecute", suspend, true);
248
249 let finalize = () => {
250 removeEventListener("beforescriptexecute", suspend, true);
251 domSuspender.disconnect();
252 };
253
254 if (callback) {
255 let realCB = callback;
256 callback = r => {
257 try {
258 realCB(r);
259 } finally {
260 finalize();
261 }
262 };
263 return;
264 }
265 try {
266 suspend();
267 } finally {
268 finalize();
269 }
270 return result;
271 }
272 // then we send the payload using a privileged XHR, which is not subject
273 // to CORS but unfortunately doesn't carry any tab id except on Chromium
274
275 url += `&msg=${encodeURIComponent(JSON.stringify(msg))}`; // adding the payload
276 let r = new XMLHttpRequest();
277 let result;
278 let key = `${ENDPOINT_PREFIX}`;
279 let reloaded;
280 try {
281 reloaded = sessionStorage.getItem(key) === "reloaded";
282 if (reloaded) {
283 sessionStorage.removeItem(key);
284 console.log("Syncmessage attempt aftert reloading page.");
285 }
286 } catch (e) {
287 // we can't access sessionStorage: let's act as we've already reloaded
288 reloaded = true;
289 }
290 for (let attempts = 3; attempts-- > 0;) {
291 try {
292 r.open("GET", url, false);
293 r.send(null);
294 result = JSON.parse(r.responseText);
295 break;
296 } catch(e) {
297 console.error(`syncMessage error in ${document.URL}: ${e.message} (response ${r.responseText}, remaining attempts ${attempts})`);
298 if (attempts === 0) {
299 if (reloaded) {
300 console.log("Already reloaded or no sessionStorage, giving up.")
301 break;
302 }
303 sessionStorage.setItem(key, "reloaded");
304 if (sessionStorage.getItem(key)) {
305 stop();
306 location.reload();
307 return {};
308 } else {
309 console.error(`Cannot set sessionStorage item ${key}`);
310 }
311 }
312 }
313 }
314 if (callback) callback(result);
315 return result;
316 };
317 }
318})();