ajaxHooker_myaijarvis

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

このスクリプトは単体で利用できません。右のようなメタデータを含むスクリプトから、ライブラリとして読み込まれます: // @require https://update.greasyfork.org/scripts/483208/1377351/ajaxHooker_myaijarvis.js

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
  1. // ==UserScript==
  2. // @name ajaxHooker_myaijarvis
  3. // @description ajax劫持库,支持xhr和fetch劫持。
  4. // @author cxxjackie
  5. // @version 1.4.1
  6. // @supportURL https://bbs.tampermonkey.net.cn/thread-3284-1-1.html
  7. // ==/UserScript==
  8.  
  9. var ajaxHooker = function() {
  10. 'use strict';
  11. const version = '1.4.1';
  12. const hookInst = {
  13. hookFns: [],
  14. filters: []
  15. };
  16. const win = window.unsafeWindow || document.defaultView || window;
  17. let winAh = win.__ajaxHooker;
  18. const resProto = win.Response.prototype;
  19. const xhrResponses = ['response', 'responseText', 'responseXML'];
  20. const fetchResponses = ['arrayBuffer', 'blob', 'formData', 'json', 'text'];
  21. const fetchInitProps = ['method', 'headers', 'body', 'mode', 'credentials', 'cache', 'redirect',
  22. 'referrer', 'referrerPolicy', 'integrity', 'keepalive', 'signal', 'priority'];
  23. const xhrAsyncEvents = ['readystatechange', 'load', 'loadend'];
  24. const getType = ({}).toString.call.bind(({}).toString);
  25. const getDescriptor = Object.getOwnPropertyDescriptor.bind(Object);
  26. const emptyFn = () => {};
  27. const errorFn = e => console.error(e);
  28. function isThenable(obj) {
  29. return obj && ['object', 'function'].includes(typeof obj) && typeof obj.then === 'function';
  30. }
  31. function catchError(fn, ...args) {
  32. try {
  33. const result = fn(...args);
  34. if (isThenable(result)) return result.then(null, errorFn);
  35. return result;
  36. } catch (err) {
  37. console.error(err);
  38. }
  39. }
  40. function defineProp(obj, prop, getter, setter) {
  41. Object.defineProperty(obj, prop, {
  42. configurable: true,
  43. enumerable: true,
  44. get: getter,
  45. set: setter
  46. });
  47. }
  48. function readonly(obj, prop, value = obj[prop]) {
  49. defineProp(obj, prop, () => value, emptyFn);
  50. }
  51. function writable(obj, prop, value = obj[prop]) {
  52. Object.defineProperty(obj, prop, {
  53. configurable: true,
  54. enumerable: true,
  55. writable: true,
  56. value: value
  57. });
  58. }
  59. function parseHeaders(obj) {
  60. const headers = {};
  61. switch (getType(obj)) {
  62. case '[object String]':
  63. for (const line of obj.trim().split(/[\r\n]+/)) {
  64. const [header, value] = line.split(/\s*:\s*/);
  65. if (!header) break;
  66. const lheader = header.toLowerCase();
  67. headers[lheader] = lheader in headers ? `${headers[lheader]}, ${value}` : value;
  68. }
  69. break;
  70. case '[object Headers]':
  71. for (const [key, val] of obj) {
  72. headers[key] = val;
  73. }
  74. break;
  75. case '[object Object]':
  76. return {...obj};
  77. }
  78. return headers;
  79. }
  80. function stopImmediatePropagation() {
  81. this.ajaxHooker_isStopped = true;
  82. }
  83. class SyncThenable {
  84. then(fn) {
  85. fn && fn();
  86. return new SyncThenable();
  87. }
  88. }
  89. class AHRequest {
  90. constructor(request) {
  91. this.request = request;
  92. this.requestClone = {...this.request};
  93. }
  94. shouldFilter(filters) {
  95. const {type, url, method, async} = this.request;
  96. return filters.length && !filters.find(obj => {
  97. switch (true) {
  98. case obj.type && obj.type !== type:
  99. case getType(obj.url) === '[object String]' && !url.includes(obj.url):
  100. case getType(obj.url) === '[object RegExp]' && !obj.url.test(url):
  101. case obj.method && obj.method.toUpperCase() !== method.toUpperCase():
  102. case 'async' in obj && obj.async !== async:
  103. return false;
  104. }
  105. return true;
  106. });
  107. }
  108. waitForRequestKeys() {
  109. const requestKeys = ['url', 'method', 'abort', 'headers', 'data'];
  110. if (!this.request.async) {
  111. win.__ajaxHooker.hookInsts.forEach(({hookFns, filters}) => {
  112. if (this.shouldFilter(filters)) return;
  113. hookFns.forEach(fn => {
  114. if (getType(fn) === '[object Function]') catchError(fn, this.request);
  115. });
  116. requestKeys.forEach(key => {
  117. if (isThenable(this.request[key])) this.request[key] = this.requestClone[key];
  118. });
  119. });
  120. return new SyncThenable();
  121. }
  122. const promises = [];
  123. win.__ajaxHooker.hookInsts.forEach(({hookFns, filters}) => {
  124. if (this.shouldFilter(filters)) return;
  125. promises.push(Promise.all(hookFns.map(fn => catchError(fn, this.request))).then(() =>
  126. Promise.all(requestKeys.map(key => Promise.resolve(this.request[key]).then(
  127. val => this.request[key] = val,
  128. () => this.request[key] = this.requestClone[key]
  129. )))
  130. ));
  131. });
  132. return Promise.all(promises);
  133. }
  134. waitForResponseKeys(response) {
  135. const responseKeys = this.request.type === 'xhr' ? xhrResponses : fetchResponses;
  136. if (!this.request.async) {
  137. if (getType(this.request.response) === '[object Function]') {
  138. catchError(this.request.response, response);
  139. responseKeys.forEach(key => {
  140. if ('get' in getDescriptor(response, key) || isThenable(response[key])) {
  141. delete response[key];
  142. }
  143. });
  144. }
  145. return new SyncThenable();
  146. }
  147. return Promise.resolve(catchError(this.request.response, response)).then(() =>
  148. Promise.all(responseKeys.map(key => {
  149. const descriptor = getDescriptor(response, key);
  150. if (descriptor && 'value' in descriptor) {
  151. return Promise.resolve(descriptor.value).then(
  152. val => response[key] = val,
  153. () => delete response[key]
  154. );
  155. } else {
  156. delete response[key];
  157. }
  158. }))
  159. );
  160. }
  161. }
  162. const proxyHandler = {
  163. get(target, prop) {
  164. const descriptor = getDescriptor(target, prop);
  165. if (descriptor && !descriptor.configurable && !descriptor.writable && !descriptor.get) return target[prop];
  166. const ah = target.__ajaxHooker;
  167. if (ah && ah.proxyProps) {
  168. if (prop in ah.proxyProps) {
  169. const pDescriptor = ah.proxyProps[prop];
  170. if ('get' in pDescriptor) return pDescriptor.get();
  171. if (typeof pDescriptor.value === 'function') return pDescriptor.value.bind(ah);
  172. return pDescriptor.value;
  173. }
  174. if (typeof target[prop] === 'function') return target[prop].bind(target);
  175. }
  176. return target[prop];
  177. },
  178. set(target, prop, value) {
  179. const descriptor = getDescriptor(target, prop);
  180. if (descriptor && !descriptor.configurable && !descriptor.writable && !descriptor.set) return true;
  181. const ah = target.__ajaxHooker;
  182. if (ah && ah.proxyProps && prop in ah.proxyProps) {
  183. const pDescriptor = ah.proxyProps[prop];
  184. pDescriptor.set ? pDescriptor.set(value) : (pDescriptor.value = value);
  185. } else {
  186. target[prop] = value;
  187. }
  188. return true;
  189. }
  190. };
  191. class XhrHooker {
  192. constructor(xhr) {
  193. const ah = this;
  194. Object.assign(ah, {
  195. originalXhr: xhr,
  196. proxyXhr: new Proxy(xhr, proxyHandler),
  197. resThenable: new SyncThenable(),
  198. proxyProps: {},
  199. proxyEvents: {}
  200. });
  201. xhr.addEventListener('readystatechange', e => {
  202. if (ah.proxyXhr.readyState === 4 && ah.request && typeof ah.request.response === 'function') {
  203. const response = {
  204. finalUrl: ah.proxyXhr.responseURL,
  205. status: ah.proxyXhr.status,
  206. responseHeaders: parseHeaders(ah.proxyXhr.getAllResponseHeaders())
  207. };
  208. const tempValues = {};
  209. for (const key of xhrResponses) {
  210. try {
  211. tempValues[key] = ah.originalXhr[key];
  212. } catch (err) {}
  213. defineProp(response, key, () => {
  214. return response[key] = tempValues[key];
  215. }, val => {
  216. delete response[key];
  217. response[key] = val;
  218. });
  219. }
  220. ah.resThenable = new AHRequest(ah.request).waitForResponseKeys(response).then(() => {
  221. for (const key of xhrResponses) {
  222. ah.proxyProps[key] = {get: () => {
  223. if (!(key in response)) response[key] = tempValues[key];
  224. return response[key];
  225. }};
  226. }
  227. });
  228. }
  229. ah.dispatchEvent(e);
  230. });
  231. xhr.addEventListener('load', e => ah.dispatchEvent(e));
  232. xhr.addEventListener('loadend', e => ah.dispatchEvent(e));
  233. for (const evt of xhrAsyncEvents) {
  234. const onEvt = 'on' + evt;
  235. ah.proxyProps[onEvt] = {
  236. get: () => ah.proxyEvents[onEvt] || null,
  237. set: val => ah.addEvent(onEvt, val)
  238. };
  239. }
  240. for (const method of ['setRequestHeader', 'addEventListener', 'removeEventListener', 'open', 'send']) {
  241. ah.proxyProps[method] = {value: ah[method]};
  242. }
  243. }
  244. toJSON() {} // Converting circular structure to JSON
  245. addEvent(type, event) {
  246. if (type.startsWith('on')) {
  247. this.proxyEvents[type] = typeof event === 'function' ? event : null;
  248. } else {
  249. if (typeof event === 'object' && event !== null) event = event.handleEvent;
  250. if (typeof event !== 'function') return;
  251. this.proxyEvents[type] = this.proxyEvents[type] || new Set();
  252. this.proxyEvents[type].add(event);
  253. }
  254. }
  255. removeEvent(type, event) {
  256. if (type.startsWith('on')) {
  257. this.proxyEvents[type] = null;
  258. } else {
  259. if (typeof event === 'object' && event !== null) event = event.handleEvent;
  260. this.proxyEvents[type] && this.proxyEvents[type].delete(event);
  261. }
  262. }
  263. dispatchEvent(e) {
  264. e.stopImmediatePropagation = stopImmediatePropagation;
  265. defineProp(e, 'target', () => this.proxyXhr);
  266. this.proxyEvents[e.type] && this.proxyEvents[e.type].forEach(fn => {
  267. this.resThenable.then(() => !e.ajaxHooker_isStopped && fn.call(this.proxyXhr, e));
  268. });
  269. if (e.ajaxHooker_isStopped) return;
  270. const onEvent = this.proxyEvents['on' + e.type];
  271. onEvent && this.resThenable.then(onEvent.bind(this.proxyXhr, e));
  272. }
  273. setRequestHeader(header, value) {
  274. this.originalXhr.setRequestHeader(header, value);
  275. if (this.originalXhr.readyState !== 1) return;
  276. const headers = this.request.headers;
  277. headers[header] = header in headers ? `${headers[header]}, ${value}` : value;
  278. }
  279. addEventListener(...args) {
  280. if (xhrAsyncEvents.includes(args[0])) {
  281. this.addEvent(args[0], args[1]);
  282. } else {
  283. this.originalXhr.addEventListener(...args);
  284. }
  285. }
  286. removeEventListener(...args) {
  287. if (xhrAsyncEvents.includes(args[0])) {
  288. this.removeEvent(args[0], args[1]);
  289. } else {
  290. this.originalXhr.removeEventListener(...args);
  291. }
  292. }
  293. open(method, url, async = true, ...args) {
  294. this.request = {
  295. type: 'xhr',
  296. url: url.toString(),
  297. method: method.toUpperCase(),
  298. abort: false,
  299. headers: {},
  300. data: null,
  301. response: null,
  302. async: !!async
  303. };
  304. this.openArgs = args;
  305. this.resThenable = new SyncThenable();
  306. ['responseURL', 'readyState', 'status', 'statusText', ...xhrResponses].forEach(key => {
  307. delete this.proxyProps[key];
  308. });
  309. return this.originalXhr.open(method, url, async, ...args);
  310. }
  311. send(data) {
  312. const ah = this;
  313. const xhr = ah.originalXhr;
  314. const request = ah.request;
  315. if (!request) return xhr.send(data);
  316. request.data = data;
  317. new AHRequest(request).waitForRequestKeys().then(() => {
  318. if (request.abort) {
  319. if (typeof request.response === 'function') {
  320. Object.assign(ah.proxyProps, {
  321. responseURL: {value: request.url},
  322. readyState: {value: 4},
  323. status: {value: 200},
  324. statusText: {value: 'OK'}
  325. });
  326. xhrAsyncEvents.forEach(evt => xhr.dispatchEvent(new Event(evt)));
  327. }
  328. } else {
  329. xhr.open(request.method, request.url, request.async, ...ah.openArgs);
  330. for (const header in request.headers) {
  331. xhr.setRequestHeader(header, request.headers[header]);
  332. }
  333. xhr.send(request.data);
  334. }
  335. });
  336. }
  337. }
  338. function fakeXHR() {
  339. const xhr = new winAh.realXHR();
  340. if ('__ajaxHooker' in xhr) console.warn('检测到不同版本的ajaxHooker,可能发生冲突!');
  341. xhr.__ajaxHooker = new XhrHooker(xhr);
  342. return xhr.__ajaxHooker.proxyXhr;
  343. }
  344. fakeXHR.prototype = win.XMLHttpRequest.prototype;
  345. Object.keys(win.XMLHttpRequest).forEach(key => fakeXHR[key] = win.XMLHttpRequest[key]);
  346. function fakeFetch(url, options = {}) {
  347. if (!url) return winAh.realFetch.call(win, url, options);
  348. const init = {};
  349. if (getType(url) === '[object Request]') {
  350. for (const prop of fetchInitProps) init[prop] = url[prop];
  351. url = url.url;
  352. }
  353. url = url.toString();
  354. Object.assign(init, options);
  355. init.method = init.method || 'GET';
  356. init.headers = init.headers || {};
  357. const request = {
  358. type: 'fetch',
  359. url: url,
  360. method: init.method.toUpperCase(),
  361. abort: false,
  362. headers: parseHeaders(init.headers),
  363. data: init.body,
  364. response: null,
  365. async: true
  366. };
  367. const req = new AHRequest(request);
  368. return new Promise((resolve, reject) => {
  369. req.waitForRequestKeys().then(() => {
  370. if (request.abort) {
  371. if (typeof request.response === 'function') {
  372. const response = {
  373. finalUrl: request.url,
  374. status: 200,
  375. responseHeaders: {}
  376. };
  377. req.waitForResponseKeys(response).then(() => {
  378. const key = fetchResponses.find(k => k in response);
  379. let val = response[key];
  380. if (key === 'json' && typeof val === 'object') {
  381. val = catchError(JSON.stringify.bind(JSON), val);
  382. }
  383. const res = new Response(val, {
  384. status: 200,
  385. statusText: 'OK'
  386. });
  387. defineProp(res, 'type', () => 'basic');
  388. defineProp(res, 'url', () => request.url);
  389. resolve(res);
  390. });
  391. } else {
  392. reject(new DOMException('aborted', 'AbortError'));
  393. }
  394. return;
  395. }
  396. init.method = request.method;
  397. init.headers = request.headers;
  398. init.body = request.data;
  399. winAh.realFetch.call(win, request.url, init).then(res => {
  400. if (typeof request.response === 'function') {
  401. const response = {
  402. finalUrl: res.url,
  403. status: res.status,
  404. responseHeaders: parseHeaders(res.headers)
  405. };
  406. fetchResponses.forEach(key => res[key] = function() {
  407. if (key in response) return Promise.resolve(response[key]);
  408. return resProto[key].call(this).then(val => {
  409. response[key] = val;
  410. return req.waitForResponseKeys(response).then(() => key in response ? response[key] : val);
  411. });
  412. });
  413. }
  414. resolve(res);
  415. }, reject);
  416. }).catch(err => {
  417. console.error(err);
  418. resolve(winAh.realFetch.call(win, url, init));
  419. });
  420. });
  421. }
  422. function fakeFetchClone() {
  423. const descriptors = Object.getOwnPropertyDescriptors(this);
  424. const res = winAh.realFetchClone.call(this);
  425. Object.defineProperties(res, descriptors);
  426. return res;
  427. }
  428. winAh = win.__ajaxHooker = winAh || {
  429. version, fakeXHR, fakeFetch, fakeFetchClone,
  430. realXHR: win.XMLHttpRequest,
  431. realFetch: win.fetch,
  432. realFetchClone: resProto.clone,
  433. hookInsts: new Set()
  434. };
  435. if (winAh.version !== version) console.warn('检测到不同版本的ajaxHooker,可能发生冲突!');
  436. win.XMLHttpRequest = winAh.fakeXHR;
  437. win.fetch = winAh.fakeFetch;
  438. resProto.clone = winAh.fakeFetchClone;
  439. winAh.hookInsts.add(hookInst);
  440. return {
  441. hook: fn => hookInst.hookFns.push(fn),
  442. filter: arr => {
  443. if (Array.isArray(arr)) hookInst.filters = arr;
  444. },
  445. protect: () => {
  446. readonly(win, 'XMLHttpRequest', winAh.fakeXHR);
  447. readonly(win, 'fetch', winAh.fakeFetch);
  448. readonly(resProto, 'clone', winAh.fakeFetchClone);
  449. },
  450. unhook: () => {
  451. winAh.hookInsts.delete(hookInst);
  452. if (!winAh.hookInsts.size) {
  453. writable(win, 'XMLHttpRequest', winAh.realXHR);
  454. writable(win, 'fetch', winAh.realFetch);
  455. writable(resProto, 'clone', winAh.realFetchClone);
  456. delete win.__ajaxHooker;
  457. }
  458. }
  459. };
  460. }();