如何在Chrome扩展程序中修改HTTP响应

118

能否创建一个修改 HTTP 响应体的 Chrome 扩展程序?

我查看了 Chrome 扩展程序 API,但是没有找到可以实现这个功能的接口。


1
如果您接受其他浏览器,那么Firefox支持webRequest.filterResponseData()。不幸的是,这是一个仅适用于Firefox的解决方案。 - Franklin Yu
10个回答

64

一般来说,您无法使用标准的Chrome扩展API更改HTTP请求的响应正文。

此功能正在104058:WebRequest API:允许扩展编辑响应正文中请求。点赞此问题以接收更新通知。

如果要编辑已知XMLHttpRequest的响应主体,则可以通过内容脚本注入代码覆盖默认的XMLHttpRequest构造函数,并使用自定义(全功能)构造函数重写响应,然后触发真正的事件。确保您的XMLHttpRequest对象完全符合Chrome内置的XMLHttpRequest对象,否则会破坏AJAX-heavy网站。

在其他情况下,您可以使用 chrome.webRequestchrome.declarativeWebRequest API 将请求重定向到 data:-URI。与XHR方法不同的是,您将无法获得请求的原始内容。实际上,请求永远不会到达服务器,因为重定向只能在实际请求发送之前完成。如果您重定向一个 main_frame 请求,则用户将看到 data:-URI 而不是请求的URL。


1
我认为data:-URI的想法行不通。我刚试过了,似乎CORS会阻止它。发出原始请求的页面最终会显示:“该请求被重定向到'data:text/json;,{...}',这在需要预检的跨域请求中是不允许的。” - Joe
1
@RobW,有哪些技巧或解决方案可以阻止URL变成data:text... - Pacerier
1
@Pacerier,实际上没有一个令人满意的解决方案。在我的回答中,我已经提到了使用内容脚本替换内容的选项,但除此之外,您不能“修改”响应而不更改URL。 - Rob W
我发誓,即使我们等到2050年,Google的邪恶也永远不会允许开发人员更改服务器响应。这是为了他们能够垄断Web浏览器,因为一旦他们实施了它,几乎没有成本的人就可以创建一个在Chrome上运行的替代浏览器。 - Pacerier
1
我尝试了这个解决方案。请注意,除了XMLHttpRequest之外,您可能需要覆盖fetch()。限制是浏览器对js / images / css的请求不会被拦截。 - user861746
显示剩余9条评论

32

如@Rob w所说,我已经重写了XMLHttpRequest,这是修改任何网站上的XHR请求的结果(类似于透明修改代理):

像@Rob w说的那样,我已经覆盖了XMLHttpRequest,这是修改任何网站上XHR请求的结果(就像透明修改代理一样):

var _open = XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function (method, URL) {
    var _onreadystatechange = this.onreadystatechange,
        _this = this;

    _this.onreadystatechange = function () {
        // catch only completed 'api/search/universal' requests
        if (_this.readyState === 4 && _this.status === 200 && ~URL.indexOf('api/search/universal')) {
            try {
                //////////////////////////////////////
                // THIS IS ACTIONS FOR YOUR REQUEST //
                //             EXAMPLE:             //
                //////////////////////////////////////
                var data = JSON.parse(_this.responseText); // {"fields": ["a","b"]}

                if (data.fields) {
                    data.fields.push('c','d');
                }

                // rewrite responseText
                Object.defineProperty(_this, 'responseText', {value: JSON.stringify(data)});
                /////////////// END //////////////////
            } catch (e) {}

            console.log('Caught! :)', method, URL/*, _this.responseText*/);
        }
        // call original callback
        if (_onreadystatechange) _onreadystatechange.apply(this, arguments);
    };

    // detect any onreadystatechange changing
    Object.defineProperty(this, "onreadystatechange", {
        get: function () {
            return _onreadystatechange;
        },
        set: function (value) {
            _onreadystatechange = value;
        }
    });

    return _open.apply(_this, arguments);
};

例如,Tampermonkey可以成功使用此代码在任何网站上进行任何修改 :)


2
我使用了你的代码,并在控制台上记录了捕获,但它没有改变我的应用程序(Angular)得到的响应。 - André Roggeri Campos
4
@AndréRoggeriCampos 我也遇到了同样的问题。Angular使用较新的response而不是responseText,所以你只需要将Object.defineProperty更改为使用response即可。 - jgawrych
非常感谢!它在我的 Chrome 扩展程序上可用! - Lancer.Yan
这似乎运行良好!如果我需要在我的生产网站上防止这种情况,有什么方法吗? - Kiran
无法重新定义属性:onreadystatechange。 - Cizaquita
如果您将responseText定义为response,就像这样: Object.defineProperty(_this,'response',{value: JSON.stringify(data)}),它就能正常工作。 - D0rm1nd0

