cxxjackie 发表于 2022-9-24 21:31:14

ajax劫持库ajaxHooker

本帖最后由 cxxjackie 于 2024-5-5 23:53 编辑

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

**符合Greasy Fork规则的引用地址:**
> // @require https://scriptcat.org/lib/637/1.4.0/ajaxHooker.js#sha256=2yxSlbNRgvhzRczZ32IEACTSHFaqFtO6VtLu769ZBdM=

## ajaxHooker.hook

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

```js
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可能的属性为`response`、`responseText`、`responseXML`,fetch可能的属性为`arrayBuffer`、`blob`、`formData`、`json`、`text`。修改对应属性即可影响读取结果,进而实现响应数据的修改。示例:

```js
ajaxHooker.hook(request => {
    if (request.url === 'https://www.example.com/') {
      request.response = res => {
            console.log(res);
            res.responseText += 'test';
      };
    }
});
```
当`abort`设置为true且`response`回调函数存在时,库将取消原请求并伪造一个成功响应,此时响应数据为空,直接对其赋值即可伪造响应结果。当你不需要原响应值时可使用此特性,以提高响应速度,减少不必要的请求。示例:
```js
ajaxHooker.hook(request => {
    if (request.url === 'https://www.example.com/') {
      resquest.abort = true;
      request.response = res => {
            // res的finalUrl、status、responseHeaders均是伪造的,其他属性不存在
            console.log(res);
            res.responseText = 'test';
      };
    }
});
```
以下情况发生时,`response`回调函数将不会被执行:
1.请求未abort且发生失败时。
2.另一个脚本引入ajaxHooker且同时修改了response,则当前回调函数可能被覆盖(取决于执行顺序)。

#### 异步特性

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

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

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

```js
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。过滤规则是一个对象数组,参考下例:

