requests.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import { getUniqId } from 'src/common';
  2. import { setCache } from './cache';
  3. const requests = {};
  4. const verify = {};
  5. const specialHeaders = [
  6. 'user-agent',
  7. 'referer',
  8. 'origin',
  9. 'host',
  10. ];
  11. // const tasks = {};
  12. export function getRequestId() {
  13. const id = getUniqId();
  14. requests[id] = {
  15. id,
  16. xhr: new XMLHttpRequest(),
  17. };
  18. return id;
  19. }
  20. function xhrCallbackWrapper(req) {
  21. let lastPromise = Promise.resolve();
  22. const { xhr } = req;
  23. return evt => {
  24. const res = {
  25. id: req.id,
  26. type: evt.type,
  27. resType: xhr.responseType,
  28. };
  29. const data = {
  30. finalUrl: req.finalUrl,
  31. readyState: xhr.readyState,
  32. responseHeaders: xhr.getAllResponseHeaders(),
  33. status: xhr.status,
  34. statusText: xhr.statusText,
  35. };
  36. res.data = data;
  37. try {
  38. data.responseText = xhr.responseText;
  39. } catch (e) {
  40. // ignore if responseText is unreachable
  41. }
  42. if (evt.type === 'loadend') clearRequest(req);
  43. lastPromise = lastPromise.then(() => new Promise(resolve => {
  44. if (xhr.response && xhr.responseType === 'blob') {
  45. const reader = new FileReader();
  46. reader.onload = () => {
  47. data.response = reader.result;
  48. resolve();
  49. };
  50. reader.readAsDataURL(xhr.response);
  51. } else {
  52. // default `null` for blob and '' for text
  53. data.response = xhr.response;
  54. resolve();
  55. }
  56. }))
  57. .then(() => {
  58. if (req.cb) req.cb(res);
  59. });
  60. };
  61. }
  62. export function httpRequest(details, cb) {
  63. const req = requests[details.id];
  64. if (!req || req.cb) return;
  65. req.cb = cb;
  66. const { xhr } = req;
  67. try {
  68. xhr.open(details.method, details.url, true, details.user, details.password);
  69. xhr.setRequestHeader('VM-Verify', details.id);
  70. if (details.headers) {
  71. Object.keys(details.headers).forEach(key => {
  72. const lowerKey = key.toLowerCase();
  73. // `VM-` headers are reserved
  74. if (lowerKey.startsWith('vm-')) return;
  75. xhr.setRequestHeader(
  76. specialHeaders.includes(lowerKey) ? `VM-${key}` : key,
  77. details.headers[key],
  78. );
  79. });
  80. }
  81. if (details.responseType) xhr.responseType = 'blob';
  82. if (details.overrideMimeType) xhr.overrideMimeType(details.overrideMimeType);
  83. const callback = xhrCallbackWrapper(req);
  84. [
  85. 'abort',
  86. 'error',
  87. 'load',
  88. 'loadend',
  89. 'progress',
  90. 'readystatechange',
  91. 'timeout',
  92. ]
  93. .forEach(evt => { xhr[`on${evt}`] = callback; });
  94. req.finalUrl = details.url;
  95. xhr.send(details.data);
  96. } catch (e) {
  97. console.warn(e);
  98. }
  99. }
  100. function clearRequest(req) {
  101. if (req.coreId) delete verify[req.coreId];
  102. delete requests[req.id];
  103. }
  104. export function abortRequest(id) {
  105. const req = requests[id];
  106. if (req) {
  107. req.xhr.abort();
  108. clearRequest(req);
  109. }
  110. }
  111. // Watch URL redirects
  112. browser.webRequest.onBeforeRedirect.addListener(details => {
  113. const reqId = verify[details.requestId];
  114. if (reqId) {
  115. const req = requests[reqId];
  116. if (req) req.finalUrl = details.redirectUrl;
  117. }
  118. }, {
  119. urls: ['<all_urls>'],
  120. types: ['xmlhttprequest'],
  121. });
  122. // Modifications on headers
  123. browser.webRequest.onBeforeSendHeaders.addListener(details => {
  124. const headers = details.requestHeaders;
  125. const newHeaders = [];
  126. const vmHeaders = {};
  127. headers.forEach(header => {
  128. // if (header.name === 'VM-Task') {
  129. // tasks[details.requestId] = header.value;
  130. // } else
  131. if (header.name.startsWith('VM-')) {
  132. vmHeaders[header.name.slice(3)] = header.value;
  133. } else {
  134. newHeaders.push(header);
  135. }
  136. });
  137. const reqId = vmHeaders.Verify;
  138. if (reqId) {
  139. const req = requests[reqId];
  140. if (req) {
  141. delete vmHeaders.Verify;
  142. verify[details.requestId] = reqId;
  143. req.coreId = details.requestId;
  144. Object.keys(vmHeaders).forEach(name => {
  145. if (specialHeaders.includes(name.toLowerCase())) {
  146. newHeaders.push({ name, value: vmHeaders[name] });
  147. }
  148. });
  149. }
  150. }
  151. return { requestHeaders: newHeaders };
  152. }, {
  153. urls: ['<all_urls>'],
  154. types: ['xmlhttprequest'],
  155. }, ['blocking', 'requestHeaders']);
  156. // tasks are not necessary now, turned off
  157. // Stop redirects
  158. // browser.webRequest.onHeadersReceived.addListener(details => {
  159. // const task = tasks[details.requestId];
  160. // if (task) {
  161. // delete tasks[details.requestId];
  162. // if (task === 'Get-Location' && [301, 302, 303].includes(details.statusCode)) {
  163. // const locationHeader = details.responseHeaders.find(
  164. // header => header.name.toLowerCase() === 'location');
  165. // const base64 = locationHeader && locationHeader.value;
  166. // return {
  167. // redirectUrl: `data:text/plain;charset=utf-8,${base64 || ''}`,
  168. // };
  169. // }
  170. // }
  171. // }, {
  172. // urls: ['<all_urls>'],
  173. // types: ['xmlhttprequest'],
  174. // }, ['blocking', 'responseHeaders']);
  175. // browser.webRequest.onCompleted.addListener(details => {
  176. // delete tasks[details.requestId];
  177. // }, {
  178. // urls: ['<all_urls>'],
  179. // types: ['xmlhttprequest'],
  180. // });
  181. // browser.webRequest.onErrorOccurred.addListener(details => {
  182. // delete tasks[details.requestId];
  183. // }, {
  184. // urls: ['<all_urls>'],
  185. // types: ['xmlhttprequest'],
  186. // });
  187. browser.webRequest.onBeforeRequest.addListener(req => {
  188. // onBeforeRequest is fired for local files too
  189. if (req.method === 'GET' && /\.user\.js([?#]|$)/.test(req.url)) {
  190. // {cancel: true} will redirect to a blocked view
  191. const noredirect = { redirectUrl: 'javascript:history.back()' }; // eslint-disable-line no-script-url
  192. const x = new XMLHttpRequest();
  193. x.open('GET', req.url, false);
  194. try {
  195. x.send();
  196. } catch (e) {
  197. // Request is redirected
  198. return;
  199. }
  200. if ((!x.status || x.status === 200) && !/^\s*</.test(x.responseText)) {
  201. setCache(req.url, x.responseText);
  202. // Firefox: slashes are decoded automatically by Firefox, thus cannot be
  203. // used as separators
  204. const optionsURL = browser.runtime.getURL(browser.runtime.getManifest().options_page);
  205. const url = `${optionsURL}#confirm?u=${encodeURIComponent(req.url)}`;
  206. if (req.tabId < 0) browser.tabs.create({ url });
  207. else {
  208. browser.tabs.get(req.tabId).then(tab => {
  209. browser.tabs.create({ url: `${url}&f=${encodeURIComponent(tab.url)}` });
  210. });
  211. }
  212. return noredirect;
  213. }
  214. }
  215. }, {
  216. urls: ['<all_urls>'],
  217. types: ['main_frame'],
  218. }, ['blocking']);