ajaxHooking

ajaxHooker

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

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