ajax劫持库ajaxHooker
本帖最后由 cxxjackie 于 2024-6-14 23:14 编辑一个ajax劫持库,支持xhr和fetch劫持。**注意**:劫持发生的时机是库引入的时候,因此脚本应运行于document-start阶段,或至少于目标请求发生之前。不同版本的ajaxHooker同时生效时可能发生冲突,相同版本则不会引发错误,但修改效果可能相互覆盖。因1.4.0合并了不同脚本的ajaxHooker实例,与之前所有版本均不兼容,**请尽量引用最新版本的库,以避免与其他脚本发生冲突**。
**符合Greasy Fork规则的引用地址:**
> // @require https://scriptcat.org/lib/637/1.4.3/ajaxHooker.js#sha256=y1sWy1M/U5JP1tlAY5e80monDp27fF+GMRLsOiIrSUY=
## 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>
<summary>1.4.0</summary>
<pre>
1.重构,减少重复代码(应该没有bug...吧)。
2.对不同脚本引入的ajaxHooker实例做合并处理,以减少冲突,提高性能。
3.现在允许通过abort参数在不发生请求的情况下伪造响应值。
4.修复已知问题。
</pre>
</details>
<details>
<summary>1.4.1</summary>
<pre>
修复特殊情况下有部分请求被跳过的问题。
</pre>
</details>
<details>
<summary>1.4.2</summary>
<pre>
修复了fetch请求的参数为Request类型时body类型不正确的bug。
</pre>
</details>
<details open>
<summary>1.4.3</summary>
<pre>
1.修复特殊情况下有部分请求头丢失的问题。
2.xhr事件增加currentTarget劫持。
</pre>
</details> // ==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...
})();
老师我这玩不转呀,似乎数据都堆积起来了,没有返回到页面 脚本体验师001 发表于 2022-9-26 14:26
老师我这玩不转呀,似乎数据都堆积起来了,没有返回到页面
调试一下发现问题了,阿里云盘搞的骚操作,在xhr.readyState上搞了个getter,在getter里读取了响应数据(好像是为了实现一个onreadystatechange的polyfill),而我代码里劫持响应数据时会判断readyState,导致陷入死循环了。。。
目前1.0.1已经修复,我保存了readyState的原始getter(必须在document-start阶段引入),但感觉这个方案有点治标不治本,不知道有没有什么更好的做法。 cxxjackie 发表于 2022-9-26 20:55
调试一下发现问题了,阿里云盘搞的骚操作,在xhr.readyState上搞了个getter,在getter里读取了响应数据( ...
嗐,我大略扫过js文件里代码,印象有多处hook样字眼,骚操作哦 老师你先喝口茶歇一歇,还有一个需求
我要改写或者说完全替换掉这个响应数据,比如请求另一个链接的响应来替换这个,这是个耗时操作
请老师指教 脚本体验师001 发表于 2022-9-26 21:18
老师你先喝口茶歇一歇,还有一个需求
我要改写或者说完全替换掉这个响应数据,比如请求另一个链接的响应来 ...
老师不敢当,这个要看他具体监听的是哪个事件,onload还是onreadystatechange,假设是onload,在XMLHttpRequest.prototype上取得原型,然后给onload绑一个getter/setter,将其替换成劫持过的onload,让真正的onload延迟触发即可。 // ==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:50
似乎感觉上比我在网上乱抄的代码流畅一点
这个排序是同步的代码就可以直接改,似乎不是很耗时?还有可以加上request.url的判断,不用每个请求都去劫持响应数据。 脚本体验师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。