IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    油猴脚本重写fetch和xhr请求

    windliang发表于 2022-11-17 00:01:24
    love 0

    写过几个油猴脚本,经常对页面请求返回的数据进行拦截或者覆盖,这篇文章就做个总结,涉及到 fetch 和 xhr 两种类型的请求。

    环境搭建

    先简单写个 html 页面,搭一个 koa 服务进行测试。

    html 页面提供一个 id=json 的 dom 用来加数据,后边我们补充 test.js 文件来请求接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    </head>
    <body>
    我运行了
    <div id="json"></div>
    </body>
    <script src="test.js"></script>
    </html>

    将 html 通过 VSCode 的 live-server 插件运行在 http://127.0.0.1:5500/ 上。

    image-20220823080047006

    安装 koa 和 koa-route 的 node 包,提供一个接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    const koa = require("koa");
    const app = new koa();
    const router = require("koa-router")();
    router.get("/api/query", async (ctx, next) => {
    ctx.body = {
    data: [1,2,3],
    code: 0,
    msg: "成功",
    };
    });
    // 跨域
    app.use(async (ctx, next) => {
    ctx.set("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
    ctx.set(
    "Access-Control-Allow-Headers",
    "Content-Type, Content-Length, Authorization, Accept, X-Requested-With"
    );
    ctx.set("Access-Control-Allow-Methods", "PUT, POST, GET, DELETE, OPTIONS");
    if (ctx.method === "OPTIONS") {
    ctx.body = 200;
    } else {
    await next();
    }
    });
    // 启动路由
    app.use(router.routes());
    // 设置响应头
    app.use(router.allowedMethods());

    // 监听端口
    app.listen(3002);

    提供了 /api/query 接口,返回 data: [1,2,3], 。运行在本地的 3002 端口上,并且设置跨域,允许从 http://127.0.0.1:5500 访问。

    油猴脚本

    先简单写一个插入 我是油猴脚本的文本 的脚本,后边再进行修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // ==UserScript==
    // @name 网络拦截测试
    // @namespace https://windliang.wang/
    // @version 0.1
    // @description 测试
    // @author windliang
    // @match http://127.0.0.1:5500/index.html
    // @run-at document-start
    // @grant unsafeWindow
    // ==/UserScript==

    (function () {
    console.log(window.unsafeWindow)
    const dom = document.createElement("div");
    dom.innerText = '我是油猴脚本的文本'
    document.getElementsByTagName("body")[0].append(dom);
    })();

    此时页面已经被成功拦截:

    image-20220823101447976

    这里提一句,油猴脚本如果使用 @grant 申请了权限,此时脚本会运行在一个沙箱环境中,如果想访问原始的 window 对象,可以通过 window.unsafeWindow 。

    并且我们加了 @run-at ,让脚本尽快执行。

    fetch 请求

    在 html 请求的 test.js 中添加 fetch 的代码。

    1
    2
    3
    4
    5
    6
    fetch("http://localhost:3002/api/query")
    .then((response) => response.json())
    .then((res) => {
    const dom = document.getElementById("json");
    dom.innerText = res.data;
    });

    看下页面,此时就会把 data 显示出来。

    image-20220823102924464

    如果想更改返回的数据,我们只需要在油猴脚本中重写 fetch 方法,将原数据拿到以后再返回即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // ==UserScript==
    // @name 网络拦截测试
    // @namespace https://windliang.wang/
    // @version 0.1
    // @description 测试
    // @author windliang
    // @match http://127.0.0.1:5500/index.html
    // @run-at document-start
    // @grant unsafeWindow
    // ==/UserScript==

    (function () {
    console.log(window.unsafeWindow)
    const dom = document.createElement("div");
    dom.innerText = '我是油猴脚本的文本'
    document.getElementsByTagName("body")[0].append(dom);
    const originFetch = fetch;
    console.log(originFetch)
    window.unsafeWindow.fetch = (url, options) => {
    return originFetch(url, options).then(async (response) => {
    console.log(url)
    if(url === 'http://localhost:3002/api/query'){
    const responseClone = response.clone();
    let res = await responseClone.json();
    res.data.push('油猴脚本修改数据')
    const responseNew = new Response(JSON.stringify(res), response);
    return responseNew;
    }else{
    return response;

    }
    });
    };
    })();

    对 response 的处理有点绕,当时也是试了好多次才试出了这种方案。

    做的事情就是把原来返回的 respones 复制,通过 json 方法拿到数据,进行修改数据,最后新生成一个 Response 进行返回。

    看下效果:

    image-20220823173813341

    成功修改了返回的数据。

    xhr

    我们将 fetch 改为用 xhr 发送请求,因为页面简单所以请求可能在油猴脚本重写之前就发送了,正常网站不会这么快,所以这里加一个 setTimeout 进行延时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    setTimeout(() => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://localhost:3002/api/query');
    xhr.send();
    xhr.onload = function() {
    const res = JSON.parse(this.responseText);
    const dom = document.getElementById("json");
    dom.innerText = res.data;
    };
    }, 0)

    和 fetch 的思路一样,我们可以在返回前更改 responseText 。

    重写 XMLHttpRequest 原型对象的 open 或者 send 方法,在函数内拿到用户当前的 xhr 实例,监听 readystatechange 事件,然后重写 responseText 。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const originOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (_, url) {
    if (url === "http://localhost:3002/api/query") {
    this.addEventListener("readystatechange", function () {
    if (this.readyState === 4) {
    const res = JSON.parse(this.responseText);
    res.data.push("油猴脚本修改数据");
    this.responseText = JSON.stringify(res);
    }
    });
    }
    originOpen.apply(this, arguments);
    };

    运行一下:

    image-20220824084014585

    拦截失败了,网上搜寻下答案,原因是 responseText 不是可写的,我们将原型对象上的 responseText 属性描述符打印一下。

    image-20220824084726967

    可以看到 set 属性是 undefined ,因此我们重写 responseText 失败了。

    我们无法修改原型对象上的 responseText ,我们可以在当前 xhr 对象,也就是 this 上边定义一个同名的 responseText 属性,赋值的话有两种思路。

    1、直接赋值

    我们定义一个 writable: true, 的属性,然后直接赋值为我们修改后的数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const originOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (_, url) {
    if (url === "http://localhost:3002/api/query") {
    this.addEventListener("readystatechange", function () {
    if (this.readyState === 4) {
    const res = JSON.parse(this.responseText);
    // 当前 xhr 对象上定义 responseText
    Object.defineProperty(this, "responseText", {
    writable: true,
    });
    res.data.push("油猴脚本修改数据");
    this.responseText = JSON.stringify(res);
    }
    });
    }

    originOpen.apply(this, arguments);
    };

    看下页面会发现成功拦截了:

    image-20220824085203088

    2、重写 get

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    const originOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (_, url) {
    if (url === "http://localhost:3002/api/query") {
    const xhr = this;
    const getter = Object.getOwnPropertyDescriptor(
    XMLHttpRequest.prototype,
    "response"
    ).get;
    Object.defineProperty(xhr, "responseText", {
    get: () => {
    let result = getter.call(xhr);
    try {
    const res = JSON.parse(result);
    res.data.push('油猴脚本修改数据')
    return JSON.stringify(res);
    } catch (e) {
    return result;
    }
    },
    });
    }
    originOpen.apply(this, arguments);
    };

    我们拿到原型对象的 get ,然后在当前对象上定义 responseText 的 get属性,修改数据后返回即可。

    相比于第一种方案,这种方案无需等待 readystatechange ,在开始的时候重写即可。

    需要注意的是,上边方案都只是重写了 responseText 字段,不排除有的网站读取的是 response 字段,但修改的话和上边是一样的,这里就不写了。

    总

    通过对 fetch 和 xhr 的重写,我们基本上可以对网页「为所欲为」了,发挥想象力通过油猴脚本应该可以做很多有意思的事情。



沪ICP备19023445号-2号
友情链接