如何在 Node-RED 中用 function 节点识别验证码

node-red captcha - How to Solve CAPTCHAs in Node-RED With a Function Node

在 Node-RED 里加一个验证码步骤:走原始 API 就是两个节点,愿意写五行 JavaScript 就是一个 function 节点。两种方式调用的都是跑在你自己硬件上的识别工具,所以都不会按次计费。真正让人卡住的不是识别本身。而是 function 节点在你于 settings.js 里打开开关之前无法 require npm 包,以及 Node-RED 常常跑在 Raspberry Pi 上,而识别工具并不在上面。

你需要什么

  • Node-RED 3 或更新版本,跑在任何你喜欢的地方。
  • CapSkip 已启动并在监听。Node-RED 与它在同一台 Windows 机器上时用 Local mode(本地模式),不在同一台时用 Server mode(服务器模式)。两种模式都说明在 连接设置.
  • 受保护表单的页面 URL,以及它的 sitekey。
  • 如果你想走 SDK 路线而不是原始 API 路线,需要有编辑 settings.js 的权限。

这里没有任何环节需要浏览器。Node-RED 不是在驱动 Chrome,而是在发 HTTP 请求,所以流程从页面 HTML 里读出 sitekey,再把 token 当成普通表单字段提交回去。

从流程中调用识别工具的两种方式

开始连线之前先选一种,因为两者做出来的流程差别很大。

选哪条路线代价是什么什么时候该选它
用 http request 节点直接调原始 API一个提交节点、一个 delay、一个轮询节点,再加一个 switch 做循环你无法编辑 settings.js,或者你希望流程在画布上一目了然
在 function 节点里使用 Node SDK一行 settings.js 配置,加上 Setup 标签页里的一个模块你希望轮询、退避和超时都由它替你处理

第二条路线更短,原因值得了解。原始 API 让你先等十五秒,然后每五秒轮询一次,手工搭的流程会照字面执行。而 SDK 从 250ms 开始轮询,再逐步退避到一个上限,所以它拿到 token 的速度通常明显快过 delay 节点。

路线一:http request 节点加原始 API

这个 API 兼容 2captcha,也就是两个端点。你向 in.php 提交,拿回一个 id,然后带着这个 id 轮询 res.php,直到它不再回答“结果还没准备好”。四个节点,连成一个循环。

流程中的节点设置
负责提交的 http request 节点POST 到 http://127.0.0.1:8080/in.php,Return 设为已解析的 JSON 对象
保存 id 的 function 节点把 msg.payload.request 存成 msg.captchaId
一个 delay 节点固定延时,reCAPTCHA v2 用 15 秒
负责轮询的 http request 节点GET 到 http://127.0.0.1:8080/res.php,Return 设为已解析的 JSON 对象
一个 switch 节点只要回复仍然是 CAPCHA_NOT_READY,就绕回 delay 节点

提交节点的请求体取自 msg.payload,所以要在它前面用一个 function 节点把这个对象构造好。把 json 设为 1,回复才会是 JSON,而不是旧的竖线分隔文本,这样你就省了一次字符串切分。

// Feed this into the submit node. No npm module needed.
msg.url = "http://127.0.0.1:8080/in.php";
msg.method = "POST";
msg.payload = {
    key: env.get("CAPSKIP_KEY") || "capskip",
    method: "userrecaptcha",
    googlekey: "YOUR_SITEKEY",
    pageurl: "https://example.com/page-with-recaptcha",
    json: 1
};
return msg;

轮询节点需要把 id 放回查询字符串里。在 function 节点里拼好 URL,这样 http request 节点就不需要做任何模板替换。

// After the delay. Loop back here until the answer arrives.
const key = env.get("CAPSKIP_KEY") || "capskip";
msg.url = "http://127.0.0.1:8080/res.php?key=" + key +
          "&action=get&id=" + msg.captchaId + "&json=1";
msg.method = "GET";
return msg;

关于回复有两点。status 为 0 且 request 为 CAPCHA_NOT_READY 不是错误,而是结果还在处理中,这条分支就是 switch 节点要绕回 delay 的那一条。status 为 1 表示 msg.payload.request 里就是 token。给循环设一个合理的次数上限,这样真正无法识别的挑战才不会一直空转。关于这个轮询状态,更详细的说明见指南 CAPCHA_NOT_READY 回复,全部参数都列在 CapSkip API 文档.

路线二:在 function 节点里使用 SDK

