【hook】使用 Proxy 实现 fetch 的 hook

开篇废话

最近蛋疼想把 Pixiv 的动图作品给切换成视频而不是 Canvas 渲染,于是写起了油猴插件。
具体想法是检测到网页在放动图的时候把 canvas 换成 video,也就是需要检测到网页上已经有 canvas 元素后进行安排。

过程

了解相关知识

这里的 Proxy 并不是大家所熟知的那种代理,而是一个对象的代理,不要被骗了 MDN 多翻一翻,还有补点 ES6 知识大概好点:

确定劫持对象

我的X想法为:只要网页在放动图,则自动把 <canvas> 换成 <video>
所以我的劫持对象为 document.createElement,当这个函数传入 "canvas" 则抛出异常阻止 <canvas> 生成。

相关代码参考:

document.createElement = new Proxy(document.createElement, {
    apply: (target, thisArg, argArray) => {
        if (argArray[0] === 'canvas') {
            throw 'e';
        }
        const originProxy = target.apply(thisArg, argArray);
        return originProxy;
    }
});

然而在实际劫持过程中会因为后续 canvas 绘画相关过程都无法进行,触发了 Pixiv 的异常页面出现,所以是行不通的(也可以安排,但是后续 hook 感觉太多了)。

hook createElement error

并且就算行得通,在 Canvas 渲染之前还是会下载动图文件,相当于重复请求了,不太划算。
于是改用劫持请求方式来安排,在 Network 标签中查看请求,发现 Pixiv 是用 fetch 函数来 ajax 的:

这里还是确认到底要 Proxy 什么对象,可能是 XMLHttpRequest 也可能是 fetch ,而 XMLHttpRequest 在本文不再介绍。

network page

于是我们劫持 fetch,本文从这里正式开始,相关代码参考:

window.fetch = new Proxy(window.fetch, {
    apply: async (target, thisArg, argArray) => {
        // output pathname (maybe)
        console.log(argArray[0]);
        let originProxy = await target.apply(thisArg, argArray);
        return originProxy;
    }
});

测试看上去 hook 到了(有输出相关请求链接)
test fetch proxy

然后我们需要拦截掉动图的相关请求。
我们手动 fetch 一下参考 fetch 函数会返回什么结果(也可以参考 MDN

test fetch

(此部分其实可以略,只是了解下而已,因为我们暂时不需要返回真实数据,让 ta 报错就够了)
可以看到返回结果有个 status 的 object,那么我们可以直接返回 {status: 404} 来当请求是 404

参考代码:

window.fetch = new Proxy(window.fetch, {
    apply: async (target, thisArg, argArray) => {
        const pathname = argArray[0];
        if(pathname.includes('ugoira_meta')) {
            // 此处添加替换播放器代码
            replaceUgoiraPlayer();

            // 请求拦截成 404
            return {
                status: 404
            };
            // 也可以直接 throw
            // throw 'e';
        }
        let originProxy = await target.apply(thisArg, argArray);
        // 此处可以拦截后修改返回值,代码不再演示
        return originProxy;
    }
});

经过测试,动图的相关请求 404 只是中断渲染:

Proxy fetch

并不会让整个页面变成错误页面,那么就可以接着安排 <video> 标签进去(此处省略)

于是就劫持完成了。

反劫持

我们让 fetch 不可配置以及不可覆写即可简单防御。
参考代码:

Object.defineProperty(window,'fetch', {
    writable:false,
    configurable:false
});

效果:
anti method

和之前文章套路一样呢

总结

继上次 解锁 ABEMA 限制 后,又学到了一个 Proxy 方法来 hook 对象,还是挺灵活的。

然后可以拿这个添加鉴权 headers,或者代码里面藏毒让脚本小子捉摸不透请求方式来反爬虫(参考 DeepL),亦或者像我一样给一些网站附加点脚本……

总之玩法多多,局限你的只有脑洞!