Как решать капчи в Node-RED с помощью узла function

Шаг с капчей в Node-RED занимает два узла, если идти через сырой API, или один узел function, если вам проще написать пять строк на JavaScript. Оба варианта обращаются к решателю, который работает на вашем собственном железе, поэтому ни один из них не считает деньги за каждое решение. Спотыкаются обычно не на самом решении капчи. Дело в том, что узел function не может подключить npm-пакет, пока вы не включите это в settings.js, и что Node-RED очень часто работает на Raspberry Pi, а решатель нет.
Что понадобится
- Node-RED 3 или новее, запущенный где угодно.
- Запущенный и слушающий CapSkip. Режим Local, когда Node-RED стоит на той же машине с Windows, и режим Server, когда нет. Оба описаны в разделе Настройки подключения.
- URL страницы с защищённой формой и её sitekey.
- Доступ к редактированию settings.js, если вы хотите путь через SDK, а не через сырой API.
Ничего из этого не требует браузера. Node-RED не управляет Chrome, он делает HTTP-вызовы, поэтому flow читает sitekey из HTML страницы и отправляет токен обратно как обычное поле формы.
Два способа вызвать решатель из flow
Выберите один до того, как начнёте соединять узлы, потому что flow получаются совсем разными.
| Какой путь | Чего это стоит | Когда он подходит |
|---|---|---|
| Узлы http request к сырому API | Узел отправки, узел delay, узел опроса и узел switch для цикла | Вы не можете править settings.js или хотите, чтобы flow оставался читаемым на канве |
| Node SDK внутри узла function | Одна строка в settings.js и модуль на вкладке Setup | Вы хотите, чтобы опрос, увеличение интервалов и таймауты были сделаны за вас |
Второй путь короче по причине, которую стоит знать. Сырой API предлагает подождать пятнадцать секунд, а затем опрашивать каждые пять, и собранный руками flow следует этому буквально. SDK вместо этого начинает опрос через 250 мс и постепенно увеличивает интервал до потолка, поэтому обычно возвращает токен заметно раньше, чем это когда-либо сделает узел delay.
Путь первый: узлы http request и сырой API
API совместим с 2captcha, а значит, точек входа две. Вы отправляете задачу на in.php и получаете id, затем опрашиваете res.php с этим id, пока он не перестанет отвечать, что ответ ещё не готов. Четыре узла, соединённые в цикл.
| Узел во flow | Настройка |
|---|---|
| Узел http request, который отправляет | POST на http://127.0.0.1:8080/in.php, Return выставлен в разобранный объект JSON |
| Узел function, который хранит id | Сохраните msg.payload.request в msg.captchaId |
| Узел delay | Фиксированная задержка, 15 секунд для reCAPTCHA v2 |
| Узел http request, который опрашивает | GET на http://127.0.0.1:8080/res.php, Return выставлен в разобранный объект JSON |
| Узел switch | Возврат к узлу delay, пока ответ остаётся CAPCHA_NOT_READY |
Узел отправки берёт тело запроса из 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 в строке запроса. Соберите URL в узле function, чтобы узлу 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 отправляет обратно на задержку. status, равный 1, означает, что токен лежит в msg.payload.request. Ограничьте цикл разумным числом проходов, чтобы действительно нерешаемая задача не крутилась вечно. Подробнее состояние опроса разобрано в руководстве по ответу CAPCHA_NOT_READY, а все параметры перечислены в документации CapSkip API.
Путь второй: SDK внутри узла function
Узел function работает в песочнице, у которой по умолчанию нет доступа к npm-пакетам. Этим управляют две настройки, и ведут они себя по-разному.
Более старая настройка functionGlobalContext: модуль подключается через require в settings.js, а внутри узла вы получаете его вызовом global.get. Работает, но модуль видят все узлы function в этом экземпляре, а добавление нового модуля требует перезапуска Node-RED.
Лучше подходит functionExternalModules. Выставьте её в true в settings.js, и у узла 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 здесь конкретное: выполните работу в асинхронном блоке, отправьте сообщение вызовом node.send и верните 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, а flow просто останавливается, и это самая частая причина, по которой ветка с капчей в Node-RED выглядит так, будто вообще ничего не сделала.
Один метод покрывает reCAPTCHA v2, Invisible, Enterprise и v3. Варианты задаются опциями, а не отдельными вызовами: для невидимого виджета это та же строка с объектом опций, где invisible равно 1, а для v3 достаточно version со значением v3 плюс action. У Turnstile и GeeTest свои методы такой же формы, и все они перечислены на на странице SDK для распознавания капчи.
Шаг 2: весь flow в одном узле
Загрузите страницу, вытащите sitekey из HTML, решите капчу, затем отправьте токен обратно вместе с остальными полями формы. Этот вариант стоит вставить себе, если вы хотите, чтобы весь flow состоял из узла 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 живёт около двух минут и принимается один раз, поэтому flow, который сначала решает капчу, затем стоит в узле delay, а потом отправляет форму, получит отказ, совсем не похожий на проблему решателя. Об этом сценарии сбоя стоит почитать в руководстве по истечению срока действия токена reCAPTCHA.
Node-RED и решатель на разных машинах
В Node-RED это важнее, чем в большинстве других инструментов, потому что заметная доля установок живёт на Raspberry Pi, NAS или небольшой машине с Linux, а CapSkip является приложением для Windows. Если это про вашу схему, то 127.0.0.1 указывает на Pi, решателя там нет, и вызов падает с отказом в соединении, так и не дойдя до API.
Ответ здесь режим Server, и это изменение настройки, а не другой продукт. Режим Local слушает 127.0.0.1 и отвечает только этому устройству. Режим Server слушает ваш сетевой или публичный IP, поэтому flow на Pi, на хосте с контейнерами или в хостинговом экземпляре Node-RED обращается к машине с Windows по тому же API. Статический публичный IP держит этот адрес постоянным. В обоих случаях это по-прежнему ваше железо и по-прежнему без счётчика, поэтому загруженный flow не стоит дороже спокойного.
// 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 вместо отправки из асинхронного блока | Вызовите node.send и верните null из тела узла |
| Flow останавливается без видимой ошибки | node.error вызвали без аргумента с сообщением | Передайте msg вторым аргументом и подключите узел catch |
| connect ECONNREFUSED 127.0.0.1:8080 | Node-RED стоит не на той машине, где работает решатель | Переключите решатель в режим Server и укажите в host его адрес |
| Цикл опроса не заканчивается | У узла switch нет ограничения на число попыток | Считайте проходы в переменной контекста и прекращайте после лимита |
| Форма отклоняет токен, который выглядит нормальным | Капчу решили несколькими узлами раньше | Решайте капчу прямо перед отправкой, а не в более ранней ветке |
| ERROR_GOOGLEKEY | Sitekey не относится к URL этой страницы | Перечитайте data-sitekey со страницы, на которую отправляете форму |
FAQ
Нужен ли SDK или хватит узлов http request?
Работают оба варианта. Путь через http request не требует доступа к settings.js и оставляет каждый шаг видимым на канве, что некоторым командам удобнее для аудита. Путь через SDK берёт на себя опрос, увеличение интервалов и таймауты и обычно возвращает токен быстрее, потому что начинает проверять через четверть секунды, а не через пятнадцать.
Может ли хостинговый экземпляр Node-RED достучаться до решателя у меня на столе?
Только в режиме Server. Хостинговый экземпляр работает на чужой инфраструктуре, поэтому 127.0.0.1 там указывает на их контейнер, а не на вашу машину. Привяжите решатель к доступному адресу, закройте его правилом фаервола, которое пропускает только исходящие адреса платформы, и включите проверку ключей. Вся настройка описана на странице параметров подключения.
Как не дать одному flow завалить решатель?
Поставьте перед узлом решения капчи узел delay в режиме ограничения скорости. Он ставит сообщения в очередь и выпускает их с фиксированной частотой, а это именно тот throttle, который нужен, когда три расписания смотрят на одну машину. Решение капчи в цикле без ограничения обычно и превращает пакет задач в кучу таймаутов.
То же самое, что и в n8n?
Со стороны решателя всё одинаково, со стороны flow нет. n8n запускает свой узел Code в закрытой песочнице без установки npm-пакетов, поэтому там остаются только HTTP-узлы. Node-RED спокойно установит пакет ради одного узла function, поэтому здесь и существует путь через SDK. Вариант для n8n разобран в руководстве по работе с капчей в n8n.
Коротко
Включите functionExternalModules, добавьте capskip на вкладке Setup и решайте капчу в асинхронном блоке, который заканчивается вызовом node.send и возвращает null. Подключите узел catch и передавайте сообщение в node.error, чтобы сбои были видны. Если Node-RED живёт на Pi, а решатель на Windows, то нужен режим Server и одна изменённая строка с хостом. Остальная часть поверхности Node.js описана на странице сервиса распознавания капчи для Node.js, а опции reCAPTCHA на странице сервиса распознавания reCAPTCHA v2.
Последнее, что стоит знать, прежде чем ставить этот flow на расписание с интервалом в пять минут. CapSkip представляет собой локальный сервис распознавания капчи и работает на железе, которое у вас уже есть, поэтому flow, который срабатывает каждые пять минут без остановки, стоит ровно столько же, сколько запущенный вручную.
