Skip to main content

Command Palette

Search for a command to run...

在 Web Extension 中拦截请求

Updated
3 min read
在 Web Extension 中拦截请求

最近在开发一个浏览器插件,有个需求需要拦截指定的请求,拿到响应结果进行储存。

在我印象中插件是有请求拦截相关 API 的,问了 GPT,他信誓旦旦地和我说,可以用 WebRequest:

browser.webRequest.onBeforeRequest.addListener(details => {
  console.log('[REQUEST]', details)
}, {
  urls: ['<all_urls>'],
});

browser.webRequest.onCompleted.addListener(details => {
  console.log('[RESPONSE]', details)
}, {
  urls: ['<all_urls>'],
});

browser.webRequest.onErrorOccurred.addListener(details => {
  console.log('[ERROR]', details)
}, {
  urls: ['<all_urls>'],
});

看起来没问题,试了一下发现只能获取请求的基本信息,比如 url、参数、时间等,不行的原因也很简单,就是安全问题

虽然官方 API 不行,但这个需求还是能做的,做起来的方法也很简单粗暴,就是 hack 原有的请求 API。

XMLHttpRequest

var script = document.createElement('script');
script.textContent = '(' + function() {
    // Save the original XMLHttpRequest object
    var originalXhr = window.XMLHttpRequest;

    // Create a new XMLHttpRequest constructor
    window.XMLHttpRequest = function() {
        var xhr = new originalXhr();

        // Save the original open and send functions
        var originalOpen = xhr.open;
        var originalSend = xhr.send;

        // Override the open function to save the method and url
        xhr.open = function(method, url) {
            xhr._method = method;
            xhr._url = url;
            originalOpen.apply(this, arguments);
        };

        // Override the send function to save the request body
        xhr.send = function(body) {
            xhr._body = body;

            // When the ready state changes, log the request and response
            this.addEventListener('readystatechange', function() {
                if (this.readyState === 4) { // 4 means the request is done
                    var id = generateId(); // Generate a unique ID for this request

                    var request = {
                        method: xhr._method,
                        url: xhr._url,
                        headers: parseRequestHeaders(xhr._requestHeaders),
                        body: xhr._body
                    };

                    var response = {
                        headers: parseResponseHeaders(this.getAllResponseHeaders()),
                        body: this.responseText
                    };

                    // Save the request and response to a map
                    window._requests = window._requests || {};
                    window._requests[id] = {
                        request: request,
                        response: response
                    };

                    console.log('Request', id, request);
                    console.log('Response', id, response);
                }
            }, false);

            // Call the original send function
            originalSend.apply(this, arguments);
        };

        // Override the setRequestHeader function to save the request headers
        xhr.setRequestHeader = function(header, value) {
            xhr._requestHeaders = xhr._requestHeaders || {};
            xhr._requestHeaders[header] = value;
            originalSetRequestHeader.apply(this, arguments);
        };

        return xhr;
    };

    // Generate a unique ID for each request
    function generateId() {
        return Math.random().toString(36).substr(2);
    }

    // Parse the request headers from an object to an array
    function parseRequestHeaders(headers) {
        var result = [];
        for (var header in headers) {
            if (headers.hasOwnProperty(header)) {
                result.push({name: header, value: headers[header]});
            }
        }
        return result;
    }

    // Parse the response headers from a string to an array
    function parseResponseHeaders(headers) {
        var result = [];
        headers.trim().split(/[\\r\\n]+/).forEach(function(line) {
            var parts = line.split(': ');
            var header = parts.shift();
            var value = parts.join(': ');
            result.push({name: header, value: value});
        });
        return result;
    }
} + ')();';
document.documentElement.appendChild(script);

Fetch