function 节点跑在一个沙箱里,默认无法访问 npm 包。有两个设置项控制这一点,而且它们的行为并不一样。

老一点的那个是 functionGlobalContext:你在 settings.js 里 require 模块,再在节点内部用 global.get 取回来。它能用,但实例里的每个 function 节点都能看到它,而且加一个模块就要重启 Node-RED。

更好的那个是 functionExternalModules。在 settings.js 里把它设为 true,function 节点就会多出一个 Setup 标签页,你在那里填模块名和它对应的变量名。部署时 Node-RED 会把它装进你的用户目录,而且只有那个节点能看到它。

// settings.js, in your Node-RED user directory.
module.exports = {
    // Lets a function node declare its own npm modules
    // on the Setup tab, installed on deploy.
    functionExternalModules: true,

    // The older, instance-wide alternative.
    // functionGlobalContext: { capskip: require("capskip") },
}

重启 Node-RED,打开一个 function 节点,切到 Setup 标签页,把模块 capskip 加进去,变量名也写 capskip。部署一次就会自动安装。从此节点代码里可以直接使用它。

第 1 步:在 function 节点里完成识别

一次识别是一个耗时若干秒的网络请求,所以节点必须异步结束。也就是说不能直接 return 消息。Node-RED 在这里的规则很明确:在 async 块里做事,用 node.send 把消息发出去,然后从节点代码里 return null,避免同一条消息被发两次。

// npm install capskip - or add it on the Setup tab
const solver = new capskip.CapSkip({
    host: "127.0.0.1",
    port: 8080,
    apiKey: env.get("CAPSKIP_KEY") || "capskip"
});

(async () => {
    try {
        const result = await solver.recaptcha(msg.sitekey, msg.pageUrl);
        msg.token = result.code;    // inject this into the form
        node.send(msg);
    } catch (err) {
        node.error(err, msg);       // routes to a catch node
    }
    node.done();
})();

return null;

把 msg 作为第二个参数传给 node.error,catch 节点才能接住这个失败。不传的话,错误只会落在 debug 侧边栏里,流程就这么停了,这也是 Node-RED 验证码分支看起来什么都没做的最常见原因。

一个方法就覆盖了 reCAPTCHA v2、Invisible、Enterprise 和 v3。这些变体是选项,而不是不同的调用,所以 Invisible 小组件就是同一行代码加一个 invisible 设为 1 的选项对象,v3 则是 version 设为 v3 再加一个 action。Turnstile 和极验(GeeTest)有各自的方法,形态完全一样,全部方法都列在 验证码识别 SDK 页面.

第 2 步:把整个流程放进一个节点

抓取页面,从 HTML 里取出 sitekey,识别,然后把 token 连同表单其余字段一起提交回去。如果你希望整个流程就是一个 inject 节点、这个 function 节点和一个 debug 节点,那就粘贴这个版本。

// Module on the Setup tab: capskip. fetch is built in
// from Node 18, which Node-RED 3 and 4 both require.
const PAGE = "https://example.com/page-with-recaptcha";
const solver = new capskip.CapSkip({ host: "127.0.0.1", port: 8080 });