32
我刚发布了一个Devtools扩展程序,可以做到这一点:)
它叫 tamper,基于 mitmproxy,它允许您查看当前选项卡所做的所有请求,修改它们并在下次刷新时提供修改后的版本。
它是一个相当早期的版本,但应该与 OS X 和 Windows 兼容。如果无法使用,请告诉我。
您可以在此处获取它 http://dutzi.github.io/tamper/ 工作原理 正如 @Xan 在下面评论中所述,该扩展通过本机消息传递与扩展mitmproxy通信。
该扩展程序使用chrome.devtools.network.onRequestFinished列出所有请求。
当您单击其中一个请求时,它将使用请求对象的getContent()方法下载其响应,然后将该响应发送到保存在本地的 Python 脚本中。
然后,它使用 call(对于OSX)或subprocess.Popen(对于Windows)在编辑器中打开文件。
这个Python脚本使用mitmproxy监听通过该代理进行的所有通信,如果它检测到请求已保存的文件,则会提供已保存的文件。
我使用Chrome的代理API(具体来说是chrome.proxy.settings.set())将PAC设置为代理设置。该PAC文件将所有通信重定向到Python脚本的代理。
mitmproxy最伟大的一点是它也可以修改HTTPS通信。所以你也可以使用它 :)

6
顺便提一句,如果您能更好地解释这里使用的技术,那将会很有帮助。 - Xan
11
有趣的 Devtools 扩展!但是 Tamper 只能修改响应的头部而不能修改响应的正文。 - Michahell
4
安装问题。 - Dmitry Pleshkov
1
我们可以使用这个来修改响应体吗? - Kiran
1
不安装 :/ 需要一个新的解决方案。 - mararn1618
显示剩余8条评论

25
是的,可以通过chrome.debugger API实现,该API允许扩展程序访问Chrome DevTools Protocol,该协议通过其网络API支持HTTP拦截和修改。 这个解决方案是由Chrome 问题 487422 的一个评论提出的: “对于任何想要一个可行替代方案的人,您可以在背景/事件页面中使用chrome.debugger来附加到您想要监听的特定选项卡(或者如果可能的话,附加到所有选项卡,我个人没有测试所有选项卡),然后使用调试协议的网络API。” “唯一的问题是,在选项卡视口的顶部会有通常的黄色条,除非用户在chrome://flags中关闭它。” 首先,将调试器附加到目标。
chrome.debugger.getTargets((targets) => {
    let target = /* Find the target. */;
    let debuggee = { targetId: target.id };

    chrome.debugger.attach(debuggee, "1.2", () => {
        // TODO
    });
});

接下来,发送Network.setRequestInterceptionEnabled命令,该命令将启用网络请求拦截:

chrome.debugger.getTargets((targets) => {
    let target = /* Find the target. */;
    let debuggee = { targetId: target.id };

    chrome.debugger.attach(debuggee, "1.2", () => {
        chrome.debugger.sendCommand(debuggee, "Network.setRequestInterceptionEnabled", { enabled: true });
    });
});

Chrome现在将开始发送Network.requestIntercepted事件。请添加监听器:

chrome.debugger.getTargets((targets) => {
    let target = /* Find the target. */;
    let debuggee = { targetId: target.id };

    chrome.debugger.attach(debuggee, "1.2", () => {
        chrome.debugger.sendCommand(debuggee, "Network.setRequestInterceptionEnabled", { enabled: true });
    });

    chrome.debugger.onEvent.addListener((source, method, params) => {
        if(source.targetId === target.id && method === "Network.requestIntercepted") {
            // TODO
        }
    });
});
在监听器中,params.request将是相应的Request对象。
使用Network.continueInterceptedRequest发送响应:
  • 将所需HTTP原始响应(包括HTTP状态行、标头等!)的base64编码作为rawResponse传递。
  • params.interceptionId作为interceptionId传递。
请注意,我完全没有测试过这些。

