新增网络请求拦截功能:在浏览器命令中添加请求和响应拦截选项,支持自定义拦截规则和内容替换

This commit is contained in:
fofolee 2025-01-24 18:19:18 +08:00
parent 9257faf132
commit e097678c93
4 changed files with 373 additions and 3 deletions

View File

@ -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);

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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: "设备模拟",