ajaxHooker_myaijarvis

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

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.org/scripts/483208/1377351/ajaxHooker_myaijarvis.js

// ==UserScript==
// @name         ajaxHooker_myaijarvis
// @description  ajax劫持库,支持xhr和fetch劫持。
// @author       cxxjackie
// @version      1.4.1
// @supportURL   https://bbs.tampermonkey.net.cn/thread-3284-1-1.html
// ==/UserScript==

var ajaxHooker = function() {
    'use strict';
    const version = '1.4.1';
    const hookInst = {
        hookFns: [],
        filters: []
    };
    const win = window.unsafeWindow || document.defaultView || window;
    let winAh = win.__ajaxHooker;
    const resProto = win.Response.prototype;
    const xhrResponses = ['response', 'responseText', 'responseXML'];
    const fetchResponses = ['arrayBuffer', 'blob', 'formData', 'json', 'text'];
    const fetchInitProps = ['method', 'headers', 'body', 'mode', 'credentials', 'cache', 'redirect',
        'referrer', 'referrerPolicy', 'integrity', 'keepalive', 'signal', 'priority'];
    const xhrAsyncEvents = ['readystatechange', 'load', 'loadend'];
    const getType = ({}).toString.call.bind(({}).toString);
    const getDescriptor = Object.getOwnPropertyDescriptor.bind(Object);
    const emptyFn = () => {};
    const errorFn = e => console.error(e);
    function isThenable(obj) {
        return obj && ['object', 'function'].includes(typeof obj) && typeof obj.then === 'function';
    }
    function catchError(fn, ...args) {
        try {
            const result = fn(...args);
            if (isThenable(result)) return result.then(null, errorFn);
            return result;
        } catch (err) {
            console.error(err);
        }
    }
    function defineProp(obj, prop, getter, setter) {
        Object.defineProperty(obj, prop, {
            configurable: true,
            enumerable: true,
            get: getter,
            set: setter
        });
    }
    function readonly(obj, prop, value = obj[prop]) {
        defineProp(obj, prop, () => value, emptyFn);
    }
    function writable(obj, prop, value = obj[prop]) {
        Object.defineProperty(obj, prop, {
            configurable: true,
            enumerable: true,
            writable: true,
            value: value
        });
    }
    function parseHeaders(obj) {
        const headers = {};
        switch (getType(obj)) {
            case '[object String]':
                for (const line of obj.trim().split(/[\r\n]+/)) {
                    const [header, value] = line.split(/\s*:\s*/);
                    if (!header) break;
                    const lheader = header.toLowerCase();
                    headers[lheader] = lheader in headers ? `${headers[lheader]}, ${value}` : value;
                }
                break;
            case '[object Headers]':
                for (const [key, val] of obj) {
                    headers[key] = val;
                }
                break;
            case '[object Object]':
                return {...obj};
        }
        return headers;
    }
    function stopImmediatePropagation() {
        this.ajaxHooker_isStopped = true;
    }
    class SyncThenable {
        then(fn) {
            fn && fn();
            return new SyncThenable();
        }
    }
    class AHRequest {
        constructor(request) {
            this.request = request;
            this.requestClone = {...this.request};
        }
        shouldFilter(filters) {
            const {type, url, method, async} = this.request;
            return filters.length && !filters.find(obj => {
                switch (true) {
                    case obj.type && obj.type !== type:
                    case getType(obj.url) === '[object String]' && !url.includes(obj.url):
                    case getType(obj.url) === '[object RegExp]' && !obj.url.test(url):
                    case obj.method && obj.method.toUpperCase() !== method.toUpperCase():
                    case 'async' in obj && obj.async !== async:
                        return false;
                }
                return true;
            });
        }
        waitForRequestKeys() {
            const requestKeys = ['url', 'method', 'abort', 'headers', 'data'];
            if (!this.request.async) {
                win.__ajaxHooker.hookInsts.forEach(({hookFns, filters}) => {
                    if (this.shouldFilter(filters)) return;
                    hookFns.forEach(fn => {
                        if (getType(fn) === '[object Function]') catchError(fn, this.request);
                    });
                    requestKeys.forEach(key => {
                        if (isThenable(this.request[key])) this.request[key] = this.requestClone[key];
                    });
                });
                return new SyncThenable();
            }
            const promises = [];
            win.__ajaxHooker.hookInsts.forEach(({hookFns, filters}) => {
                if (this.shouldFilter(filters)) return;
                promises.push(Promise.all(hookFns.map(fn => catchError(fn, this.request))).then(() => 
                    Promise.all(requestKeys.map(key => Promise.resolve(this.request[key]).then(
                        val => this.request[key] = val,
                        () => this.request[key] = this.requestClone[key]
                    )))
                ));
            });
            return Promise.all(promises);
        }
        waitForResponseKeys(response) {
            const responseKeys = this.request.type === 'xhr' ? xhrResponses : fetchResponses;
            if (!this.request.async) {
                if (getType(this.request.response) === '[object Function]') {
                    catchError(this.request.response, response);
                    responseKeys.forEach(key => {
                        if ('get' in getDescriptor(response, key) || isThenable(response[key])) {
                            delete response[key];
                        }
                    });
                }
                return new SyncThenable();
            }
            return Promise.resolve(catchError(this.request.response, response)).then(() =>
                Promise.all(responseKeys.map(key => {
                    const descriptor = getDescriptor(response, key);
                    if (descriptor && 'value' in descriptor) {
                        return Promise.resolve(descriptor.value).then(
                            val => response[key] = val,
                            () => delete response[key]
                        );
                    } else {
                        delete response[key];
                    }
                }))
            );
        }
    }
    const proxyHandler = {
        get(target, prop) {
            const descriptor = getDescriptor(target, prop);
            if (descriptor && !descriptor.configurable && !descriptor.writable && !descriptor.get) return target[prop];
            const ah = target.__ajaxHooker;
            if (ah && ah.proxyProps) {
                if (prop in ah.proxyProps) {
                    const pDescriptor = ah.proxyProps[prop];
                    if ('get' in pDescriptor) return pDescriptor.get();
                    if (typeof pDescriptor.value === 'function') return pDescriptor.value.bind(ah);
                    return pDescriptor.value;
                }
                if (typeof target[prop] === 'function') return target[prop].bind(target);
            }
            return target[prop];
        },
        set(target, prop, value) {
            const descriptor = getDescriptor(target, prop);
            if (descriptor && !descriptor.configurable && !descriptor.writable && !descriptor.set) return true;
            const ah = target.__ajaxHooker;
            if (ah && ah.proxyProps && prop in ah.proxyProps) {
                const pDescriptor = ah.proxyProps[prop];
                pDescriptor.set ? pDescriptor.set(value) : (pDescriptor.value = value);
            } else {
                target[prop] = value;
            }
            return true;
        }
    };
    class XhrHooker {
        constructor(xhr) {
            const ah = this;
            Object.assign(ah, {
                originalXhr: xhr,
                proxyXhr: new Proxy(xhr, proxyHandler),
                resThenable: new SyncThenable(),
                proxyProps: {},
                proxyEvents: {}
            });
            xhr.addEventListener('readystatechange', e => {
                if (ah.proxyXhr.readyState === 4 && ah.request && typeof ah.request.response === 'function') {
                    const response = {
                        finalUrl: ah.proxyXhr.responseURL,
                        status: ah.proxyXhr.status,
                        responseHeaders: parseHeaders(ah.proxyXhr.getAllResponseHeaders())
                    };
                    const tempValues = {};
                    for (const key of xhrResponses) {
                        try {
                            tempValues[key] = ah.originalXhr[key];
                        } catch (err) {}
                        defineProp(response, key, () => {
                            return response[key] = tempValues[key];
                        }, val => {
                            delete response[key];
                            response[key] = val;
                        });
                    }
                    ah.resThenable = new AHRequest(ah.request).waitForResponseKeys(response).then(() => {
                        for (const key of xhrResponses) {
                            ah.proxyProps[key] = {get: () => {
                                if (!(key in response)) response[key] = tempValues[key];
                                return response[key];
                            }};
                        }
                    });
                }
                ah.dispatchEvent(e);
            });
            xhr.addEventListener('load', e => ah.dispatchEvent(e));
            xhr.addEventListener('loadend', e => ah.dispatchEvent(e));
            for (const evt of xhrAsyncEvents) {
                const onEvt = 'on' + evt;
                ah.proxyProps[onEvt] = {
                    get: () => ah.proxyEvents[onEvt] || null,
                    set: val => ah.addEvent(onEvt, val)
                };
            }
            for (const method of ['setRequestHeader', 'addEventListener', 'removeEventListener', 'open', 'send']) {
                ah.proxyProps[method] = {value: ah[method]};
            }
        }
        toJSON() {} // Converting circular structure to JSON
        addEvent(type, event) {
            if (type.startsWith('on')) {
                this.proxyEvents[type] = typeof event === 'function' ? event : null;
            } else {
                if (typeof event === 'object' && event !== null) event = event.handleEvent;
                if (typeof event !== 'function') return;
                this.proxyEvents[type] = this.proxyEvents[type] || new Set();
                this.proxyEvents[type].add(event);
            }
        }
        removeEvent(type, event) {
            if (type.startsWith('on')) {
                this.proxyEvents[type] = null;
            } else {
                if (typeof event === 'object' && event !== null) event = event.handleEvent;
                this.proxyEvents[type] && this.proxyEvents[type].delete(event);
            }
        }
        dispatchEvent(e) {
            e.stopImmediatePropagation = stopImmediatePropagation;
            defineProp(e, 'target', () => this.proxyXhr);
            this.proxyEvents[e.type] && this.proxyEvents[e.type].forEach(fn => {
                this.resThenable.then(() => !e.ajaxHooker_isStopped && fn.call(this.proxyXhr, e));
            });
            if (e.ajaxHooker_isStopped) return;
            const onEvent = this.proxyEvents['on' + e.type];
            onEvent && this.resThenable.then(onEvent.bind(this.proxyXhr, e));
        }
        setRequestHeader(header, value) {
            this.originalXhr.setRequestHeader(header, value);
            if (this.originalXhr.readyState !== 1) return;
            const headers = this.request.headers;
            headers[header] = header in headers ? `${headers[header]}, ${value}` : value;
        }
        addEventListener(...args) {
            if (xhrAsyncEvents.includes(args[0])) {
                this.addEvent(args[0], args[1]);
            } else {
                this.originalXhr.addEventListener(...args);
            }
        }
        removeEventListener(...args) {
            if (xhrAsyncEvents.includes(args[0])) {
                this.removeEvent(args[0], args[1]);
            } else {
                this.originalXhr.removeEventListener(...args);
            }
        }
        open(method, url, async = true, ...args) {
            this.request = {
                type: 'xhr',
                url: url.toString(),
                method: method.toUpperCase(),
                abort: false,
                headers: {},
                data: null,
                response: null,
                async: !!async
            };
            this.openArgs = args;
            this.resThenable = new SyncThenable();
            ['responseURL', 'readyState', 'status', 'statusText', ...xhrResponses].forEach(key => {
                delete this.proxyProps[key];
            });
            return this.originalXhr.open(method, url, async, ...args);
        }
        send(data) {
            const ah = this;
            const xhr = ah.originalXhr;
            const request = ah.request;
            if (!request) return xhr.send(data);
            request.data = data;
            new AHRequest(request).waitForRequestKeys().then(() => {
                if (request.abort) {
                    if (typeof request.response === 'function') {
                        Object.assign(ah.proxyProps, {
                            responseURL: {value: request.url},
                            readyState: {value: 4},
                            status: {value: 200},
                            statusText: {value: 'OK'}
                        });
                        xhrAsyncEvents.forEach(evt => xhr.dispatchEvent(new Event(evt)));
                    }
                } else {
                    xhr.open(request.method, request.url, request.async, ...ah.openArgs);
                    for (const header in request.headers) {
                        xhr.setRequestHeader(header, request.headers[header]);
                    }
                    xhr.send(request.data);
                }
            });
        }
    }
    function fakeXHR() {
        const xhr = new winAh.realXHR();
        if ('__ajaxHooker' in xhr) console.warn('检测到不同版本的ajaxHooker,可能发生冲突!');
        xhr.__ajaxHooker = new XhrHooker(xhr);
        return xhr.__ajaxHooker.proxyXhr;
    }
    fakeXHR.prototype = win.XMLHttpRequest.prototype;
    Object.keys(win.XMLHttpRequest).forEach(key => fakeXHR[key] = win.XMLHttpRequest[key]);
    function fakeFetch(url, options = {}) {
        if (!url) return winAh.realFetch.call(win, url, options);
        const init = {};
        if (getType(url) === '[object Request]') {
            for (const prop of fetchInitProps) init[prop] = url[prop];
            url = url.url;
        }
        url = url.toString();
        Object.assign(init, options);
        init.method = init.method || 'GET';
        init.headers = init.headers || {};
        const request = {
            type: 'fetch',
            url: url,
            method: init.method.toUpperCase(),
            abort: false,
            headers: parseHeaders(init.headers),
            data: init.body,
            response: null,
            async: true
        };
        const req = new AHRequest(request);
        return new Promise((resolve, reject) => {
            req.waitForRequestKeys().then(() => {
                if (request.abort) {
                    if (typeof request.response === 'function') {
                        const response = {
                            finalUrl: request.url,
                            status: 200,
                            responseHeaders: {}
                        };
                        req.waitForResponseKeys(response).then(() => {
                            const key = fetchResponses.find(k => k in response);
                            let val = response[key];
                            if (key === 'json' && typeof val === 'object') {
                                val = catchError(JSON.stringify.bind(JSON), val);
                            }
                            const res = new Response(val, {
                                status: 200,
                                statusText: 'OK'
                            });
                            defineProp(res, 'type', () => 'basic');
                            defineProp(res, 'url', () => request.url);
                            resolve(res);
                        });
                    } else {
                        reject(new DOMException('aborted', 'AbortError'));
                    }
                    return;
                }
                init.method = request.method;
                init.headers = request.headers;
                init.body = request.data;
                winAh.realFetch.call(win, request.url, init).then(res => {
                    if (typeof request.response === 'function') {
                        const response = {
                            finalUrl: res.url,
                            status: res.status,
                            responseHeaders: parseHeaders(res.headers)
                        };
                        fetchResponses.forEach(key => res[key] = function() {
                            if (key in response) return Promise.resolve(response[key]);
                            return resProto[key].call(this).then(val => {
                                response[key] = val;
                                return req.waitForResponseKeys(response).then(() => key in response ? response[key] : val);
                            });
                        });
                    }
                    resolve(res);
                }, reject);
            }).catch(err => {
                console.error(err);
                resolve(winAh.realFetch.call(win, url, init));
            });
        });
    }
    function fakeFetchClone() {
        const descriptors = Object.getOwnPropertyDescriptors(this);
        const res = winAh.realFetchClone.call(this);
        Object.defineProperties(res, descriptors);
        return res;
    }
    winAh = win.__ajaxHooker = winAh || {
        version, fakeXHR, fakeFetch, fakeFetchClone,
        realXHR: win.XMLHttpRequest,
        realFetch: win.fetch,
        realFetchClone: resProto.clone,
        hookInsts: new Set()
    };
    if (winAh.version !== version) console.warn('检测到不同版本的ajaxHooker,可能发生冲突!');
    win.XMLHttpRequest = winAh.fakeXHR;
    win.fetch = winAh.fakeFetch;
    resProto.clone = winAh.fakeFetchClone;
    winAh.hookInsts.add(hookInst);
    return {
        hook: fn => hookInst.hookFns.push(fn),
        filter: arr => {
            if (Array.isArray(arr)) hookInst.filters = arr;
        },
        protect: () => {
            readonly(win, 'XMLHttpRequest', winAh.fakeXHR);
            readonly(win, 'fetch', winAh.fakeFetch);
            readonly(resProto, 'clone', winAh.fakeFetchClone);
        },
        unhook: () => {
            winAh.hookInsts.delete(hookInst);
            if (!winAh.hookInsts.size) {
                writable(win, 'XMLHttpRequest', winAh.realXHR);
                writable(win, 'fetch', winAh.realFetch);
                writable(resProto, 'clone', winAh.realFetchClone);
                delete win.__ajaxHooker;
            }
        }
    };
}();