上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

ajax劫持库ajaxHooker

  [复制链接]
  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    844

    回帖

    1345

    积分

    荣誉开发者

    积分
    1345

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-9-24 21:31:14 | 显示全部楼层 | 阅读模式

    如何使用? 库问题反馈 给库评分 查看代码

    本帖最后由 cxxjackie 于 2023-9-11 22:38 编辑

    一个ajax劫持库,支持xhr和fetch劫持。注意:劫持发生的时机是库引入的时候,因此脚本应运行于document-start阶段,或至少于目标请求发生之前。不同版本的ajaxHooker同时生效时可能发生冲突,相同版本则不会引发错误,但修改效果可能相互覆盖,请尽量引用最新版本的库。

    ajaxHooker.hook

    核心方法,通过一个回调函数进行劫持,每次请求发生时自动调用回调函数。可以将所有劫持放在同一回调函数中,也可以多次调用hook方法。示例:

    ajaxHooker.hook(request => {
        console.log(request);
    });

    参数request是一个对象,其包含以下属性:
    type
    只读属性。一个字符串,表明请求类型是xhr还是fetch。
    async
    只读属性。异步请求为true,同步请求为false,异步特性无法作用于同步请求
    url
    method
    请求的url和method,可以直接修改。
    abort
    是否取消请求,设置为true即可取消本次请求。
    headers
    请求头,可以直接修改。
    data
    请求携带的数据,可以直接修改。
    response
    响应内容,必须通过一个回调函数进行读取和修改。响应内容为一个对象,包含finalUrl、status、responseHeaders和被读取的响应数据,除响应数据可修改,其他属性是只读的。响应数据是哪个属性取决于哪个属性被读取,xhr可能的属性为responseresponseTextresponseXML,fetch可能的属性为arrayBufferblobformDatajsontext。在控制台输出时,xhr响应将包含所有属性,但只有被读取过的属性具有明确的值。修改对应属性即可影响读取结果,进而实现响应数据的修改。示例:

    ajaxHooker.hook(request => {
        if (request.url === 'https://www.example.com/') {
            request.response = res => {
                console.log(res);
                res.responseText += 'test';
            };
        }
    });

    异步特性

    注意:异步特性无法作用于同步请求,但同步修改仍然有效。
    你可以将以上所有可修改属性赋值为Promise,原请求将被阻塞直至Promise完成(若发生reject,数据将不会被修改),此特性可用于异步劫持。以下是一个异步修改响应数据的例子:

    ajaxHooker.hook(request => {
        request.response = res => {
            const responseText = res.responseText; // 注意保存原数据
            res.responseText = new Promise(resolve => {
                setTimeout(() => {
                    resolve(responseText + 'test');
                }, 3000);
            });
        };
    });

    也可以传入async回调函数以实现异步:

    ajaxHooker.hook(async request => {
        request.data = await modifyData(request.data);
        request.response = async res => {
            res.responseText = await modifyResponse(res.responseText);
        };
    });

    ajaxHooker.filter

    应于hook方法之前执行,此方法若尽早执行,有助于提升性能。
    为hook方法设置过滤规则,只有符合规则的请求才会触发hook。过滤规则是一个对象数组,参考下例:

    ajaxHooker.filter([
        {type: 'xhr', url: 'www.example.com', method: 'GET', async: true},
        {url: /^http/},
    ]);

    type 可选,应是xhr或fetch。
    url 可选,字符串或正则表达式,无需完全匹配。
    method 可选,不区分大小写。
    async 可选,布尔值。

    ajaxHooker.protect

    如果库劫持失败,可能是其他代码对xhr/fetch进行了二次劫持,protect方法会尝试阻止xhr和fetch被改写。应于document-start阶段尽早执行,部分网页下可能引发错误,谨慎使用。示例:

    ajaxHooker.protect();

    ajaxHooker.unhook

    将xhr和fetch恢复至劫持前的状态,调用此方法后,hook方法不再生效。示例:

    ajaxHooker.unhook();

    更新日志

    1.0.0
    初始版本
    
    1.0.1
    修复应用于阿里云盘时的一个bug。
    
    1.0.2
    对响应头中的重复字段做合并处理。
    
    1.1.0
    1.headers和data属性现在可以直接读取修改了,回调函数方式已废弃,不向下兼容。
    2.修复因原请求多次open和send引发的bug,减少多个ajaxHooker实例运行时的冲突现象。
    
    1.1.1
    处理多个ajaxHooker实例运行时的请求头冲突问题。
    
    1.2.0
    1.新增异步特性。
    2.优化错误处理。
    
    1.2.1
    1.新增async函数支持。
    2.优化xhr的劫持逻辑,以减少冲突概率。
    
    1.2.2
    1.新增filter方法。
    2.修复已知问题。
    
    1.2.3
    1.修复filter方法的一个bug。
    2.优化引用类型的响应数据读取问题。
    
    1.2.4
    1.减少对document-start的依赖。
    2.现在可以正确处理URL类型的链接了。
    
    1.3.0
    1.重构部分代码,将xhr劫持改为Proxy方式。
    2.修复已知问题。
    
    1.3.1
    修复了fetch请求的参数为Request类型时的一个bug。
    
    1.3.2
    处理异步劫持作用于同步请求时的bug,新增一个async参数。
    
    1.3.3
    修复一个小bug。
    
  • TA的每日心情
    开心
    2024-1-5 00:00
  • 签到天数: 116 天

    [LV.6]常住居民II

    26

    主题

    581

    回帖

    513

    积分

    专家

    积分
    513

    油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-9-26 14:26:53 | 显示全部楼层
    1. // ==UserScript==
    2. // [url=home.php?mod=space&uid=23356]@name[/url]         测试 ajaxHooker
    3. // @namespace    http://tampermonkey.net/
    4. // @version      0.1
    5. // @description  try to take over the world!
    6. // @author       You
    7. // [url=home.php?mod=space&uid=52134]@match[/url]        https://www.aliyundrive.com/s/*
    8. // @match        *://*/*
    9. // @icon         https://www.google.com/s2/favicons?sz=64&domain=aliyundrive.com
    10. // @require      https://scriptcat.org/lib/637/1.0.0/ajaxHooker.js
    11. // @run-at       document-start
    12. // @grant        none
    13. // ==/UserScript==

    14. (function() {
    15.     'use strict';

    16.     //ajaxHooker.protect();

    17.     ajaxHooker.hook(request => {
    18.         console.log("request", request);
    19.         request.response = value => {
    20.             console.log("value", value);
    21.             console.log("response", value.response);
    22.             console.log("responseText", value.responseText);
    23.         };
    24.     });

    25.     // Your code here...
    26. })();
    复制代码


    老师我这玩不转呀,似乎数据都堆积起来了,没有返回到页面
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    844

    回帖

    1345

    积分

    荣誉开发者

    积分
    1345

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-9-26 20:55:05 | 显示全部楼层
    脚本体验师001 发表于 2022-9-26 14:26
    老师我这玩不转呀,似乎数据都堆积起来了,没有返回到页面

    调试一下发现问题了,阿里云盘搞的骚操作,在xhr.readyState上搞了个getter,在getter里读取了响应数据(好像是为了实现一个onreadystatechange的polyfill),而我代码里劫持响应数据时会判断readyState,导致陷入死循环了。。。
    目前1.0.1已经修复,我保存了readyState的原始getter(必须在document-start阶段引入),但感觉这个方案有点治标不治本,不知道有没有什么更好的做法。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2024-1-5 00:00
  • 签到天数: 116 天

    [LV.6]常住居民II

    26

    主题

    581

    回帖

    513

    积分

    专家

    积分
    513

    油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-9-26 21:09:16 | 显示全部楼层
    cxxjackie 发表于 2022-9-26 20:55
    调试一下发现问题了,阿里云盘搞的骚操作,在xhr.readyState上搞了个getter,在getter里读取了响应数据( ...

    嗐,我大略扫过js文件里代码,印象有多处hook样字眼,骚操作哦
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2024-1-5 00:00
  • 签到天数: 116 天

    [LV.6]常住居民II

    26

    主题

    581

    回帖

    513

    积分

    专家

    积分
    513

    油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-9-26 21:18:33 | 显示全部楼层
    老师你先喝口茶歇一歇,还有一个需求
    我要改写或者说完全替换掉这个响应数据,比如请求另一个链接的响应来替换这个,这是个耗时操作
    请老师指教
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    844

    回帖

    1345

    积分

    荣誉开发者

    积分
    1345

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-9-26 21:34:21 | 显示全部楼层
    脚本体验师001 发表于 2022-9-26 21:18
    老师你先喝口茶歇一歇,还有一个需求
    我要改写或者说完全替换掉这个响应数据,比如请求另一个链接的响应来 ...

    老师不敢当,这个要看他具体监听的是哪个事件,onload还是onreadystatechange,假设是onload,在XMLHttpRequest.prototype上取得原型,然后给onload绑一个getter/setter,将其替换成劫持过的onload,让真正的onload延迟触发即可。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2024-1-5 00:00
  • 签到天数: 116 天

    [LV.6]常住居民II

    26

    主题

    581

    回帖

    513

    积分

    专家

    积分
    513

    油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-9-26 21:50:38 | 显示全部楼层
    1. // ==UserScript==
    2. // [url=home.php?mod=space&uid=23356]@name[/url]         测试 ajaxHooker
    3. // @namespace    http://tampermonkey.net/
    4. // @version      0.2
    5. // @description  try to take over the world!
    6. // @author       You
    7. // [url=home.php?mod=space&uid=52134]@match[/url]        https://www.aliyundrive.com/s/*
    8. // @match        *://*/*
    9. // @icon         https://www.google.com/s2/favicons?sz=64&domain=aliyundrive.com
    10. // @require      https://scriptcat.org/lib/637/1.0.1/ajaxHooker.js
    11. // @run-at       document-start
    12. // @grant        none
    13. // ==/UserScript==

    14. (function() {
    15.     'use strict';

    16.     //ajaxHooker.protect();

    17.     var sortByName = function (n, i) {
    18.         const a = n.name.split(".").slice(0, -1).join(".").match(/(\d+)/g);
    19.         const b = i.name.split(".").slice(0, -1).join(".").match(/(\d+)/g);
    20.         if (a && b) {
    21.             return +a[0] > +b[0] ? 1 : +b[0] > +a[0] ? -1 : +a[1] > +b[1] ? 1 : +b[1] > +a[1] ? -1 : +a[2] > +b[2] ? 1 : +b[2] > +a[2] ? -1 : 0;
    22.         }
    23.         return n > i ? 1 : i > n ? -1 : 0;
    24.     };

    25.     ajaxHooker.hook(request => {
    26.         console.log("request", request);
    27.         request.response = value => {
    28.             console.log("value", value);
    29.             console.log("response", value.response);
    30.             console.log("responseText", value.responseText);
    31.             value.response && value.response.items && value.response.items.sort(sortByName);
    32.         };
    33.     });

    34.     // Your code here...
    35. })();
    复制代码


    似乎感觉上比我在网上乱抄的代码流畅一点
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2024-1-5 00:00
  • 签到天数: 116 天

    [LV.6]常住居民II

    26

    主题

    581

    回帖

    513

    积分

    专家

    积分
    513

    油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-9-26 21:57:38 | 显示全部楼层
    突然觉得老师一动点脑筋就显得比较高级

    点评

    gg说话真好听  发表于 2023-4-3 19:30
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    844

    回帖

    1345

    积分

    荣誉开发者

    积分
    1345

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-9-26 22:36:11 | 显示全部楼层
    脚本体验师001 发表于 2022-9-26 21:50
    似乎感觉上比我在网上乱抄的代码流畅一点

    这个排序是同步的代码就可以直接改,似乎不是很耗时?还有可以加上request.url的判断,不用每个请求都去劫持响应数据。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    844

    回帖

    1345

    积分

    荣誉开发者

    积分
    1345

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2022-9-26 23:01:52 | 显示全部楼层
    脚本体验师001 发表于 2022-9-26 21:18
    老师你先喝口茶歇一歇,还有一个需求
    我要改写或者说完全替换掉这个响应数据,比如请求另一个链接的响应来 ...

    给你写了个例子,我搞错了一点,onload的原型不在XMLHttpRequest.prototype,而在XMLHttpRequestEventTarget.prototype上:
    1. const setOnload = Object.getOwnPropertyDescriptor(XMLHttpRequestEventTarget.prototype, 'onload').set;
    2. const xhrOpen = XMLHttpRequest.prototype.open;
    3. XMLHttpRequest.prototype.open = function(...args1) {
    4.     const xhr = this;
    5.     let onload;
    6.     Object.defineProperty(xhr, 'onload', {
    7.         configurable: true,
    8.         enumerable: true,
    9.         get: () => onload,
    10.         set: fn => {
    11.             onload = fn;
    12.             const fakeOnload = function(...args2) {
    13.                 setTimeout(() => {
    14.                     fn.apply(xhr, args2);
    15.                 }, 3000);
    16.             };
    17.             setOnload.call(xhr, fakeOnload);
    18.         }
    19.     });
    20.     return xhrOpen.apply(xhr, args1);
    21. };
    复制代码

    如果是fetch请求则相对更简单一点,因为fetch的响应数据本身就是个Promise。
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表