var script = document.createElement('script');
script.textContent = '(' + function() {
    // Save the original fetch function
    var originalFetch = window.fetch;

    // Override the fetch function
    window.fetch = function(input, init) {
        // Generate a unique ID for this request
        var id = generateId();

        // Save the request info
        var request = {
            method: (init && init.method) || 'GET',
            url: input instanceof Request ? input.url : input,
            headers: (init && init.headers) || {},
            body: (init && init.body) || null
        };

        // Call the original fetch function
        return originalFetch.apply(this, arguments).then(function(response) {
            // When the response is ready, clone it and read its body
            var clone = response.clone();
            clone.text().then(function(body) {
                // Save the response info
                var responseInfo = {
                    headers: parseResponseHeaders(response.headers),
                    body: body
                };

                // Save the request and response to a map
                window._requests = window._requests || {};
                window._requests[id] = {
                    request: request,
                    response: responseInfo
                };

                console.log('Request', id, request);
                console.log('Response', id, responseInfo);
            });

            return response;
        });
    };

    // Generate a unique ID for each request
    function generateId() {
        return Math.random().toString(36).substr(2);
    }

    // Parse the response headers from a Headers object to an array
    function parseResponseHeaders(headers) {
        var result = [];
        headers.forEach(function(value, name) {
            result.push({name: name, value: value});
        });
        return result;
    }
} + ')();';
document.documentElement.appendChild(script);

大概是这样,直接对 XMLHttpRequestfetch 动手(也可以修改 XMLHttpRequest.prototype.open 等)。

但要注意,这样做的风险不低:

  • 有的网站自己的脚本也会修改这两个 API,插件可能破坏了网站的环境

  • 有的网站可能对一些原生 API 做了校验,直接修改他们会被检测出来

还有一个问题,这样做能不能过商店审核呢?

More from this blog

12 月装机行动记录

(Banner 图文无关) 这几天买了新的装备回来升级配置,除了显卡和散热器,其他都更新了,这两个不更新的原因是太贵了。 由于我不太懂选配置,所以还是让朋友给推荐,我说我的预算在 5000 左右,他就给我转了一个整机,配置大概是这样的: CPU:AMD 9700X 主板:微星 B650M GAMING PLUS WIFI 或者 微星 B650M GAMING WIFI 显卡:木有 内存:英睿达/宇瞻 DDR5 6000 32G 硬盘:1T NVMe PCIe4 SSD 读速 3500M...

Dec 7, 20241 min read
12 月装机行动记录

Homekit + cozylife 插座连接 HA

近期在淘宝上买了个 Homekit + cozylife 的插座,就这种: 一开始只通过 iOS 访问,就是只连接 Homekit,长按开关重置插座,iOS 一扫码就连上了,后来我嫌在外面访问不了,又不想掏钱买苹果的 HomePod,于是就装上了 Home Assistant,打算让设备们都连上 HA,这样就不用交苹果税了。 连接方式还是通过 Homekit,一般来说支持 Homekit 设备都能这样连接,先连上 iOS,然后在 Home App 中移除设备,这时候就能在 HA 中找到设备了:...

Nov 30, 20241 min read
Homekit + cozylife 插座连接 HA

找到了一台祖传的 Ccd 相机

开个玩笑,这台相机其实是我们家在 05 年的时候买的,发票都还在呢,当时花了 4000 块钱!搁现在我都受不了,更别说当年了,看到价格我都震惊了。 相机的型号是索尼的 Cybershot DSC-N1,属于小红书时尚单品 CCD 相机,由于一直放在包装盒里,现在还有 99 新呢。 机子还是正常的,能开机,能拍照,其中一个问题是日期,这款没有 WiFi 功能,所以时间只能保存在本地,不知道是不是 BUG,每次开机都让我重新设置,默认就定在 2005 年 1 月 1 日。 第二个问题是电池,电池应该...

Nov 28, 20241 min read
找到了一台祖传的 Ccd 相机

记录和 ffmpeg 与 LLM 搏斗的两天

要做的 最近在写一个制作视频的功能,就是把 N 个视频合并,然后把对应的 N 张图片,在视频开始的前 5 秒叠加显示出来。 第一口 - diffusion studio 本来我用的是 diffusion studio,这是一个 JS 库,但这玩意性能太差了,因为他要把视频每一帧都读到 canvas 里,数据一多页面就卡住了(为啥要折腾 DOM 呢?) 而且他的 API 十分不好用,作为浏览器脚本你无法读本地数据也就算了,你起码给一个接受纯数据的参数吧,比如 HTML 类型接受源代码,Image ...

Nov 16, 20242 min read
记录和 ffmpeg 与 LLM 搏斗的两天
V

void mian

39 posts