您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
inspect/intercept/modify any network requests
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/472943/1234300/Itsnotlupus%27%20MiddleMan.js
// ==UserScript== // @name Itsnotlupus' MiddleMan // @namespace Itsnotlupus Industries // @version 1.0.1 // @description inspect/intercept/modify any network requests // @author Itsnotlupus // @license MIT // ==/UserScript== const middleMan = (function(window) { /** * A small class that lets you register middleware for Fetch/XHR traffic. * */ class MiddleMan { routes = { Request: {}, Response: {} }; regexps = {}; addHook(route, {requestHandler, responseHandler}) { if (requestHandler) { this.routes.Request[route]??=[]; this.routes.Request[route].push(requestHandler); } if (responseHandler) { this.routes.Response[route]??=[]; this.routes.Response[route].push(responseHandler); } this.regexps[route]??=this.routeToRegexp(route); } removeHook(route, {requestHandler, responseHandler}) { if (requestHandler && this.routes.Request[route]?.includes(requestHandler)) { const i = this.routes.Request[route].indexOf(requestHandler); this.routes.Request[route].splice(i,1); } if (responseHandler && this.routes.Response[route]?.includes(responseHandler)) { const i = this.routes.Response[route].indexOf(responseHandler); this.routes.Response[route].splice(i,1); } } // 2 modes: start with '/' => full regexp, otherwise we only recognize '*" as a wildcard. routeToRegexp(path) { const r = path instanceof RegExp ? path : path.startsWith('/') ? path.split('/').slice(1,-1) : ['^', ...path.split(/([*])/).map((chunk, i) => i%2==0 ? chunk.replace(/([^a-zA-Z0-9])/g, "\\$1") : '.'+chunk), '$']; return new RegExp(r.join('')); } /** * Call this with a Request or a Response, and it'll loop through * each relevant hook to inspect and/or transform it. */ async process(obj) { const { constructor: type, constructor: { name } } = obj; const routes = this.routes[name], hooks = []; Object.keys(routes).forEach(k => { if (obj.url.match(this.regexps[k])) hooks.push(...routes[k]); }); for (const hook of hooks) { if (obj instanceof type) obj = await hook(obj.clone()) ?? obj; } return obj; } } // The only instance we'll need const middleMan = new MiddleMan; // A wrapper for fetch() that plugs into middleMan. const _fetch = window.fetch; async function fetch(resource, options) { const request = new Request(resource, options); const result = await middleMan.process(request); const response = result instanceof Request ? await _fetch(result) : result; return middleMan.process(response); } /** * An XMLHttpRequest polyfill written on top of fetch(). * Nothing special here, except that means XHR can get hooked through middleMan too. * * A few gotchas: * - This is not spec-compliant. In many ways. https://xhr.spec.whatwg.org/ * - xhr.upload is not implemented. we'll throw an exception if someone tries to use it. * - that "extends EventTarget" line below used to be a non-starter on Safari. Perhaps it still is. * - no test coverage. But I tried it on 2 sites and it didn't explode, so.. pretty good. */ class XMLHttpRequest extends EventTarget { #readyState; #requestOptions; #requestURL; #abortController; #timeout; #responseType; #mimeTypeOverride; #response; #responseText; #responseXML; #responseAny; #dataLengthComputable = false; #dataLoaded = 0; #dataTotal = 0; #errorEvent; UNSENT = 0; OPENED = 1; HEADERS_RECEIVED = 2; LOADING = 3; DONE = 4; static UNSENT = 0; static OPENED = 1; static HEADERS_RECEIVED = 2; static LOADING = 3; static DONE = 4; constructor() { super(); this.#readyState = 0; } get readyState() { return this.#readyState; } #assertReadyState(...validValues) { if (!validValues.includes(this.#readyState)) { throw new Error("Failed to take action on XMLHttpRequest: Invalid state."); } } #updateReadyState(value) { this.#readyState = value; this.#emitEvent("readystatechange"); } // Request setup open(method, url, async, user, password) { this.#assertReadyState(0,1); this.#requestOptions = { method: method.toString().toUpperCase(), headers: new Headers() }; this.#requestURL = url; this.#abortController = null; this.#timeout = 0; this.#responseType = ''; this.#mimeTypeOverride = null; this.#response = null; this.#responseText = ''; this.#responseAny = ''; this.#responseXML = null; this.#dataLengthComputable = false; this.#dataLoaded = 0; this.#dataTotal = 0; if (async === false) { throw new Error("Synchronous XHR is not supported."); } if (user || password) { this.#requestOptions.headers.set('Authorization', 'Basic '+btoa(`${user??''}:${password??''}`)); } this.#updateReadyState(1); } setRequestHeader(header, value) { this.#assertReadyState(1); this.#requestOptions.headers.set(header, value); } overrideMimeType(mimeType) { this.#mimeTypeOverride = mimeType; } set responseType(type) { if (!["","arraybuffer","blob","document","json","text"].includes(type)) { console.warn(`The provided value '${type}' is not a valid enum value of type XMLHttpRequestResponseType.`); return; } this.#responseType = type; } get responseType() { return this.#responseType; } set timeout(value) { const ms = isNaN(Number(value)) ? 0 : Math.floor(Number(value)); this.#timeout = value; } get timeout() { return this.#timeout; } get upload() { throw new Error("XMLHttpRequestUpload is not implemented."); } set withCredentials(flag) { this.#requestOptions.credentials = flag ? "include" : "omit"; } get withCredentials() { return this.#requestOptions.credentials !== "omit"; // "same-origin" returns true here. whatever. } async send(body = null) { this.#assertReadyState(1); this.#requestOptions.body = body instanceof Document ? body.documentElement.outerHTML : body; const request = new Request(this.#requestURL, this.#requestOptions); this.#abortController = new AbortController(); const signal = this.#abortController.signal; if (this.#timeout) { setTimeout(()=> this.#timedOut(), this.#timeout); } this.#emitEvent("loadstart"); let response; try { response = await fetch(request, { signal }); } catch (e) { return this.#error(); } this.#dataTotal = response.headers.get('content-length') ?? 0; this.#dataLengthComputable = this.#dataTotal !== 0; this.#updateReadyState(2); this.#processResponse(response); } abort() { this.#abortController?.abort(); this.#errorEvent = "abort"; } #timedOut() { this.#abortController?.abort(); this.#errorEvent = "timeout"; } #error() { // abort and timeout end up here. this.#response = new Response(''); this.#updateReadyState(4); this.#emitEvent(this.#errorEvent ?? "error"); this.#emitEvent("loadend"); this.#errorEvent = null; } async #processResponse(response) { this.#response = response; // TODO: remove all the clone() calls, I probably don't need them. // ok, maybe one clone, if we start using body.getReader() to track downloads and emit meaningful "progress" events. TODO LATER const progressResponse = response.clone(); const { size } = await progressResponse.blob(); this.#dataLoaded = size; this.#emitEvent('progress'); switch (this.#responseType) { case 'arraybuffer': try { this.#responseAny = await response.clone().arrayBuffer(); } catch { this.#responseAny = null; } break; case 'blob': try { this.#responseAny = await response.clone().blob(); } catch { this.#responseAny = null; } break; case 'document': { this.#responseText = await response.clone().text(); const mimeType = this.#mimeTypeOverride ?? this.#response.headers.get('content-type')?.split(';')[0].trim() ?? 'text/xml'; try { const parser = new DOMParser(); const doc = parser.parseFromString(this.#responseText, mimeType); this.#responseAny = this.#responseXML = doc; } catch { this.#responseAny = null; } break; } case 'json': try { this.#responseAny = await response.clone().json(); } catch { this.#responseAny = null; } break; case '': case 'text': default: this.#responseAny = this.#responseText = await response.clone().text(); break; } this.#updateReadyState(4); this.#emitEvent("load"); this.#emitEvent("loadend"); } // Response access getResponseHeader(header) { return this.#response?.headers.get(header) ?? null; } getAllResponseHeaders() { return [...this.#response?.headers.entries()??[]].map(([key,value]) => `${key}: ${value}\r\n`).join(''); } get response() { return this.#responseAny; } get responseText() { return this.#responseText; } get responseXML() { return this.#responseXML; } get responseURL() { return this.#response?.url; } get status() { return this.#response?.status ?? 0; } get statusText() { return this.#response?.statusText ?? ''; } // event dispatching resiliency async #emitEvent(name) { try { this.dispatchEvent(new ProgressEvent(name, { lengthComputable: this.#dataLengthComputable, loaded: this.#dataLoaded, total: this.#dataTotal })); } catch (e) { await 0; throw e; } } // informal event handlers #events = {}; #setEvent(name, f) { if (this.#events[name]) this.removeEventListener(name, this.#events[name]); this.#events[name] = f; this.addEventListener(name, this.#events[name]); } #getEvent(name) { return this.#events[name]; } set onabort(f) { this.#setEvent('abort', f); } get onabort() { return this.#getEvent('abort'); } set onerror(f) { this.#setEvent('error', f); } get onerror() { return this.#getEvent('error'); } set onload(f) { this.#setEvent('load', f); } get onload() { return this.#getEvent('load'); } set onloadend(f) { this.#setEvent('loadend', f); } get onloadend() { this.#getEvent('loadend'); } set onloadstart(f) { this.#setEvent('loadstart', f); } get onloadstart() { this.#getEvent('loadstart'); } set onprogress(f) { this.#setEvent('progress', f); } get onprogress() { this.#getEvent('progress'); } set onreadystatechange(f) { this.#setEvent('readystatechange', f); } get onreadystatechange() { this.#getEvent('readystatechange'); } set ontimeout(f) { this.#setEvent('timeout', f); } get ontimeout() { this.#getEvent('timeout'); } // I've got the perfect disguise.. get [Symbol.toStringTag]() { return 'XMLHttpRequest'; } static toString = ()=> 'function XMLHttpRequest() { [native code] }'; } window.XMLHttpRequest = XMLHttpRequest; window.fetch = fetch; return middleMan; })(globalThis.unsafeWindow ?? window);