看起来非常有前途,尽管我现在正在尝试它(Chrome 60),但我可能错过了什么或者它仍然不可能;setRequestInterceptionEnabled方法似乎没有包含在DevTools协议v1.2中,我找不到一种方法来代替最新的(tip-of-tree)版本。 - Aioros
1
我尝试了这个解决方案,它在某种程度上起作用。如果您想修改请求,这个解决方案很好。但是,如果您想根据服务器返回的响应修改响应,则无法实现。此时没有响应。当然,您可以像作者所说的那样覆盖rawresponse字段。 - user861746
1
chrome.debugger.sendCommand(debuggee, "Network.setRequestInterceptionEnabled", { enabled: true }); 失败,显示 'Network.setRequestInterceptionEnabled' 未找到。 - Pacerier
1
@MultiplyByZer0,好的,我成功让它工作了。https://i.stack.imgur.com/n0Gff.png 然而,需要移除“topbar”,即需要设置浏览器标志并重新启动浏览器,这意味着需要一个替代的真正解决方案。 - Pacerier
Network.setRequestInterceptionEnabled已经过时,现在应该使用这个新的API: https://chromedevtools.github.io/devtools-protocol/tot/Fetch - macabeus
显示剩余2条评论

3

虽然Safari浏览器内置了此功能,但目前我发现在Chrome中最好的解决方法是使用Cypress的拦截(intercept)功能。它可以干净地让我在Chrome中对HTTP响应进行存根化(stub)。我调用cy.intercept然后cy.visit(<URL>),它就会拦截并提供一个针对所访问页面特定请求的存根响应。这里有一个例子:

cy.intercept('GET', '/myapiendpoint', {
  statusCode: 200,
  body: {
    myexamplefield: 'Example value',
  },
})
cy.visit('http://localhost:8080/mytestpage')

注意:您可能还需要配置Cypress以禁用一些Chrome特定的安全设置。


1
切换到Safari来解决我的问题真的帮了我很多,谢谢! - Joe

