Files
im0o 3d7304dd22
release / release (push) Successful in 29s
入站详情可以测常用 AI 服务的可访问性
判定按状态码白名单而不是只认 200:探测不带鉴权,OpenAI 回 401、Anthropic 回 405
才是正常应答,说明 TLS 握手、路由、目标服务全都正常。清单和白名单照 sub2api 的
代理质量检查抄来。

失败文案不照抄 Bun:本机端口没人监听时它给的是「Unable to connect. Is the
computer able to access the url?」,把矛头指向目标网址,而真正的原因在本机。

对话框留在 InboundDetail 而不是提到 RouterPage——它不写配置文件也不改内核状态,
不需要走那条 commit 链路。firstPort 挪进 shared/inbounds.ts,两个页面都要用它了。
2026-08-31 03:59:08 +08:00

121 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from 'bun:test'
import {
describeProbeFailure,
findTarget,
isChallenge,
judge,
rayId,
REACH_TARGETS,
} from './reachability.ts'
const openai = REACH_TARGETS[0]!
const gemini = REACH_TARGETS[2]!
const headers = (init: Record<string, string> = {}) => new Headers(init)
describe('REACH_TARGETS', () => {
test('标识不重名,否则接口按 id 取目标会取错', () => {
const ids = REACH_TARGETS.map((t) => t.id)
expect(new Set(ids).size).toBe(ids.length)
})
test('findTarget 认不出来的标识返回 null', () => {
expect(findTarget('openai')?.label).toBe('OpenAI')
expect(findTarget('nonesuch')).toBeNull()
})
})
describe('judge', () => {
test('无鉴权探测拿到 401 算可达,并说明这是预期结果', () => {
const { verdict, message } = judge(openai, 401, headers(), '')
expect(verdict).toBe('reachable')
expect(message).toBe('HTTP 401,目标可达')
})
test('2xx 不需要解释,只报状态码', () => {
expect(judge(gemini, 200, headers(), '{}')).toEqual({
verdict: 'reachable',
message: 'HTTP 200',
})
})
test('白名单之外的状态码算不通', () => {
const { verdict, message } = judge(gemini, 403, headers(), '')
expect(verdict).toBe('unreachable')
expect(message).toBe('HTTP 403,不是预期的状态码')
})
test('429 单独说是限流,不和连不上混在一起', () => {
expect(judge(openai, 429, headers(), '').verdict).toBe('throttled')
})
test('Cloudflare 拦截优先于状态码白名单', () => {
// 429 在 openai 的白名单外,但结论要是 challenge 而不是 throttled:
// 前者要换出口 IP,后者只要等
const { verdict, message } = judge(
openai,
429,
headers({ 'cf-mitigated': 'challenge', 'cf-ray': '8f0a1b2c3d4e5f60-LAX' }),
'',
)
expect(verdict).toBe('challenge')
expect(message).toContain('8f0a1b2c3d4e5f60-LAX')
})
})
describe('isChallenge', () => {
test('只有 403 和 429 才可能是拦截页', () => {
const body = '<html>window._cf_chl_opt</html>'
expect(isChallenge(403, headers(), body)).toBe(true)
expect(isChallenge(429, headers(), body)).toBe(true)
expect(isChallenge(200, headers(), body)).toBe(false)
})
test('没有 cf-mitigated 头时认正文里的标记', () => {
expect(isChallenge(403, headers(), '<title>Just a Moment...</title>')).toBe(true)
})
test('普通的 403 不是拦截页', () => {
expect(isChallenge(403, headers(), '{"error":"forbidden"}')).toBe(false)
})
test('正文只看前 4 KB,标记在那之后不算', () => {
expect(isChallenge(403, headers(), 'x'.repeat(5000) + 'just a moment')).toBe(false)
})
})
describe('rayId', () => {
test('优先取响应头', () => {
expect(rayId(headers({ 'cf-ray': ' abc123-LAX ' }), 'cf-ray: zzz')).toBe('abc123-LAX')
})
test('没有响应头时从正文里找', () => {
expect(rayId(headers(), 'Cloudflare Ray ID: <code>cf-ray: abc123</code>')).toBe('abc123')
})
test('都没有就是空串', () => {
expect(rayId(headers(), 'nothing here')).toBe('')
})
})
describe('describeProbeFailure', () => {
test('超时说等了多久', () => {
const error = new Error('timed out')
error.name = 'TimeoutError'
expect(describeProbeFailure(7890, 10_000, error)).toBe('10 秒内没有响应')
})
test('本机端口没人监听时指向内核,不照抄 Bun 那句指向目标网址的话', () => {
const error = Object.assign(new Error('Unable to connect. Is the computer able to access the url?'), {
code: 'ConnectionRefused',
})
expect(describeProbeFailure(7890, 10_000, error)).toBe('本机 7890 端口没有响应,内核可能没在运行')
})
test('其余原因照原文给出,不猜', () => {
expect(describeProbeFailure(7890, 10_000, new Error('TLS handshake failed'))).toBe(
'连接失败:TLS handshake failed',
)
})
})