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

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

Шаг с капчей в 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:8080Node-RED стоит не на той машине, где работает решательПереключите решатель в режим Server и укажите в host его адрес
Цикл опроса не заканчиваетсяУ узла switch нет ограничения на число попытокСчитайте проходы в переменной контекста и прекращайте после лимита
Форма отклоняет токен, который выглядит нормальнымКапчу решили несколькими узлами раньшеРешайте капчу прямо перед отправкой, а не в более ранней ветке
ERROR_GOOGLEKEYSitekey не относится к 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, который срабатывает каждые пять минут без остановки, стоит ровно столько же, сколько запущенный вручную.