- http.ts:cf-mitigated: challenge 或 403/503/429 挑战页 → SiteError(challenge),503 不再算维护、403 不再落到 auth_invalid - 挑战页分类 classifyChallengePage:自动 / 可点(Turnstile)/ 真验证码 - 刷新登录 / 用户数据遇 Cloudflare 挑战只记 last_error,账号状态不变、不开登录失效事项 Co-Authored-By: Claude Opus 5.5 <[email protected]>
117 lines
5.0 KiB
TypeScript
117 lines
5.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { challengeRuleOfHtml, classifyChallengePage, detectChallenge } from "./challenge.ts";
|
|
import { siteFetch } from "./http.ts";
|
|
import { createSiteRateLimiter } from "./rate-limit.ts";
|
|
import { SiteError } from "./types.ts";
|
|
|
|
// CF-2:Cloudflare 挑战识别。HTTP 端挑战不是凭据错误(403 不再算 auth_invalid、503 不再算维护);
|
|
// 浏览器端按页面分:自动 / 可点(Turnstile)/ 真验证码。
|
|
|
|
const CF_PAGE = `<!DOCTYPE html><html><head><title>Just a moment...</title></head><body>
|
|
<div id="challenge-spinner"></div><script>window._cf_chl_opt={cType: 'managed'}</script></body></html>`;
|
|
const CF_JS_PAGE = `<html><head><title>Just a moment...</title></head><body>
|
|
<script>window._cf_chl_opt={cType: 'non-interactive'}</script></body></html>`;
|
|
const NORMAL = `<html><head><title>种子 - 观众</title><script src="/cdn-cgi/challenge-platform/scripts/jsd/main.js"></script></head><body>ok</body></html>`;
|
|
|
|
function ctx(fetchImpl: typeof fetch) {
|
|
return {
|
|
siteId: "s1",
|
|
fetch: fetchImpl,
|
|
limiter: createSiteRateLimiter({ minIntervalMs: 0, burst: 10 }),
|
|
timeoutMs: 5_000,
|
|
now: () => new Date(),
|
|
};
|
|
}
|
|
const reply = (status: number, body: string, headers: Record<string, string> = {}) =>
|
|
(async () => new Response(body, { status, headers: { "content-type": "text/html", ...headers } })) as typeof fetch;
|
|
|
|
describe("challengeRuleOfHtml", () => {
|
|
it("挑战页标题 / 脚本特征命中;正常页面里注入的 challenge-platform 脚本不算", () => {
|
|
expect(challengeRuleOfHtml(CF_PAGE)).toBe("title=Just a moment...");
|
|
expect(challengeRuleOfHtml('<html><body><form id="challenge-form"></form></body></html>')).toContain(
|
|
"challenge-form",
|
|
);
|
|
expect(challengeRuleOfHtml(NORMAL)).toBeNull();
|
|
expect(challengeRuleOfHtml("")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("detectChallenge", () => {
|
|
it("cf-mitigated: challenge 不读正文;403 / 503 读正文判定并重新包装响应", async () => {
|
|
const a = await detectChallenge(new Response("x", { status: 200, headers: { "cf-mitigated": "challenge" } }));
|
|
expect(a.rule).toBe("header cf-mitigated=challenge");
|
|
const b = await detectChallenge(new Response(CF_PAGE, { status: 403 }));
|
|
expect(b.rule).toContain("Just a moment");
|
|
expect(await b.res.text()).toBe(CF_PAGE);
|
|
const c = await detectChallenge(new Response("Forbidden", { status: 403 }));
|
|
expect(c.rule).toBeNull();
|
|
expect(await c.res.text()).toBe("Forbidden");
|
|
const d = await detectChallenge(new Response(NORMAL, { status: 200 }));
|
|
expect(d.rule).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("siteFetch 挑战分类", () => {
|
|
const url = "https://audiences.example/torrents.php";
|
|
const kind = async (f: typeof fetch) => {
|
|
try {
|
|
await siteFetch(ctx(f), url);
|
|
return "ok";
|
|
} catch (e) {
|
|
return e instanceof SiteError ? e.kind : "other";
|
|
}
|
|
};
|
|
|
|
it("403 + cf-mitigated → challenge(不是 auth_invalid),不算凭据错误", async () => {
|
|
let err: unknown;
|
|
try {
|
|
await siteFetch(ctx(reply(403, CF_PAGE, { "cf-mitigated": "challenge" })), url);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
expect(err).toBeInstanceOf(SiteError);
|
|
const se = err as SiteError;
|
|
expect(se.kind).toBe("challenge");
|
|
expect(se.isAuthError).toBe(false);
|
|
expect(se.detail).toMatchObject({ reason: "challenge", rule: "header cf-mitigated=challenge", httpStatus: 403 });
|
|
});
|
|
|
|
it("503 挑战页 → challenge(不是维护);403 挑战页(没有响应头)也认", async () => {
|
|
expect(await kind(reply(503, CF_PAGE))).toBe("challenge");
|
|
expect(await kind(reply(403, CF_PAGE))).toBe("challenge");
|
|
expect(await kind(reply(429, CF_JS_PAGE))).toBe("challenge");
|
|
});
|
|
|
|
it("普通 503 仍是维护,普通 403 交给适配器(响应体可读)", async () => {
|
|
expect(await kind(reply(503, "maintenance"))).toBe("site_maintenance");
|
|
const res = await siteFetch(ctx(reply(403, "Forbidden")), url);
|
|
expect(res.status).toBe(403);
|
|
expect(await res.text()).toBe("Forbidden");
|
|
});
|
|
});
|
|
|
|
describe("classifyChallengePage", () => {
|
|
it("不是挑战页 → none", () => {
|
|
expect(classifyChallengePage({ title: "种子", html: NORMAL, text: "种子列表" }).kind).toBe("none");
|
|
});
|
|
it("JS 检查 → waiting;托管 / Turnstile → interactive", () => {
|
|
expect(classifyChallengePage({ html: CF_JS_PAGE, text: "Checking your browser" }).kind).toBe("waiting");
|
|
expect(classifyChallengePage({ html: CF_PAGE, text: "" }).kind).toBe("interactive");
|
|
expect(
|
|
classifyChallengePage({
|
|
title: "请稍候…",
|
|
html: "<html><body></body></html>",
|
|
text: "请完成以下操作,验证您是真人。",
|
|
}).kind,
|
|
).toBe("interactive");
|
|
});
|
|
it("选图 / 输码 → captcha(不自动处理)", () => {
|
|
expect(classifyChallengePage({ html: `${CF_PAGE}<div class="h-captcha"></div>`, text: "Just a moment" }).kind).toBe(
|
|
"captcha",
|
|
);
|
|
expect(classifyChallengePage({ title: "Just a moment...", html: "", text: "请点击图中所有的红绿灯" }).kind).toBe(
|
|
"captcha",
|
|
);
|
|
});
|
|
});
|