验证 CrawlForge 抓取器
日志里有一个请求自称是 CrawlForge。本页说明如何核实它确实是我们、如果你不希望被抓取该如何屏蔽,以及可以写信给谁。
CrawlForge 是稳定的。它后面的版本号每次发版都会变化,两个运行面上的值也不相同,因此针对完整 User-Agent 字符串写的规则迟早会失效。请匹配令牌;如果你想要的是证据而不是声明,请验证签名。CrawlForge 如何标识自己
CrawlForge 抓取的每个页面都携带同一个诚实的 User-Agent:产品令牌 CrawlForge、一个版本号和一个联系 URL。没有第二个身份,不做轮换,也不会默认伪装成浏览器——抓取器运行规则解释了为什么这是一条规则而不是一种偏好。
CrawlForge 有两个运行面,两者都发送这个令牌:托管的 REST API,以及由客户自行运行的 crawlforge-mcp-server 包。两者各自发送自己包的版本号,因此版本号部分会随运行面和发版而变化,令牌则不会。
User-Agent 模式
这才是我们支持你去匹配的内容。只有版本号部分会变化。
CrawlForge/<version> (+https://crawlforge.dev)托管 API 当前发送的内容
它由抓取器实际发送的同一个常量生成,所以本页不会与请求头脱节。它只是示例,不是用来匹配的字符串:下一次发版就会变。MCP 服务器发送同样的形式,只是带上它自己包的版本号。
User-Agent: CrawlForge/1.0.0 (+https://crawlforge.dev)在日志中识别我们
对令牌做前缀匹配,可以在我们每一次发版之后继续有效。
# Match the product token. Anchored, and stopping at the slash, so the
# version cannot break the match on our next release.
^CrawlForge/已停用的 CrawlForge-Bot 令牌
我们的部分工具过去标识为 CrawlForge-Bot。现在已经没有任何东西发送该令牌,全部发送 CrawlForge。
不过它仍然被当作 disallow 的来源继续遵守。否则统一令牌会悄悄解除对我们所有已在 robots.txt 中写下旧名称的站点所有者的屏蔽,而那是他们针对我们做出的决定,改名不该把它一笔勾销。两个令牌都会被查询,任意一个的 disallow 都优先生效。
如果你的 robots.txt 已经写了这个停用令牌,不需要做任何改动。
# Already in your file? Leave it — it still blocks us.
User-agent: CrawlForge-Bot
Disallow: /密码学验证(Web Bot Auth)
User-Agent 只是一种声明,任何人都可以发送我们的那一串。因此 CrawlForge 使用 Web Bot Auth 对外发请求签名:基于 HTTP 消息签名(RFC 9421)和一把 Ed25519 密钥,其公钥部分由我们公开发布。你无需联系我们、也无需任何白名单,就可以验证一个请求。
两个运行面都会签名。每个签名请求都携带 Signature-Input(被覆盖的组件与签名参数)、Signature(base64 编码的 Ed25519 签名)和 Signature-Agent(发布我们密钥的目录 URL)。
签名覆盖请求发往的 authority,以及在公布时的 Signature-Agent 值。它证明该请求来自持有我们私钥的一方。它不覆盖路径、请求体或 User-Agent,所以请把它当作*谁发的*的证明,而不是*请求了什么*的证明。
我们的签名目录
一个 JSON 密钥集,以 application/http-message-signatures-directory+json 提供,可缓存一天。注意 signatures 是复数。
curl -sS https://www.crawlforge.dev/.well-known/http-message-signatures-directory我们发送的请求头
参数列表由为我们的请求签名的同一个函数序列化生成。尖括号中的值每次请求都不同。请到目录中解析 `keyid`,不要把它写死:它是密钥的指纹,轮换密钥时会变。
Signature-Input: sig1=("@authority" "signature-agent");created=1787788800;keyid="<keyid — look it up in the directory>";alg="ed25519";expires=1787789100;nonce="<64 random bytes, base64>";tag="web-bot-auth"
Signature: sig1=:<base64 Ed25519 signature>:
Signature-Agent: "https://www.crawlforge.dev"验证一个请求
用你收到的请求重建签名基串,再用已发布的公钥核验。Node 内置的 crypto 就够了;任何 RFC 9421 验证器做的都是同一件事。
import { createPublicKey, verify } from 'node:crypto';
// `request` is the request you received; `signatureInput` and `signature`
// are its Signature-Input and Signature header values.
// 1. Fetch the directory and index the published keys by their thumbprint.
const { keys } = await fetch(
'https://www.crawlforge.dev/.well-known/http-message-signatures-directory'
).then((r) => r.json());
// 2. Read the parameters off Signature-Input, dropping the "sig1=" label.
const params = signatureInput.replace(/^[^=]+=/, '');
const keyid = /keyid="([^"]+)"/.exec(params)?.[1];
const published = keys.find((k) => k.kid === keyid);
if (!published) throw new Error('keyid is not in the directory — not us');
// 3. Rebuild the signature base from the request you received. The covered
// components are listed in the parentheses at the start of params.
const covered = /^\(([^)]*)\)/.exec(params)[1];
const lines = [`"@authority": ${request.headers.host}`];
if (covered.includes('"signature-agent"')) {
lines.push(`"signature-agent": ${request.headers['signature-agent']}`);
}
lines.push(`"@signature-params": ${params}`);
// 4. Verify the Ed25519 signature (base64, between the colons).
const key = createPublicKey({
key: { kty: 'OKP', crv: 'Ed25519', x: published.x },
format: 'jwk',
});
const bytes = Buffer.from(
signature.replace(/^[^=]+=:/, '').replace(/:$/, ''),
'base64'
);
const ok = verify(null, Buffer.from(lines.join('\n'), 'utf8'), key, bytes);
// 5. Finally, reject a stale signature: params carries created= and expires=,
// and we sign with a five-minute window.屏蔽 CrawlForge
一条 robots.txt 规则即可覆盖全部。组名就是产品令牌,两个运行面都以它进行匹配,因此这条规则会屏蔽两边的所有 CrawlForge 工具。
把它加进你的 robots.txt。下一次抓取即生效:我们在每次抓取开始时按来源读取一次该文件。
User-agent: CrawlForge
Disallow: /我们不会对你隐瞒的部分
所有抓取工具都默认遵守 robots.txt。客户也可以在单次请求中传入 respect_robots: false:这个覆盖开关确实存在,有文档记载,而且是有意保留的——有些客户与被抓取的站点另有协议,把这个决定逼到暗处只会让它变得不可见,而不会让它变少。
它并不是悄无声息的。该覆盖只作用于一次请求,绝不作用于账户或整个产品。响应中会带上一条使用了覆盖的警告,请求也会在服务端记录为一条 robots_override 条目,包含 URL、工具名以及提出请求的 API 密钥的内部 ID——绝不包含密钥本身——因此有争议的抓取可以追溯到做出该选择的客户。
在这一切之下还有一层平台级排除,任何参数都触及不到它。如果一条 robots.txt 规则对你还不够,请改用排除通道。
出口 IP 地址
我们目前不公布出口 IP 地址清单。我们的托管 API 与浏览器后端运行在第三方基础设施上;在我们能够承诺一个下个月依然准确的地址段之前,公布一个只会比什么都不公布更糟——基于过期地址段建立的白名单会挡住真实流量,却对其他所有人敞开。等我们有了能够为之背书的清单,它会出现在本节。
无论如何,签名验证都是更可靠的检查。地址段只能证明数据包从哪里发出,而任何能在该段内租到地址的人都会一并继承这个声明;常见的补救办法——再加一次反向 DNS 查询——证明的是对某个主机名的控制权,而不是对这个请求的控制权。Ed25519 签名证明的是持有那把私钥,其公钥就在我们的目录中;即便我们更换托管商它依然有效,也无法被重放到另一个 authority 上。
如果就你方的商务安排而言签名还不够,请写信到 support@crawlforge.dev 说明你的需求。
排除与下架请求
如需在平台层面被永久排除——覆盖所有工具、所有 API 密钥和所有套餐,且没有任何客户端参数可以推翻——请写信到 support@crawlforge.dev,说明域名和相关页面。同一地址也处理下架请求,以及你认为来自我们的流量的任何疑问。