3
是的,你可以在Chrome扩展中修改HTTP响应。我建立了ModResponse (https://modheader.com/modresponse) 来实现这一功能。它可以记录和重放HTTP响应,修改响应内容,添加延迟,甚至使用来自不同服务器(例如来自本地主机)的HTTP响应。
它的工作原理是使用chrome.debugger API (https://developer.chrome.com/docs/extensions/reference/debugger/),该API使您可以访问Chrome DevTools Protocol (https://chromedevtools.github.io/devtools-protocol/)。然后,您可以使用Fetch Domain API (https://chromedevtools.github.io/devtools-protocol/tot/Fetch/) 拦截请求和响应,然后覆盖您想要的响应。(您也可以使用Network Domain,但它已被Fetch Domain取代)
这种方法的好处是它可以直接使用,无需安装桌面应用程序,也无需额外设置代理。但是,它会在Chrome中显示一个调试横幅(您可以向Chrome添加一个参数来隐藏它),并且设置比其他API复杂得多。
有关如何使用调试器API的示例,请查看chrome-extensions-samples: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/_archive/mv2/api/debugger/live-headers

1

通过Chrome DevTools Protocol version 1.3更改响应的版本

当您点击扩展程序的操作按钮时,该扩展程序在选项卡上调用chrome.debugger.attach()来捕获网络事件。

service-worker.js

// start on click in extension action button
chrome.action.onClicked.addListener(function (tab) {
    setupDebugger(tab)
});


function setupDebugger(tab) {
    const debuggee = {tabId: tab.id};
    // The extension calls chrome.debugger.attach() on a tab 
    // to capture network events when you click the extension's action button.
    chrome.debugger.attach(debuggee, "1.0", () => {
        chrome.debugger.sendCommand(debuggee, "Fetch.enable", {
            patterns: [{
                urlPattern: '*', requestStage: 'Response'
            }]
        });
    });
    chrome.debugger.onEvent.addListener((source, method, params) => processEvent(debuggee, tab, source, method, params))
}

async function processEvent(debuggee, tab, source, method, params) {
    let continueParams = {
        requestId: params.requestId,
    };
    if (source.tabId === debuggee.tabId) {
        if (method === "Fetch.requestPaused") {
            let request_url = new URL(params.request.url, location)
            let target_url = new URL(tab.url, location)
            // we determine that this is the request we need, you can do this through the pattern
            if (request_url.host === target_url.host && handlers.hasOwnProperty(request_url.pathname)) {

                // an example of how to get the response body in order to partially change it
                let body = await getResponseBodyJson(debuggee, continueParams)
                let new_body = body

                // create and send a new response
                continueParams.responseCode = event_params.responseStatusCode
                continueParams.responseHeaders = event_params.responseHeaders
                continueParams.body = b64EncodeUnicode(JSON.stringify(body));
                chrome.debugger.sendCommand(debuggee, 'Fetch.fulfillRequest', continueParams);

            } else {
                // if the request is not ours, let it go without changes
                chrome.debugger.sendCommand(debuggee, 'Fetch.continueRequest', continueParams);
            }
        }
    }
}

async function getResponseBodyJson(debuggee, continueParams) {
    let res = await chrome.debugger.sendCommand(debuggee, 'Fetch.getResponseBody', continueParams);
    let body = res.body
    if (res.base64Encoded) {
        body = b64DecodeUnicode(body)
    }
    body = JSON.parse(decodeURIComponent(body))
    return body
}

function b64DecodeUnicode(str) {
    // Going backwards: from bytestream, to percent-encoding, to original string.
    return decodeURIComponent(atob(str).split('').map(function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
}

function b64EncodeUnicode(str) {
    // first we use encodeURIComponent to get percent-encoded Unicode,
    // then we convert the percent encodings into raw bytes which
    // can be fed into btoa.
    return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
        function toSolidBytes(match, p1) {
            return String.fromCharCode('0x' + p1);
        }));
}

manifest.json

{
  "manifest_version": 3,
  "name": "example",
  "version": "1.0",
  "description": "",
  "background": {
      "service_worker": "service-worker.js"
  },
  "action": {},
  "permissions": [
      "activeTab",
      "tabs",
      "debugger",
  ],
  "host_permissions": [
      "https://*/*"
  ]
}

如何调试Service Worker

一个与调试器一起工作的Chrome扩展示例


你的脚本充满了谬误... - 丶 Limeー来夢 丶
它有效,有时候甚至是唯一可行的方法。 - Mikhail Razgovorov

0
原始问题是关于Chrome扩展的,但我注意到它已经分支出了不同的方法,根据那些有非Chrome扩展方法的答案的赞数。
这里有一种使用Puppeteer实现这种方式的方法。请注意originalContent行上提到的警告 - 在某些情况下,fetch的响应可能与原始响应不同。

使用Node.js:

npm install puppeteer node-fetch@2.6.7

创建这个 main.js 文件:

const puppeteer = require("puppeteer");
const fetch = require("node-fetch");

(async function() {

  const browser = await puppeteer.launch({headless:false});
  const page = await browser.newPage();
  await page.setRequestInterception(true);

  page.on('request', async (request) => {
    let url = request.url().replace(/\/$/g, ""); // remove trailing slash from urls
    console.log("REQUEST:", url);

    let originalContent = await fetch(url).then(r => r.text()); // TODO: Pass request headers here for more accurate response (still not perfect, but more likely to be the same as the "actual" response)

    if(url === "https://example.com") {
      request.respond({
        status: 200,
        contentType: 'text/html; charset=utf-8',    // For JS files: 'application/javascript; charset=utf-8'
        body: originalContent.replace(/example/gi, "TESTING123"),
      });
    } else {
      request.continue();
    }
  });

  await page.goto("https://example.com");
})();

运行它:

node main.js

使用 Deno:

安装 Deno

curl -fsSL https://deno.land/install.sh | sh # linux, mac
irm https://deno.land/install.ps1 | iex      # windows powershell

下载 Puppeteer 的 Chrome 浏览器:

PUPPETEER_PRODUCT=chrome deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts

创建这个main.js文件:
import puppeteer from "https://deno.land/x/puppeteer@16.2.0/mod.ts";
const browser = await puppeteer.launch({headless:false});
const page = await browser.newPage();
await page.setRequestInterception(true);

page.on('request', async (request) => {
  let url = request.url().replace(/\/$/g, ""); // remove trailing slash from urls
  console.log("REQUEST:", url);

  let originalContent = await fetch(url).then(r => r.text()); // TODO: Pass request headers here for more accurate response (still not perfect, but more likely to be the same as the "actual" response)

  if(url === "https://example.com") {
    request.respond({
      status: 200,
      contentType: 'text/html; charset=utf-8',    // For JS files: 'application/javascript; charset=utf-8'
      body: originalContent.replace(/example/gi, "TESTING123"),
    });
  } else {
    request.continue();
  }
});

await page.goto("https://example.com");

运行它:

deno run -A --unstable main.js

我目前遇到了一个TimeoutError,希望很快能够解决:https://github.com/lucacasonato/deno-puppeteer/issues/65


0

1
问题是关于修改正文而不是标题。 - Yourin
这样的话,它还会有调试器工具栏界面,对吧? - undefined

-3

我刚发现了这个扩展,它可以做很多其他的事情,但在浏览器中修改API响应非常有效:https://requestly.io/

按照以下步骤使其正常工作:

  1. 安装扩展程序

  2. 进入HttpRules

  3. 添加一个新规则并添加URL和响应

  4. 使用单选按钮启用规则

  5. 转到Chrome,您应该看到响应已被修改

您可以拥有多个具有不同响应的规则,并根据需要启用/禁用。不幸的是,如果URL相同,我还没有找到如何每个请求都有不同的响应。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接