OmX 被动 URL 读取器(Passive URL Reader)实战指南:omx url read的安全边界与结构化输出
【免费下载链接】oh-my-codexOmX - Oh My codeX: Your codex is not alone. Add hooks, agent teams, HUDs, and so much more.项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-codex
omx url read <url> --json是 OmX(Oh My codeX)提供的一项被动式、有界的 URL 读取能力:它只负责把用户提供的 HTTP(S) 地址抓取回来,并以结构化 JSON 交付给自动化流程,全程不依赖浏览器、不注入 Cookie、不解挑战码,也不修改任何全局二进制或PATH。本文以 docs/url-reader.md 为骨架,结合 src/url-reader/index.ts 的真实实现与测试用例,完整讲解命令用法、JSON 输出结构、网络地址封锁策略、DNS 防重绑定机制与重定向校验,并给出可在仓库内直接复现的验证方式。读完你将能安全地把"抓取一个 URL"封装进自己的脚本、Agent 或自动化管线。
一、命令总览:一个命令,四类结果
omx url read的 v0 读取器在设计上刻意保守,它的定位不是"万能抓取器",而是"可审计、可自动化的安全读取器"。CLI 帮助文本(见 src/cli/url.ts)明确列出了它的约束:
- 不启动浏览器自动化,也不引入浏览器依赖;
- 不注入 Cookie、不解挑战、不做任何反机器人检测绕过;
- 不改变全局二进制或
PATH的所有权; - 仅支持
http:与https:协议; - 在抓取之前就封锁本地、回环、私网、链路本地、唯一本地、组播、保留及内部网络地址;
- IPv6 特殊用途/保留地址段全部封锁(细节见下文第三节);
- 主机名在抓取前先解析,任何解析出不安全地址的主机名都会使用与字面 IP 相同的分类器被拦截;
- 重定向被手动跟踪,且每一个重定向目标在被请求前都会重新校验;
- 在文本解码之前对响应体做有界读取;
- 输出结构化
verdict:ok、redirect、blocked或error。
命令格式如下:
omx url read https://example.com --jsonCLI 层面的参数解析位于 src/cli/url.ts 的parseUrlReadArgs:
--json:输出结构化 JSON,v0 阶段必填,缺省会直接报Missing required --json flag;-h, --help/help:打印帮助;- 其余以
-开头的 token 一律报Unknown option; - 恰好接受一个位置参数(URL),多余或缺失都会报错。
CLI 命令在 src/cli/index.ts 中被分发到urlCommand,最终调用核心函数readUrl(src/url-reader/index.ts)。
二、JSON 输出结构:每一个字段的含义
读取结果由 src/url-reader/types.ts 的UrlReadResult定义,字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
input_url | string | 用户最初输入的 URL |
final_url | string | null | 重定向后的最终 URL;被封锁或出错时为null |
verdict | ok|redirect|blocked|error | 整体判定结论 |
status | number | null | HTTP 状态码(如 200、302、403) |
status_text | string | null | 状态文本(如OK、Forbidden) |
content_type | string | null | 响应头的Content-Type |
redirected | boolean | 是否发生过重定向 |
title | string | null | 文本类响应的 best-effort 标题(HTML<title>,最长 200 字符) |
snippet | string | null | 文本类响应的摘要(剔除 script/style 与标签后截取 500 字符) |
signals | string[] | 封锁/挑战信号列表,见下 |
truncated | boolean | 响应体是否因超过maxBytes被截断 |
bytes_read | number | 实际读取的字节数 |
error | UrlReadError | null | 失败时的安全错误细节{ name, message, code? } |
一个成功抓取 HTML 的典型输出大致形如:
{ "input_url": "https://example.com/", "final_url": "https://example.com/", "verdict": "ok", "status": 200, "status_text": "OK", "content_type": "text/html; charset=utf-8", "redirected": false, "title": "Example Domain", "snippet": "Example Domain This domain is for use in illustrative examples ...", "signals": [], "truncated": false, "bytes_read": 1256, "error": null }verdict 的四态语义
ok:成功取得响应,且没有检测到封锁/挑战信号;redirect:请求最终停在一个重定向后的 URL(final_url与input_url不同),响应本身仍可读;blocked:目标被安全策略拦截——可能是不支持的协议、不安全地址、本地主机名、HTTPS 主机名无法钉扎(pinning)、非法/过多重定向,或响应体命中封锁信号(见signals);error:抓取过程中发生运行时错误(如ECONNREFUSED、DNS 解析失败、fetch 实现不可用)。
signals:封锁与挑战信号
src/url-reader/index.ts 的classifySignals会把两类信息写进signals:
- 状态码信号:
401、403、407、423、429、451、503会生成status-<code>(如status-403); - 挑战内容信号:对响应文本做正则匹配(src/url-reader/index.ts 的
CHALLENGE_MARKERS),包括:captcha-marker(匹配captcha)cloudflare-marker(匹配cloudflare|cf-chl|cf_clearance)access-denied-marker、just-a-moment-marker、human-verification-marker、bot-detection-marker、blocked-marker、challenge-marker
只要signals非空,verdict就会被置为blocked。测试 src/url-reader/tests/url-reader.test.ts 验证了"403 + Just a moment + Verify you are human"这类反爬页面会被同时标记status-403、just-a-moment-marker与human-verification-marker。
三、网络地址封锁策略:字面 IP 与主机名共用同一分类器
这是 v0 读取器最核心的安全设计。无论目标是 IP 字面量还是主机名,最终都汇入同一套地址分类器isSafeIpAddress(src/url-reader/index.ts)。
IPv4 封锁规则(isSafeIpv4,src/url-reader/index.ts)
以下地址段在抓取前一律拦截:
0.0.0.0/8(a === 0)10.0.0.0/8私网127.0.0.0/8回环224.0.0.0/4及以上(组播/保留)100.64.0.0/10运营商级 NAT(a === 100 && 64 <= b <= 127)169.254.0.0/16链路本地(含云元数据地址169.254.169.254)172.16.0.0/12私网192.168.0.0/16私网192.0.0.0/24(IANA 保留)192.88.99.0/24(6to4 relay anycast)198.18.0.0/15、198.51.100.0/24(基准测试/文档地址)203.0.113.0/24(文档地址)
IPv6 封锁规则(isSafeIpv6,src/url-reader/index.ts)
IPv6 判定把地址转成大整数后逐段比较,封锁范围与原文档完全一致:
- 全零
::与::1(回环/未指定) 64:ff9b:1::/48本地使用的 IPv4/IPv6 翻译(NAT64)100::/64仅丢弃(discard-only)、100:0:0:1::/64伪前缀(dummy)2001::/23IETF 协议分配2001:db8::/32与3fff::/20文档/保留2002::/166to45f00::/16Segment Routing SIDfc00::/7唯一本地地址(ULA)fe80::/10链路本地ff00::/8组播- IPv4 映射地址(
::ffff:a.b.c.d)会先解包成 IPv4 再走isSafeIpv4判定,因此::ffff:10.0.0.5、::ffff:127.0.0.1、::ffff:169.254.169.254全部被拦 - 公网 IPv6(如
2606:4700:4700::1111)允许访问
测试 src/url-reader/tests/url-reader.test.ts 用一组不安全 IPv6 字面量逐一断言"先封锁、绝不发起抓取",同时验证公网 IPv6 字面量返回ok。
主机名解析与 DNS 防重绑定
对主机名目标,validateAndResolveSafeUrl 会先调用dnsLookup(hostname, { all: true, verbatim: true })解析出全部地址;只要任意一个解析结果不安全,整个读取即被封锁(signal 为unsafe-address)。localhost、localhost.localdomain以及*.localhost后缀的主机名直接以localhost-name拦截,不会进入解析流程。
更关键的是防 DNS 重绑定/TOCTOU 设计(src/url-reader/index.ts 的prepareSafeFetchTarget):
- HTTP 主机名:抓取请求不会发给原主机名,而是发给"已校验并钉扎(pin)的 IP"(
fetchUrl.hostname = validation.address),同时用Host头携带原始主机名(含端口,见originalHttpHostHeader,src/url-reader/index.ts)。测试 src/url-reader/tests/url-reader.test.ts 证明http://example.test:8080/page实际请求的是http://93.184.216.34:8080/page且Host: example.test:8080;防重绑定测试(src/url-reader/tests/url-reader.test.ts)则证明同一主机名只解析一次,绝不给"第一次返回公网 IP、第二次返回 127.0.0.1"的翻转攻击留机会; - HTTPS 主机名:由于当前 v0 运行时而无法在钉扎 TCP 连接的同时安全保留 TLS SNI/证书校验,因此直接封锁(signal
https-hostname-not-pinned),见测试 src/url-reader/tests/url-reader.test.ts; - 公网 IP 字面量的 HTTPS:不涉及主机名解析,直接放行(测试 src/url-reader/tests/url-reader.test.ts)。
四、重定向:手动跟随,逐个校验
v0 读取器不依赖 fetch 内置的自动重定向,而是设置redirect: "manual"手动处理(src/url-reader/index.ts):
- 只认
301、302、303、307、308为可跟随重定向; - 读取
Location头,以当前 URL 为基准解析出下一个目标;解析失败直接按unsafe-redirect-url封锁; - 跟随前对下一个目标再次执行完整的
prepareSafeFetchTarget校验——包括协议、本地主机名、DNS 解析与地址分类; - 每跳都重复上述过程,超过
maxRedirects(默认 10)即按too-many-redirects封锁。
因此,任何"把用户重定向到http://localhost/secret、10.0.0.2、[::1]或解析到私网地址"的攻击在下一跳发生前就会被拦截,测试 src/url-reader/tests/url-reader.test.ts 覆盖了重定向到 localhost、私网 IPv4、不安全 IPv6 字面量以及解析出不安全 IPv6 的主机名四类场景,均断言只发出了第一跳请求。
五、有界读取:在解码之前截断
响应体通过readBoundedBody(src/url-reader/index.ts)按字节上限流式读取,默认maxBytes = 256 * 1024(256 KiB)。读取逻辑:
- 逐块累积,达到上限后立即停止并取消底层流(
reader.cancel()); - 恰好等于上限的完整响应不算截断(测试 src/url-reader/tests/url-reader.test.ts);
- 超过上限的响应标记
truncated: true,bytes_read为实际读取字节数(测试 src/url-reader/tests/url-reader.test.ts 验证 128 字节上限下bytes_read === 128且流被取消)。
文本解码发生在有界读取之后:优先使用Content-Type中的charset,缺省按 UTF-8(TextDecoder容错模式,src/url-reader/index.ts)。只有"文本类"响应(text/*、application/json、application/xml、application/xhtml+xml、application/rss+xml、application/atom+xml、application/ld+json,见 src/url-reader/index.ts)才会提取title与snippet;snippet生成前会先剥离<script>/<style>与所有标签,并解码常见 HTML 实体(src/url-reader/index.ts)。
六、可调参数与可注入依赖
虽然 CLI 的 v0 形态只暴露--json,但核心函数readUrl(url, options)的UrlReaderOptions(src/url-reader/types.ts)为嵌入方提供了四个可调项与两个注入点:
| 选项 | 默认值 | 说明 |
|---|---|---|
timeoutMs | 15_000 | 单次请求超时(毫秒),通过AbortSignal.timeout生效 |
maxBytes | 256 * 1024 | 响应体读取上限(字节) |
maxRedirects | 10 | 最大重定向跳数 |
fetch | globalThis.fetch | 可注入自定义 fetch 实现(如测试中的桩实现) |
resolveHostname | node:dns/promises的lookup | 可注入自定义 DNS 解析器(测试用它模拟公网/私网解析结果) |
其中timeoutMs、maxBytes、maxRedirects均需为正整数,非法值自动回退到默认值(normalizePositiveInteger,src/url-reader/index.ts)。这正是 src/url-reader/tests/url-reader.test.ts 与 src/cli/tests/url.test.ts 能够在不联网的前提下完整验证全部安全行为的原因。
七、错误处理:只暴露安全细节
任何异常都会被normalizeError(src/url-reader/index.ts)收敛为{ name, message, code? },绝不携带堆栈。测试 src/url-reader/tests/url-reader.test.ts 明确断言:一个带code: "ECONNREFUSED"的异常返回verdict: "error"、error.name === "Error"、error.code === "ECONNREFUSED",且 JSON 化后的结果不包含 stack。同时读取器会校验运行时是否存在 fetch 实现,缺失时返回FetchUnavailableError(src/url-reader/index.ts)。
八、在仓库中验证与扩展
如果你想在本地复现或深入调试:
- 运行单元测试(覆盖全部封锁规则、重定向校验、截断与标题/摘要提取):
node --test src/url-reader/__tests__/url-reader.test.ts node --test src/cli/__tests__/url.test.ts其中 src/cli/tests/url.test.ts 还会通过
dist/cli/omx.js真实执行omx url --help,断言帮助文本与"被动读取不绕过挑战、不使用浏览器、不注入 Cookie"的承诺出现在顶层及子命令帮助中。 - CLI 实跑(需要先完成构建得到
dist/cli/omx.js):node dist/cli/omx.js url read https://example.com --json - 阅读实现:入口 src/url-reader/index.ts、类型 src/url-reader/types.ts、CLI 封装 src/cli/url.ts、命令分发 src/cli/index.ts。
总而言之,omx url read --json把"读取一个 URL"封装成了一个默认安全的原语:地址封锁发生在任何网络请求之前,主机名解析被钉扎到已校验 IP,重定向每跳重新校验,响应体有界读取,输出全程结构化 JSON。对于需要把"抓取外部网页"接入 Agent、HUD 或自动化脚本的开发者而言,这是一份开箱即用且可审计的参考实现。
【免费下载链接】oh-my-codexOmX - Oh My codeX: Your codex is not alone. Add hooks, agent teams, HUDs, and so much more.项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-codex
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考