refactor: HTTP checker 质量加固
- failure actual 截断格式改为 …(共 N 字符),标量不序列化直接返回 - 新增 redos.ts 实现 ReDoS 静态检测(嵌套量词/重叠交替),启动期拒绝危险正则 - JSON body rules 共享同一次 JSON.parse 结果,避免重复解析 - checkCssRule 重构为线性流程,消除 exist:true 与无 operator 的冗余分支 - extract checkEarlyTimeout 辅助函数,明确提前 duration 检查意图 - 补充 303/307/308 重定向、相对路径 Location、混合 body rules 集成测试
This commit is contained in:
@@ -67,13 +67,17 @@ describe("HttpChecker", () => {
|
||||
|
||||
beforeAll(() => {
|
||||
server = Bun.serve({
|
||||
fetch(req) {
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
switch (url.pathname) {
|
||||
case "/echo":
|
||||
return new Response(JSON.stringify({ body: req.body ? "present" : "empty", method: req.method }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
case "/echo-actual":
|
||||
return new Response(JSON.stringify({ body: await req.text(), method: req.method }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
case "/gbk": {
|
||||
const gbkBytes = new Uint8Array([0xc4, 0xe3, 0xba, 0xc3]);
|
||||
return new Response(gbkBytes, { headers: { "content-type": "text/plain; charset=gbk" } });
|
||||
@@ -88,6 +92,10 @@ describe("HttpChecker", () => {
|
||||
});
|
||||
case "/large":
|
||||
return new Response("x".repeat(2000));
|
||||
case "/mixed":
|
||||
return new Response(JSON.stringify({ html: "<span>OK</span>", status: "ok" }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
case "/notfound":
|
||||
return new Response("not found", { status: 404 });
|
||||
case "/ok":
|
||||
@@ -96,6 +104,12 @@ describe("HttpChecker", () => {
|
||||
});
|
||||
case "/redirect":
|
||||
return new Response(null, { headers: { location: `${baseUrl}/ok` }, status: 301 });
|
||||
case "/redirect-303":
|
||||
return new Response(null, { headers: { location: `${baseUrl}/echo-actual` }, status: 303 });
|
||||
case "/redirect-307":
|
||||
return new Response(null, { headers: { location: `${baseUrl}/echo-actual` }, status: 307 });
|
||||
case "/redirect-308":
|
||||
return new Response(null, { headers: { location: `${baseUrl}/echo-actual` }, status: 308 });
|
||||
case "/redirect-chain-1":
|
||||
return new Response(null, { headers: { location: `${baseUrl}/redirect-chain-2` }, status: 302 });
|
||||
case "/redirect-chain-2":
|
||||
@@ -109,6 +123,8 @@ describe("HttpChecker", () => {
|
||||
}
|
||||
case "/redirect-post":
|
||||
return new Response(null, { headers: { location: `${baseUrl}/echo` }, status: 301 });
|
||||
case "/redirect-relative":
|
||||
return new Response(null, { headers: { location: "/ok" }, status: 302 });
|
||||
case "/slow-body":
|
||||
return new Response("x".repeat(2000));
|
||||
case "/unknown-charset": {
|
||||
@@ -529,6 +545,74 @@ describe("HttpChecker", () => {
|
||||
expect(result.matched).toBe(true);
|
||||
});
|
||||
|
||||
test("303 重定向将 method 转为 GET 且清空 body", async () => {
|
||||
const result = await checker.execute(
|
||||
makeTarget({
|
||||
body: "payload",
|
||||
expect: {
|
||||
body: [{ json: { equals: "GET", path: "$.method" } }, { json: { equals: "", path: "$.body" } }],
|
||||
status: [200],
|
||||
},
|
||||
headers: { "content-type": "text/plain" },
|
||||
maxRedirects: 1,
|
||||
method: "POST",
|
||||
url: `${baseUrl}/redirect-303`,
|
||||
}),
|
||||
makeCtx(),
|
||||
);
|
||||
expect(result.matched).toBe(true);
|
||||
});
|
||||
|
||||
test("307/308 重定向保持原始 method 和 body", async () => {
|
||||
for (const statusCode of [307, 308]) {
|
||||
const result = await checker.execute(
|
||||
makeTarget({
|
||||
body: `payload-${statusCode}`,
|
||||
expect: {
|
||||
body: [
|
||||
{ json: { equals: "POST", path: "$.method" } },
|
||||
{ json: { equals: `payload-${statusCode}`, path: "$.body" } },
|
||||
],
|
||||
status: [200],
|
||||
},
|
||||
headers: { "content-type": "text/plain" },
|
||||
maxRedirects: 1,
|
||||
method: "POST",
|
||||
url: `${baseUrl}/redirect-${statusCode}`,
|
||||
}),
|
||||
makeCtx(),
|
||||
);
|
||||
expect(result.matched).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("相对路径 Location header 重定向", async () => {
|
||||
const result = await checker.execute(
|
||||
makeTarget({ maxRedirects: 1, url: `${baseUrl}/redirect-relative` }),
|
||||
makeCtx(),
|
||||
);
|
||||
expect(result.matched).toBe(true);
|
||||
expect(result.statusDetail).toBe("HTTP 200");
|
||||
});
|
||||
|
||||
test("混合 body rules 集成检查", async () => {
|
||||
const result = await checker.execute(
|
||||
makeTarget({
|
||||
expect: {
|
||||
body: [
|
||||
{ contains: '"status":"ok"' },
|
||||
{ json: { equals: "ok", path: "$.status" } },
|
||||
{ css: { equals: "OK", selector: "span" } },
|
||||
],
|
||||
status: [200],
|
||||
},
|
||||
url: `${baseUrl}/mixed`,
|
||||
}),
|
||||
makeCtx(),
|
||||
);
|
||||
expect(result.matched).toBe(true);
|
||||
});
|
||||
|
||||
test("跨 origin 重定向剥离敏感 headers", async () => {
|
||||
const targetServer = Bun.serve({
|
||||
fetch(req) {
|
||||
@@ -616,6 +700,16 @@ describe("HttpChecker", () => {
|
||||
expect(errors).toContain("regex 正则不合法");
|
||||
});
|
||||
|
||||
test("ReDoS regex body rule 启动校验失败", () => {
|
||||
const errors = validateHttpTarget({
|
||||
expect: { body: [{ regex: "(a+)+$" }] },
|
||||
http: { url: "https://example.com" },
|
||||
name: "test",
|
||||
type: "http",
|
||||
});
|
||||
expect(errors).toContain("正则存在 ReDoS 风险");
|
||||
});
|
||||
|
||||
test("非法 JSONPath 启动校验失败", () => {
|
||||
const errors = validateHttpTarget({
|
||||
expect: { body: [{ json: { equals: "ok", path: "status" } }] },
|
||||
@@ -636,6 +730,16 @@ describe("HttpChecker", () => {
|
||||
expect(errors).toContain("match 正则不合法");
|
||||
});
|
||||
|
||||
test("ReDoS operator match 启动校验失败", () => {
|
||||
const errors = validateHttpTarget({
|
||||
expect: { headers: { "x-test": { match: "(\\d+)*x" } } },
|
||||
http: { url: "https://example.com" },
|
||||
name: "test",
|
||||
type: "http",
|
||||
});
|
||||
expect(errors).toContain("正则存在 ReDoS 风险");
|
||||
});
|
||||
|
||||
test("非法 operator gte 类型启动失败", () => {
|
||||
const errors = validateHttpTarget({
|
||||
expect: { body: [{ json: { gte: "abc", path: "$.count" } }] },
|
||||
|
||||
Reference in New Issue
Block a user