(async () => {
    try {
        const html = await (await fetch(PAGE)).text();
        const found = html.match(/data-sitekey=["']([^"']+)/);
        if (!found) { throw new Error("No data-sitekey on the page."); }

        // Solve, then submit straight away. Tokens go stale.
        const result = await solver.recaptcha(found[1], PAGE);
        const reply = await fetch(PAGE, {
            method: "POST",
            body: new URLSearchParams({
                "g-recaptcha-response": result.code
            })
        });

        msg.payload = { status: reply.status, token: result.code };
        node.send(msg);
    } catch (err) {
        node.error(err, msg);
    }
    node.done();
})();

return null;

识别要放在紧挨着提交的那一步,绝不要放在更早、后面还要等别的东西的分支里。reCAPTCHA token 的有效期大约两分钟,而且只能用一次,所以一个先识别、再停在 delay 节点、最后才提交的流程会被拒绝,而这个拒绝看起来完全不像识别工具的问题。这种失败模式值得读一读指南 reCAPTCHA token 过期.

把 Node-RED 和识别工具跑在不同机器上

这一点在 Node-RED 里比在多数工具里更重要,因为相当大一部分安装是在 Raspberry Pi、NAS 或一台小型 Linux 机器上,而 CapSkip 是 Windows 应用。如果这就是你的情况,那么 127.0.0.1 指的是那台 Pi,识别工具并不在上面,请求还没到 API 就以连接被拒绝告终。

答案是 Server mode(服务器模式),它只是一个设置项,不是另一个产品。Local mode(本地模式)绑定 127.0.0.1,只响应本机。Server mode 绑定你的内网或公网 IP,于是跑在 Pi、容器主机或托管 Node-RED 实例上的流程可以用同一套 API 调用那台 Windows 机器。固定公网 IP 能让这个地址保持不变。两种模式下硬件都还是你自己的,也都不计次,所以繁忙的流程并不比清闲的流程花更多钱。

// Same call, same SDK. Only the host moves.
const solver = new capskip.CapSkip({
    host: "10.0.0.12",
    port: 8080,
    apiKey: env.get("CAPSKIP_KEY")
});

一旦识别工具监听在网络地址上,就打开密钥校验,并给每个 Node-RED 实例分配各自的密钥,这样吊销其中一个不会影响其他。把密钥放进环境变量,而不要写在节点代码里:flows 文件就是磁盘上的 JSON,还经常被提交进 git 仓库。两种模式的完整步骤见 CapSkip 设置指南.

常见错误及其含义

你所看到的原因修复
capskip is not defined模块根本没有在 Setup 标签页里声明把 functionExternalModules 设为 true,再添加模块并部署
function 节点什么都不输出消息是被 return 的,而不是在 async 块里发出去的调用 node.send,并在节点代码里 return null
流程停住了,也看不到错误调用 node.error 时没有传消息参数把 msg 作为第二个参数传入,并接上一个 catch 节点
connect ECONNREFUSED 127.0.0.1:8080Node-RED 不在运行识别工具的那台机器上把识别工具切到 Server mode,并把 host 设为它的地址
轮询循环停不下来switch 节点没有设置尝试次数上限用上下文变量统计次数,超过上限就放弃
表单拒绝了一个看起来没问题的 token识别是在好几个节点之前做的紧挨着提交那一步识别,不要放在更早的分支里
ERROR_GOOGLEKEYsitekey 与那个页面 URL 不匹配从你要提交的那个页面重新读取 data-sitekey

常见问题

我一定要用 SDK 吗,还是 http request 节点就够了?

两种都行。http request 路线不需要 settings.js 的访问权限,每一步都留在画布上可见,有些团队为了便于审计更喜欢这样。SDK 路线替你处理轮询、退避和超时,而且通常更快拿到 token,因为它在四分之一秒后就开始查询,而不是十五秒后。

托管的 Node-RED 实例能连到我桌上的识别工具吗?

只有在 Server mode 下可以。托管实例跑在别人的基础设施上,所以那里的 127.0.0.1 是他们的容器,不是你的机器。把识别工具绑定到一个可达的地址,用防火墙规则只放行该平台的出站地址,并打开密钥校验。连接设置页面讲了完整的配置过程。

怎样避免某个流程把识别工具压垮?

在识别节点前面放一个设为限速模式的 delay 节点。它会把消息排队,再按固定速率放出去,当三个定时任务都指向同一台机器时,这正是你需要的限流。在没有上限的循环里做识别,是批量任务变成一堆超时的常见原因。

这和在 n8n 里做是一回事吗?

识别工具这一侧完全一样,流程这一侧不一样。n8n 的 Code 节点跑在一个锁死的沙箱里,不能安装 npm 包,所以在那边只能用 HTTP 节点。Node-RED 则愿意为单个 function 节点安装一个包,这也是这里存在 SDK 路线的原因。n8n 版本写在 n8n 验证码工作流指南.

简短版结论

打开 functionExternalModules,在 Setup 标签页里添加 capskip,然后在 async 块里完成识别,块的结尾调用 node.send 并 return null。接上一个 catch 节点,并把消息传给 node.error,这样失败才看得见。如果 Node-RED 在 Pi 上而识别工具在 Windows 上,那就是 Server mode 加上改一个 host 字符串的事。Node.js 的其余接口见 Node.js 验证码识别页面,reCAPTCHA 的各项选项见 reCAPTCHA v2 识别页面.

在把那个流程设成每五分钟跑一次之前,还有最后一点值得知道。CapSkip 是一款 本地验证码识别工具 ,跑在你已经拥有的硬件上,所以一个永远每五分钟触发一次的流程,和你手动触发一次的成本完全相同。