ajaxHooker

ajax hook

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/455943/1270016/ajaxHooker.js

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址