From e097678c933f99e73bad2021ec911fc0c305e54f Mon Sep 17 00:00:00 2001 From: fofolee Date: Fri, 24 Jan 2025 18:19:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=BD=91=E7=BB=9C=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E6=8B=A6=E6=88=AA=E5=8A=9F=E8=83=BD=EF=BC=9A=E5=9C=A8?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E5=91=BD=E4=BB=A4=E4=B8=AD=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=AF=B7=E6=B1=82=E5=92=8C=E5=93=8D=E5=BA=94=E6=8B=A6?= =?UTF-8?q?=E6=88=AA=E9=80=89=E9=A1=B9=EF=BC=8C=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=8B=A6=E6=88=AA=E8=A7=84=E5=88=99=E5=92=8C?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E6=9B=BF=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/lib/quickcomposer/browser/cdp.js | 10 +- plugin/lib/quickcomposer/browser/index.js | 2 + plugin/lib/quickcomposer/browser/network.js | 240 ++++++++++++++++++++ src/js/composer/commands/browserCommands.js | 124 +++++++++- 4 files changed, 373 insertions(+), 3 deletions(-) create mode 100644 plugin/lib/quickcomposer/browser/network.js diff --git a/plugin/lib/quickcomposer/browser/cdp.js b/plugin/lib/quickcomposer/browser/cdp.js index 9565da9..02cf566 100644 --- a/plugin/lib/quickcomposer/browser/cdp.js +++ b/plugin/lib/quickcomposer/browser/cdp.js @@ -9,8 +9,13 @@ const initCDP = async (targetId) => { port, }); - const { Page, Runtime, Target, Network, Emulation, DOM } = client; - await Promise.all([Page.enable(), Runtime.enable(), DOM.enable()]); + const { Page, Runtime, Target, Network, Emulation, DOM, Fetch } = client; + await Promise.all([ + Page.enable(), + Runtime.enable(), + DOM.enable(), + Fetch.enable(), + ]); return { client, @@ -20,6 +25,7 @@ const initCDP = async (targetId) => { Network, Emulation, DOM, + Fetch, }; } catch (err) { console.log(err); diff --git a/plugin/lib/quickcomposer/browser/index.js b/plugin/lib/quickcomposer/browser/index.js index 2cf721d..c009603 100644 --- a/plugin/lib/quickcomposer/browser/index.js +++ b/plugin/lib/quickcomposer/browser/index.js @@ -6,6 +6,7 @@ const url = require("./url"); const cookie = require("./cookie"); const screenshot = require("./screenshot"); const device = require("./device"); +const network = require("./network"); module.exports = { ...url, @@ -16,4 +17,5 @@ module.exports = { ...cookie, ...screenshot, ...device, + ...network, }; diff --git a/plugin/lib/quickcomposer/browser/network.js b/plugin/lib/quickcomposer/browser/network.js new file mode 100644 index 0000000..0b37a9e --- /dev/null +++ b/plugin/lib/quickcomposer/browser/network.js @@ -0,0 +1,240 @@ +const { initCDP, cleanupCDP } = require("./cdp"); +const { searchTarget } = require("./tabs"); + +// 使用正则替换内容 +const replaceWithRegex = (content, pattern, replacement) => { + try { + const regex = new RegExp(pattern, "g"); + return content.replace(regex, replacement); + } catch (error) { + return content; + } +}; + +// 存储活动的拦截连接 +let activeInterceptions = new Map(); + +// 将对象格式的 headers 转换为数组格式 +const convertHeaders = (headers) => { + return Object.entries(headers).map(([name, value]) => ({ + name, + value: String(value), + })); +}; + +// 检查 URL 是否匹配规则 +const isUrlMatch = (url, pattern) => { + try { + const regex = new RegExp(pattern); + return regex.test(url); + } catch (error) { + return url.includes(pattern); + } +}; + +// 修改请求 +const setRequestInterception = async (tab, rules) => { + // 先清除所有拦截规则 + await clearInterception(); + + const target = await searchTarget(tab); + const client = await initCDP(target.id); + + try { + await client.Fetch.enable({ + patterns: [{ url: "*", requestStage: "Request" }], + }); + + client.Fetch.requestPaused(async ({ requestId, request }) => { + try { + let modified = null; + + for (const rule of rules) { + if (isUrlMatch(request.url, rule.url)) { + modified = { + url: rule.redirectUrl || request.url, + method: request.method, + headers: { ...request.headers }, + postData: request.postData, + }; + + if (rule.headerKey && rule.headerValue) { + modified.headers[rule.headerKey] = rule.headerValue; + } + + if (rule.pattern && rule.replacement) { + const url = new URL(modified.url); + for (const [key, value] of url.searchParams.entries()) { + const decodedValue = decodeURIComponent(value); + const newValue = replaceWithRegex( + decodedValue, + rule.pattern, + rule.replacement + ); + if (decodedValue !== newValue) { + url.searchParams.set(key, newValue); + } + } + modified.url = url.toString(); + + if (modified.postData) { + modified.postData = replaceWithRegex( + modified.postData, + rule.pattern, + rule.replacement + ); + } + } + break; + } + } + + if (modified) { + await client.Fetch.continueRequest({ + requestId, + url: modified.url, + method: modified.method, + headers: convertHeaders(modified.headers), + postData: modified.postData, + }); + } else { + await client.Fetch.continueRequest({ requestId }); + } + } catch (error) { + await client.Fetch.continueRequest({ requestId }); + } + }); + + activeInterceptions.set("request", client); + return { + success: true, + message: `设置请求拦截规则成功`, + rules, + }; + } catch (error) { + await cleanupCDP(client); + return { + success: false, + message: error.message, + }; + } +}; + +// 修改响应 +const setResponseInterception = async (tab, rules) => { + // 先清除所有拦截规则 + await clearInterception(); + + const target = await searchTarget(tab); + const client = await initCDP(target.id); + + try { + await client.Fetch.enable({ + patterns: [{ url: "*", requestStage: "Response" }], + }); + + client.Fetch.requestPaused( + async ({ requestId, request, responseHeaders, responseStatusCode }) => { + try { + const contentType = responseHeaders.find( + (h) => h.name.toLowerCase() === "content-type" + )?.value; + const isTextContent = contentType && contentType.includes("text"); + + if (!isTextContent) { + await client.Fetch.continueRequest({ requestId }); + return; + } + + let shouldIntercept = false; + let modifiedBody = null; + let modifiedStatus = responseStatusCode; + + for (const rule of rules) { + if (isUrlMatch(request.url, rule.url)) { + shouldIntercept = true; + + if (rule.statusCode) { + modifiedStatus = rule.statusCode; + } + + if (rule.pattern) { + try { + const response = await client.Fetch.getResponseBody({ + requestId, + }); + const originalBody = response.base64Encoded + ? Buffer.from(response.body, "base64").toString() + : response.body; + + if (originalBody) { + modifiedBody = replaceWithRegex( + originalBody, + rule.pattern, + rule.replacement || "" + ); + } + } catch (error) { + shouldIntercept = false; + } + } + } + } + + if (shouldIntercept && modifiedBody !== null) { + await client.Fetch.fulfillRequest({ + requestId, + responseCode: modifiedStatus, + responseHeaders, + body: Buffer.from(modifiedBody).toString("base64"), + }); + } else { + await client.Fetch.continueRequest({ requestId }); + } + } catch (error) { + await client.Fetch.continueRequest({ requestId }); + } + } + ); + + activeInterceptions.set("response", client); + return { + success: true, + message: `设置响应拦截规则成功`, + rules, + }; + } catch (error) { + await cleanupCDP(client); + return { + success: false, + message: error.message, + }; + } +}; + +// 清除所有拦截规则 +const clearInterception = async () => { + if (activeInterceptions.size === 0) { + return { + success: true, + message: `还没有设置拦截规则`, + }; + } + for (const [type, client] of activeInterceptions.entries()) { + try { + await client.Fetch.disable(); + await cleanupCDP(client); + } catch (error) {} + } + activeInterceptions.clear(); + return { + success: true, + message: `清除拦截规则成功`, + }; +}; + +module.exports = { + setRequestInterception, + setResponseInterception, + clearInterception, +}; diff --git a/src/js/composer/commands/browserCommands.js b/src/js/composer/commands/browserCommands.js index 6c049c8..3675425 100644 --- a/src/js/composer/commands/browserCommands.js +++ b/src/js/composer/commands/browserCommands.js @@ -1,5 +1,5 @@ import { newVarInputVal } from "js/composer/varInputValManager"; -import { deviceName, userAgent } from "js/options/httpOptions"; +import { deviceName, userAgent, commonHeaders } from "js/options/httpOptions"; const tabConfig = { component: "OptionEditor", @@ -625,6 +625,128 @@ export const browserCommands = { }, ], }, + { + value: "quickcomposer.browser.setRequestInterception", + label: "修改请求/响应", + icon: "network", + isAsync: true, + isAsync: true, + subCommands: [ + { + value: "quickcomposer.browser.setRequestInterception", + label: "修改请求", + icon: "upload", + config: [ + tabConfig, + { + topLabel: "拦截规则", + isCollapse: false, + component: "ArrayEditor", + icon: "rule", + columns: { + url: { + label: "要拦截的URL", + defaultValue: newVarInputVal("str"), + placeholder: "支持正则,如.*\\.baidu\\.com", + width: 12, + }, + headerKey: { + label: "要修改的请求头", + component: "VariableInput", + options: { + items: commonHeaders, + }, + width: 6, + }, + headerValue: { + label: "要修改的请求头值", + component: "VariableInput", + defaultValue: newVarInputVal("str"), + width: 6, + }, + pattern: { + label: "要修改的请求内容(body及url参数)", + defaultValue: newVarInputVal("str"), + width: 6, + placeholder: "支持正则,如(role: )[guest|user]", + }, + replacement: { + label: "替换内容", + defaultValue: newVarInputVal("str"), + width: 6, + placeholder: "支持替换符,如$1admin", + }, + redirectUrl: { + label: "重定向到指定URL", + defaultValue: newVarInputVal("str"), + width: 12, + }, + }, + }, + ], + }, + { + value: "quickcomposer.browser.setResponseInterception", + label: "修改响应", + icon: "download", + config: [ + tabConfig, + { + topLabel: "拦截规则", + isCollapse: false, + component: "ArrayEditor", + icon: "rule", + width: 12, + columns: { + url: { + label: "要拦截的URL", + defaultValue: newVarInputVal("str"), + placeholder: "支持正则,如.*\\.baidu\\.com", + width: 9, + }, + statusCode: { + label: "状态码", + component: "VariableInput", + defaultValue: 200, + options: { + items: [ + { label: "200", value: 200 }, + { label: "302", value: 302 }, + { label: "401", value: 401 }, + { label: "403", value: 403 }, + { label: "404", value: 404 }, + { label: "500", value: 500 }, + { label: "502", value: 502 }, + { label: "503", value: 503 }, + { label: "504", value: 504 }, + ], + }, + defaultValue: newVarInputVal("var", ""), + width: 3, + }, + pattern: { + label: "要修改的响应内容", + defaultValue: newVarInputVal("str"), + placeholder: "支持正则,如(role: )[guest|user]", + width: 6, + }, + replacement: { + label: "替换内容", + defaultValue: newVarInputVal("str"), + placeholder: "支持替换符,如$1admin", + width: 6, + }, + }, + }, + ], + }, + { + value: "quickcomposer.browser.clearInterception", + label: "清除所有拦截规则", + icon: "clear", + }, + ], + }, { value: "quickcomposer.browser.setDevice", label: "设备模拟",