```js
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阶段尽早执行,**部分网页下可能引发错误,谨慎使用**。示例:

```js
ajaxHooker.protect();
```

## ajaxHooker.unhook

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

```js
ajaxHooker.unhook();
```

## 更新日志

<details>
<summary>1.0.0</summary>
<pre>
初始版本
</pre>
</details>
<details>
<summary>1.0.1</summary>
<pre>
修复应用于阿里云盘时的一个bug。
</pre>
</details>
<details>
<summary>1.0.2</summary>
<pre>
对响应头中的重复字段做合并处理。
</pre>
</details>
<details>
<summary>1.1.0</summary>
<pre>
1.headers和data属性现在可以直接读取修改了,回调函数方式已废弃,不向下兼容。
2.修复因原请求多次open和send引发的bug,减少多个ajaxHooker实例运行时的冲突现象。
</pre>
</details>
<details>
<summary>1.1.1</summary>
<pre>
处理多个ajaxHooker实例运行时的请求头冲突问题。
</pre>
</details>
<details>
<summary>1.2.0</summary>
<pre>
1.新增异步特性。
2.优化错误处理。
</pre>
</details>
<details>
<summary>1.2.1</summary>
<pre>
1.新增async函数支持。
2.优化xhr的劫持逻辑,以减少冲突概率。
</pre>
</details>
<details>
<summary>1.2.2</summary>
<pre>
1.新增filter方法。
2.修复已知问题。
</pre>
</details>
<details>
<summary>1.2.3</summary>
<pre>
1.修复filter方法的一个bug。
2.优化引用类型的响应数据读取问题。
</pre>
</details>
<details>
<summary>1.2.4</summary>
<pre>
1.减少对document-start的依赖。
2.现在可以正确处理URL类型的链接了。
</pre>
</details>
<details>
<summary>1.3.0</summary>
<pre>
1.重构部分代码,将xhr劫持改为Proxy方式。
2.修复已知问题。
</pre>
</details>
<details>
<summary>1.3.1</summary>
<pre>
修复了fetch请求的参数为Request类型时的一个bug。
</pre>
</details>
<details>
<summary>1.3.2</summary>
<pre>
处理异步劫持作用于同步请求时的bug,新增一个async参数。
</pre>
</details>
<details>
<summary>1.3.3</summary>
<pre>
修复一个小bug。
</pre>
</details>
<details>
<summary>1.3.4</summary>
<pre>
修复xhr请求可能意外变成同步的问题。
</pre>
</details>
<details open>
<summary>1.4.0</summary>
<pre>
1.重构,减少重复代码(应该没有bug...吧)。
2.对不同脚本引入的ajaxHooker实例做合并处理,以减少冲突,提高性能。
3.现在允许通过abort参数在不发生请求的情况下伪造响应值。
4.修复已知问题。
</pre>
</details>

脚本体验师001 发表于 2022-9-26 14:26:53

// ==UserScript==
// @name         测试 ajaxHooker
// @namespace    http://tampermonkey.net/
// @version      0.1
// @descriptiontry to take over the world!
// @author       You
// @match      https://www.aliyundrive.com/s/*
// @match      *://*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=aliyundrive.com
// @require      https://scriptcat.org/lib/637/1.0.0/ajaxHooker.js
// @run-at       document-start
// @grant      none
// ==/UserScript==

(function() {
    'use strict';

    //ajaxHooker.protect();

    ajaxHooker.hook(request => {
      console.log("request", request);
      request.response = value => {
            console.log("value", value);
            console.log("response", value.response);
            console.log("responseText", value.responseText);
      };
    });

    // Your code here...
})();

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

cxxjackie 发表于 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阶段引入),但感觉这个方案有点治标不治本,不知道有没有什么更好的做法。

脚本体验师001 发表于 2022-9-26 21:09:16

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

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

脚本体验师001 发表于 2022-9-26 21:18:33

老师你先喝口茶歇一歇,还有一个需求
我要改写或者说完全替换掉这个响应数据,比如请求另一个链接的响应来替换这个,这是个耗时操作
请老师指教

cxxjackie 发表于 2022-9-26 21:34:21

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

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

脚本体验师001 发表于 2022-9-26 21:50:38

// ==UserScript==
// @name         测试 ajaxHooker
// @namespace    http://tampermonkey.net/
// @version      0.2
// @descriptiontry to take over the world!
// @author       You
// @match      https://www.aliyundrive.com/s/*
// @match      *://*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=aliyundrive.com
// @require      https://scriptcat.org/lib/637/1.0.1/ajaxHooker.js
// @run-at       document-start
// @grant      none
// ==/UserScript==

(function() {
    'use strict';

    //ajaxHooker.protect();

    var sortByName = function (n, i) {
      const a = n.name.split(".").slice(0, -1).join(".").match(/(\d+)/g);
      const b = i.name.split(".").slice(0, -1).join(".").match(/(\d+)/g);
      if (a && b) {
            return +a > +b ? 1 : +b > +a ? -1 : +a > +b ? 1 : +b > +a ? -1 : +a > +b ? 1 : +b > +a ? -1 : 0;
      }
      return n > i ? 1 : i > n ? -1 : 0;
    };

    ajaxHooker.hook(request => {
      console.log("request", request);
      request.response = value => {
            console.log("value", value);
            console.log("response", value.response);
            console.log("responseText", value.responseText);
            value.response && value.response.items && value.response.items.sort(sortByName);
      };
    });

    // Your code here...
})();

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

脚本体验师001 发表于 2022-9-26 21:57:38

突然觉得老师一动点脑筋就显得比较高级

cxxjackie 发表于 2022-9-26 22:36:11

脚本体验师001 发表于 2022-9-26 21:50
似乎感觉上比我在网上乱抄的代码流畅一点

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

cxxjackie 发表于 2022-9-26 23:01:52

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

给你写了个例子,我搞错了一点,onload的原型不在XMLHttpRequest.prototype,而在XMLHttpRequestEventTarget.prototype上:
const setOnload = Object.getOwnPropertyDescriptor(XMLHttpRequestEventTarget.prototype, 'onload').set;
const xhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(...args1) {
    const xhr = this;
    let onload;
    Object.defineProperty(xhr, 'onload', {
      configurable: true,
      enumerable: true,
      get: () => onload,
      set: fn => {
            onload = fn;
            const fakeOnload = function(...args2) {
                setTimeout(() => {
                  fn.apply(xhr, args2);
                }, 3000);
            };
            setOnload.call(xhr, fakeOnload);
      }
    });
    return xhrOpen.apply(xhr, args1);
};
如果是fetch请求则相对更简单一点,因为fetch的响应数据本身就是个Promise。
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: ajax劫持库ajaxHooker