ajaxHooker_myaijarvis

ajax劫持库,支持xhr和fetch劫持。

当前为 2023-12-27 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/483208/1302155/ajaxHooker_myaijarvis.js

  1. // ==UserScript==
  2. // @name ajaxHooker_myaijarvis
  3. // @description ajax劫持库,支持xhr和fetch劫持。
  4. // @author cxxjackie
  5. // @version 1.3.3
  6. // @supportURL https://bbs.tampermonkey.net.cn/thread-3284-1-1.html
  7. // ==/UserScript==
  8. // 来源:https://scriptcat.org/zh-CN/script-show-page/637/code
  9. var ajaxHooker = function() {
  10. 'use strict';
  11. const win = window.unsafeWindow || document.defaultView || window;
  12. const toString = Object.prototype.toString;
  13. const getDescriptor = Object.getOwnPropertyDescriptor;
  14. const hookFns = [];
  15. const realXhr = win.XMLHttpRequest;
  16. const realFetch = win.fetch;
  17. const resProto = win.Response.prototype;
  18. const xhrResponses = ['response', 'responseText', 'responseXML'];
  19. const fetchResponses = ['arrayBuffer', 'blob', 'formData', 'json', 'text'];
  20. const fetchInitProps = ['method', 'headers', 'body', 'mode', 'credentials', 'cache', 'redirect',
  21. 'referrer', 'referrerPolicy', 'integrity', 'keepalive', 'signal', 'priority'];
  22. const xhrAsyncEvents = ['readystatechange', 'load', 'loadend'];
  23. let filter;
  24. function emptyFn() {}
  25. function errorFn(err) {
  26. console.error(err);
  27. }
  28. function defineProp(obj, prop, getter, setter) {
  29. Object.defineProperty(obj, prop, {
  30. configurable: true,
  31. enumerable: true,
  32. get: getter,
  33. set: setter
  34. });
  35. }
  36. function readonly(obj, prop, value = obj[prop]) {
  37. defineProp(obj, prop, () => value, emptyFn);
  38. }
  39. function writable(obj, prop, value = obj[prop]) {
  40. Object.defineProperty(obj, prop, {
  41. configurable: true,
  42. enumerable: true,
  43. writable: true,
  44. value: value
  45. });
  46. }
  47. function shouldFilter(type, url, method, async) {
  48. return filter && !filter.find(obj => {
  49. switch (true) {
  50. case obj.type && obj.type !== type:
  51. case toString.call(obj.url) === '[object String]' && !url.includes(obj.url):
  52. case toString.call(obj.url) === '[object RegExp]' && !obj.url.test(url):
  53. case obj.method && obj.method.toUpperCase() !== method.toUpperCase():
  54. case 'async' in obj && obj.async !== async:
  55. return false;
  56. }
  57. return true;
  58. });
  59. }
  60. function parseHeaders(obj) {
  61. const headers = {};
  62. switch (toString.call(obj)) {
  63. case '[object String]':
  64. for (const line of obj.trim().split(/[\r\n]+/)) {
  65. const parts = line.split(/\s*:\s*/);
  66. if (parts.length !== 2) continue;
  67. const lheader = parts[0].toLowerCase();
  68. if (lheader in headers) {
  69. headers[lheader] += ', ' + parts[1];
  70. } else {
  71. headers[lheader] = parts[1];
  72. }
  73. }
  74. return headers;
  75. case '[object Headers]':
  76. for (const [key, val] of obj) {
  77. headers[key] = val;
  78. }
  79. return headers;
  80. case '[object Object]':
  81. return {...obj};
  82. default:
  83. return headers;
  84. }
  85. }
  86. class AHRequest {
  87. constructor(request) {
  88. this.request = request;
  89. this.requestClone = {...this.request};
  90. this.response = {};
  91. }
  92. waitForHookFns() {
  93. return Promise.all(hookFns.map(fn => {
  94. try {
  95. return Promise.resolve(fn(this.request)).then(emptyFn, errorFn);
  96. } catch (err) {
  97. console.error(err);
  98. }
  99. }));
  100. }
  101. waitForResponseFn() {
  102. try {
  103. return Promise.resolve(this.request.response(this.response)).then(emptyFn, errorFn);
  104. } catch (err) {
  105. console.error(err);
  106. return Promise.resolve();
  107. }
  108. }
  109. waitForRequestKeys() {
  110. if (this.reqPromise) return this.reqPromise;
  111. const requestKeys = ['url', 'method', 'abort', 'headers', 'data'];
  112. return this.reqPromise = this.waitForHookFns().then(() => Promise.all(
  113. requestKeys.map(key => Promise.resolve(this.request[key]).then(
  114. val => this.request[key] = val,
  115. e => this.request[key] = this.requestClone[key]
  116. ))
  117. ));
  118. }
  119. waitForResponseKeys() {
  120. if (this.resPromise) return this.resPromise;
  121. const responseKeys = this.request.type === 'xhr' ? xhrResponses : fetchResponses;
  122. return this.resPromise = this.waitForResponseFn().then(() => Promise.all(
  123. responseKeys.map(key => {
  124. const descriptor = getDescriptor(this.response, key);
  125. if (descriptor && 'value' in descriptor) {
  126. return Promise.resolve(descriptor.value).then(
  127. val => this.response[key] = val,
  128. e => delete this.response[key]
  129. );
  130. } else {
  131. delete this.response[key];
  132. }
  133. })
  134. ));
  135. }
  136. }
  137. class XhrEvents {
  138. constructor() {
  139. this.events = {};
  140. }
  141. add(type, event) {
  142. if (type.startsWith('on')) {
  143. this.events[type] = typeof event === 'function' ? event : null;
  144. } else {
  145. this.events[type] = this.events[type] || new Set();
  146. this.events[type].add(event);
  147. }
  148. }
  149. remove(type, event) {
  150. if (type.startsWith('on')) {
  151. this.events[type] = null;
  152. } else {
  153. this.events[type] && this.events[type].delete(event);
  154. }
  155. }
  156. _sIP() {
  157. this.ajaxHooker_isStopped = true;
  158. }
  159. trigger(e) {
  160. if (e.ajaxHooker_isTriggered || e.ajaxHooker_isStopped) return;
  161. e.stopImmediatePropagation = this._sIP;
  162. this.events[e.type] && this.events[e.type].forEach(fn => {
  163. !e.ajaxHooker_isStopped && fn.call(e.target, e);
  164. });
  165. this.events['on' + e.type] && this.events['on' + e.type].call(e.target, e);
  166. e.ajaxHooker_isTriggered = true;
  167. }
  168. clone() {
  169. const eventsClone = new XhrEvents();
  170. for (const type in this.events) {
  171. if (type.startsWith('on')) {
  172. eventsClone.events[type] = this.events[type];
  173. } else {
  174. eventsClone.events[type] = new Set([...this.events[type]]);
  175. }
  176. }
  177. return eventsClone;
  178. }
  179. }
  180. const xhrMethods = {
  181. readyStateChange(e) {
  182. if (e.target.readyState === 4) {
  183. e.target.dispatchEvent(new CustomEvent('ajaxHooker_responseReady', {detail: e}));
  184. } else {
  185. e.target.__ajaxHooker.eventTrigger(e);
  186. }
  187. },
  188. asyncListener(e) {
  189. e.target.__ajaxHooker.eventTrigger(e);
  190. },
  191. setRequestHeader(header, value) {
  192. const ah = this.__ajaxHooker;
  193. ah.originalXhr.setRequestHeader(header, value);
  194. if (this.readyState !== 1) return;
  195. if (header in ah.headers) {
  196. ah.headers[header] += ', ' + value;
  197. } else {
  198. ah.headers[header] = value;
  199. }
  200. },
  201. addEventListener(...args) {
  202. const ah = this.__ajaxHooker;
  203. if (xhrAsyncEvents.includes(args[0])) {
  204. ah.proxyEvents.add(args[0], args[1]);
  205. } else {
  206. ah.originalXhr.addEventListener(...args);
  207. }
  208. },
  209. removeEventListener(...args) {
  210. const ah = this.__ajaxHooker;
  211. if (xhrAsyncEvents.includes(args[0])) {
  212. ah.proxyEvents.remove(args[0], args[1]);
  213. } else {
  214. ah.originalXhr.removeEventListener(...args);
  215. }
  216. },
  217. open(method, url, async = true, ...args) {
  218. const ah = this.__ajaxHooker;
  219. ah.url = url.toString();
  220. ah.method = method.toUpperCase();
  221. ah.async = !!async;
  222. ah.openArgs = args;
  223. ah.headers = {};
  224. for (const key of xhrResponses) {
  225. ah.proxyProps[key] = {
  226. get: () => {
  227. const val = ah.originalXhr[key];
  228. ah.originalXhr.dispatchEvent(new CustomEvent('ajaxHooker_readResponse', {
  229. detail: {key, val}
  230. }));
  231. return val;
  232. }
  233. };
  234. }
  235. return ah.originalXhr.open(method, url, ...args);
  236. },
  237. sendFactory(realSend) {
  238. return function(data) {
  239. const ah = this.__ajaxHooker;
  240. const xhr = ah.originalXhr;
  241. if (xhr.readyState !== 1) return realSend.call(xhr, data);
  242. ah.eventTrigger = e => ah.proxyEvents.trigger(e);
  243. if (shouldFilter('xhr', ah.url, ah.method, ah.async)) {
  244. xhr.addEventListener('ajaxHooker_responseReady', e => {
  245. ah.eventTrigger(e.detail);
  246. }, {once: true});
  247. return realSend.call(xhr, data);
  248. }
  249. const request = {
  250. type: 'xhr',
  251. url: ah.url,
  252. method: ah.method,
  253. abort: false,
  254. headers: ah.headers,
  255. data: data,
  256. response: null,
  257. async: ah.async
  258. };
  259. if (!ah.async) {
  260. const requestClone = {...request};
  261. hookFns.forEach(fn => {
  262. try {
  263. toString.call(fn) === '[object Function]' && fn(request);
  264. } catch (err) {
  265. console.error(err);
  266. }
  267. });
  268. for (const key in request) {
  269. if (toString.call(request[key]) === '[object Promise]') {
  270. request[key] = requestClone[key];
  271. }
  272. }
  273. xhr.open(request.method, request.url, ah.async, ...ah.openArgs);
  274. for (const header in request.headers) {
  275. xhr.setRequestHeader(header, request.headers[header]);
  276. }
  277. data = request.data;
  278. xhr.addEventListener('ajaxHooker_responseReady', e => {
  279. ah.eventTrigger(e.detail);
  280. }, {once: true});
  281. realSend.call(xhr, data);
  282. if (toString.call(request.response) === '[object Function]') {
  283. const response = {
  284. finalUrl: xhr.responseURL,
  285. status: xhr.status,
  286. responseHeaders: parseHeaders(xhr.getAllResponseHeaders())
  287. };
  288. for (const key of xhrResponses) {
  289. defineProp(response, key, () => {
  290. return response[key] = ah.originalXhr[key];
  291. }, val => {
  292. if (toString.call(val) !== '[object Promise]') {
  293. delete response[key];
  294. response[key] = val;
  295. }
  296. });
  297. }
  298. try {
  299. request.response(response);
  300. } catch (err) {
  301. console.error(err);
  302. }
  303. for (const key of xhrResponses) {
  304. ah.proxyProps[key] = {get: () => response[key]};
  305. };
  306. }
  307. return;
  308. }
  309. const req = new AHRequest(request);
  310. req.waitForRequestKeys().then(() => {
  311. if (request.abort) return;
  312. xhr.open(request.method, request.url, ...ah.openArgs);
  313. for (const header in request.headers) {
  314. xhr.setRequestHeader(header, request.headers[header]);
  315. }
  316. data = request.data;
  317. xhr.addEventListener('ajaxHooker_responseReady', e => {
  318. if (typeof request.response !== 'function') return ah.eventTrigger(e.detail);
  319. req.response = {
  320. finalUrl: xhr.responseURL,
  321. status: xhr.status,
  322. responseHeaders: parseHeaders(xhr.getAllResponseHeaders())
  323. };
  324. for (const key of xhrResponses) {
  325. defineProp(req.response, key, () => {
  326. return req.response[key] = ah.originalXhr[key];
  327. }, val => {
  328. delete req.response[key];
  329. req.response[key] = val;
  330. });
  331. }
  332. const resPromise = req.waitForResponseKeys().then(() => {
  333. for (const key of xhrResponses) {
  334. if (!(key in req.response)) continue;
  335. ah.proxyProps[key] = {
  336. get: () => {
  337. const val = req.response[key];
  338. xhr.dispatchEvent(new CustomEvent('ajaxHooker_readResponse', {
  339. detail: {key, val}
  340. }));
  341. return val;
  342. }
  343. };
  344. }
  345. });
  346. xhr.addEventListener('ajaxHooker_readResponse', e => {
  347. const descriptor = getDescriptor(req.response, e.detail.key);
  348. if (!descriptor || 'get' in descriptor) {
  349. req.response[e.detail.key] = e.detail.val;
  350. }
  351. });
  352. const eventsClone = ah.proxyEvents.clone();
  353. ah.eventTrigger = event => resPromise.then(() => eventsClone.trigger(event));
  354. ah.eventTrigger(e.detail);
  355. }, {once: true});
  356. realSend.call(xhr, data);
  357. });
  358. };
  359. }
  360. };
  361. function fakeXhr() {
  362. const xhr = new realXhr();
  363. let ah = xhr.__ajaxHooker;
  364. let xhrProxy = xhr;
  365. if (!ah) {
  366. const proxyEvents = new XhrEvents();
  367. ah = xhr.__ajaxHooker = {
  368. headers: {},
  369. originalXhr: xhr,
  370. proxyProps: {},
  371. proxyEvents: proxyEvents,
  372. eventTrigger: e => proxyEvents.trigger(e),
  373. toJSON: emptyFn // Converting circular structure to JSON
  374. };
  375. xhrProxy = new Proxy(xhr, {
  376. get(target, prop) {
  377. try {
  378. if (target === xhr) {
  379. if (prop in ah.proxyProps) {
  380. const descriptor = ah.proxyProps[prop];
  381. return descriptor.get ? descriptor.get() : descriptor.value;
  382. }
  383. if (typeof xhr[prop] === 'function') return xhr[prop].bind(xhr);
  384. }
  385. } catch (err) {
  386. console.error(err);
  387. }
  388. return target[prop];
  389. },
  390. set(target, prop, value) {
  391. try {
  392. if (target === xhr && prop in ah.proxyProps) {
  393. const descriptor = ah.proxyProps[prop];
  394. descriptor.set ? descriptor.set(value) : (descriptor.value = value);
  395. } else {
  396. target[prop] = value;
  397. }
  398. } catch (err) {
  399. console.error(err);
  400. }
  401. return true;
  402. }
  403. });
  404. xhr.addEventListener('readystatechange', xhrMethods.readyStateChange);
  405. xhr.addEventListener('load', xhrMethods.asyncListener);
  406. xhr.addEventListener('loadend', xhrMethods.asyncListener);
  407. for (const evt of xhrAsyncEvents) {
  408. const onEvt = 'on' + evt;
  409. ah.proxyProps[onEvt] = {
  410. get: () => proxyEvents.events[onEvt] || null,
  411. set: val => proxyEvents.add(onEvt, val)
  412. };
  413. }
  414. for (const method of ['setRequestHeader', 'addEventListener', 'removeEventListener', 'open']) {
  415. ah.proxyProps[method] = { value: xhrMethods[method] };
  416. }
  417. }
  418. ah.proxyProps.send = { value: xhrMethods.sendFactory(xhr.send) };
  419. return xhrProxy;
  420. }
  421. function hookFetchResponse(response, req) {
  422. for (const key of fetchResponses) {
  423. response[key] = () => new Promise((resolve, reject) => {
  424. if (key in req.response) return resolve(req.response[key]);
  425. resProto[key].call(response).then(res => {
  426. req.response[key] = res;
  427. req.waitForResponseKeys().then(() => {
  428. resolve(key in req.response ? req.response[key] : res);
  429. });
  430. }, reject);
  431. });
  432. }
  433. }
  434. function fakeFetch(url, options = {}) {
  435. if (!url) return realFetch.call(win, url, options);
  436. let init = {...options};
  437. if (toString.call(url) === '[object Request]') {
  438. init = {};
  439. for (const prop of fetchInitProps) init[prop] = url[prop];
  440. Object.assign(init, options);
  441. url = url.url;
  442. }
  443. url = url.toString();
  444. init.method = init.method || 'GET';
  445. init.headers = init.headers || {};
  446. if (shouldFilter('fetch', url, init.method, true)) return realFetch.call(win, url, init);
  447. const request = {
  448. type: 'fetch',
  449. url: url,
  450. method: init.method.toUpperCase(),
  451. abort: false,
  452. headers: parseHeaders(init.headers),
  453. data: init.body,
  454. response: null,
  455. async: true
  456. };
  457. const req = new AHRequest(request);
  458. return new Promise((resolve, reject) => {
  459. req.waitForRequestKeys().then(() => {
  460. if (request.abort) return reject(new DOMException('aborted', 'AbortError'));
  461. init.method = request.method;
  462. init.headers = request.headers;
  463. init.body = request.data;
  464. realFetch.call(win, request.url, init).then(response => {
  465. if (typeof request.response === 'function') {
  466. req.response = {
  467. finalUrl: response.url,
  468. status: response.status,
  469. responseHeaders: parseHeaders(response.headers)
  470. };
  471. hookFetchResponse(response, req);
  472. response.clone = () => {
  473. const resClone = resProto.clone.call(response);
  474. hookFetchResponse(resClone, req);
  475. return resClone;
  476. };
  477. }
  478. resolve(response);
  479. }, reject);
  480. }).catch(err => {
  481. console.error(err);
  482. resolve(realFetch.call(win, url, init));
  483. });
  484. });
  485. }
  486. win.XMLHttpRequest = fakeXhr;
  487. Object.keys(realXhr).forEach(key => fakeXhr[key] = realXhr[key]);
  488. fakeXhr.prototype = realXhr.prototype;
  489. win.fetch = fakeFetch;
  490. return {
  491. hook: fn => hookFns.push(fn),
  492. filter: arr => {
  493. filter = Array.isArray(arr) && arr;
  494. },
  495. protect: () => {
  496. readonly(win, 'XMLHttpRequest', fakeXhr);
  497. readonly(win, 'fetch', fakeFetch);
  498. },
  499. unhook: () => {
  500. writable(win, 'XMLHttpRequest', realXhr);
  501. writable(win, 'fetch', realFetch);
  502. }
  503. };
  504. }();