在 Web Extension 中拦截请求

ErioifpudErioifpud
3 min read

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

在我印象中插件是有请求拦截相关 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 做了校验,直接修改他们会被检测出来

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

0
Subscribe to my newsletter

Read articles from Erioifpud directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Erioifpud
Erioifpud