Как решить ALTCHA в Node.js и отправить результат через Fetch

Решить ALTCHA в Node.js можно одним вызовом, и браузер не нужен нигде в стеке. ALTCHA построена на доказательстве работы, а не на распознавании: сайт выдаёт задачу, и клиент должен хешировать до тех пор, пока не найдёт счётчик, который ей удовлетворяет. Смотреть тут не на что, поэтому в деле нет ни WebDriver, ни headless Chrome, ни user agent, а ответ вычисляется, а не угадывается. CapSkip добавил этот тип в версии 1.2.6, и Node SDK предоставляет его одним методом. Получается редкий тип капчи, у которого весь прогон представляет собой обычный HTTP-скрипт: получить страницу, считать с неё задачу, решить, отправить token обратно, и всё это через глобальный fetch и единственный вызов SDK.
Что понадобится
- CapSkip 1.2.6 или новее на машине с Windows. Поддержка ALTCHA появилась именно в этом выпуске.
- Node 18 или новее: этого требует сам пакет, и оттуда же берётся глобальный fetch, который используется ниже. Определения TypeScript поставляются внутри пакета, поэтому отдельный пакет с типами ставить не нужно.
- URL страницы, на которой стоит виджет, и эндпоинт, у которого виджет запрашивает свою задачу.
- Адрес решателя. Local mode отвечает на 127.0.0.1 только для этого устройства; Server mode слушает ваш сетевой адрес или публичный IP, чтобы до него могла достучаться другая машина. Какой вариант подходит, разбирается в шаге 4, и оба настраиваются в разделе Настройки подключения.
# npm install capskip npm install capskip
Шаг 1: вызов решения и откуда берётся задача
Один метод, два аргумента: URL страницы, а затем объект опций с задачей. Передайте ему эндпоинт, и CapSkip сам получит задачу.
// npm install capskip
const { CapSkip } = require('capskip');
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
// CapSkip fetches the challenge, then hashes until the counter fits.
const result = await solver.altcha('https://example.com/signup', {
challengeUrl: 'https://example.com/altcha/challenge',
});
console.log(result.token); // base64 payload for the form field
console.log(result.number); // the counter that satisfied itДва поля результата принадлежат только ALTCHA. В token лежит base64-полезная нагрузка, которую ждёт форма, а в number лежит счётчик, который решил задачу. Поле code содержит ту же строку, что и token, поэтому подойдёт любое из них, но token назван по имени поля, в которое попадает, и в месте вызова читается лучше. Поля GeeTest и user agent для Turnstile здесь отсутствуют.
У названия опции есть не одно допустимое написание. И challengeUrl, и challenge_url ведут к одному и тому же параметру API, и то же самое верно для challengeJson и challenge_json. Вариант в camel case используется в документации для Node и совпадает с остальной частью SDK, поэтому выбирайте его и придерживайтесь одного стиля; псевдонимы в snake case существуют для того, чтобы пример, скопированный из руководства по PHP или Python, всё равно работал.
Найдите эндпоинт, который запрашивает виджет
Откройте DevTools, перейдите на вкладку Network и перезагрузите страницу, на которой стоит виджет. Виджет делает один запрос за своей задачей, обычно по пути, в котором есть altcha. Именно URL этого запроса вы и передаёте, а JSON, который он возвращает, представляет собой документ задачи, и его можно передать вместо URL.
Не угадывайте атрибут, который его задаёт: он менялся от одного поколения виджета к другому. Читайте исходный код страницы.
| Поколение виджета | Атрибут, который задаёт challenge |
|---|---|
| v1 и v2 | challengeurl для эндпоинта плюс отдельный атрибут challengejson для встроенного challenge |
| v3 и новее | challenge, причём этот же атрибут принимает либо URL, либо сами данные challenge |
<!-- v1 and v2 name the endpoint on its own attribute --> <altcha-widget challengeurl="https://example.com/altcha/challenge"></altcha-widget> <!-- v3 and later put both forms behind one attribute --> <altcha-widget challenge="https://example.com/altcha/challenge"></altcha-widget>
Три стиля отображения, native, checkbox и switch, различаются только внешне. Они отправляют одну и ту же полезную нагрузку, и разница никогда не доходит до решателя, поэтому разбираться, какой из них перед вами, не нужно. Атрибуты ALTCHA описывает в собственном руководстве по интеграции.
Как передать документ challenge вместо эндпоинта
Если ваш парсер уже считал задачу со страницы, передайте документ, и никакого сетевого запроса не будет вообще.
// No fetch happens: the document is already here.
const result = await solver.altcha('https://example.com/signup', {
challengeJson: {
algorithm: 'SHA-256',
challenge: 'YOUR_CHALLENGE_HASH',
salt: 'YOUR_SALT',
signature: 'YOUR_SIGNATURE',
maxnumber: 1000000,
},
});Эта опция принимает объект, который сериализуется за вас, либо строку JSON, если она у вас уже есть. Передавать одновременно и эндпоинт, и документ разрешено, и побеждает встроенный документ, потому что запрос по сети лишь повторно получил бы то, что вы только что передали. Но под нагрузкой эти два пути ведут себя по-разному. Встроенная задача, у которой уже истёк срок, отклоняется сразу, а не хешируется впустую, тогда как эндпоинт позволяет решателю получить свежую задачу, если первая умерла, пока задание стояло в очереди.
Какие алгоритмы покрывает решатель
Один и тот же метод обрабатывает оба поколения. Устаревшая схема покрыта алгоритмами SHA-1, SHA-256, SHA-384 и SHA-512, а второе поколение с доказательством работы покрыто PBKDF2 и итеративным SHA. PBKDF2 представляет собой вариант по умолчанию, который рекомендует сама ALTCHA, поэтому покрытие охватывает подавляющее большинство работающих сайтов.
Исключения составляют Argon2id и scrypt: их не пытаются посчитать, а отклоняют. Задание с одним из них возвращается примерно за треть секунды с ERROR_CAPTCHA_UNSOLVABLE и никогда не повторяется. Так сделано намеренно. Повторная попытка не помогает против функции, требовательной к памяти, поэтому быстрый отказ лучше имитации бурной деятельности. Для ALTCHA такой результат указывает на алгоритм, а не на нечитаемую картинку.
Шаг 2: весь прогон на fetch, без браузера
Рендерить нечего, поэтому страница, с которой вам нужна задача, представляет собой обычный документ, который можно получить запросом. Об этом стоит сказать прямо, потому что для любого типа капчи с виджетом честный ответ где-нибудь да включает браузер. Здесь нет. Получите страницу, вытащите атрибут из разметки и передайте его прямо решателю.
// npm install capskip
const PAGE = 'https://example.com/signup';
// The page is only a document here: no browser, no rendering.
const html = await (await fetch(PAGE)).text();
// v1 and v2 use challengeurl; v3 and later use challenge.
const found = html.match(/(?:challengeurl|challenge)="([^"]+)"/i);
if (!found) throw new Error('no ALTCHA widget on this page');
const result = await solver.altcha(PAGE, { challengeUrl: found[1] });Регулярное выражение годится для одной известной страницы и плохо подходит для краулера, поэтому берите настоящий HTML-парсер, как только имеете дело с разметкой, которую писали не вы. Смысл примера в его форме, а не в разборе разметки: запрос, строка, решение и ни одного процесса, который нужно запускать и останавливать. Поэтому же этот тип хорошо ведёт себя в serverless-функции или в короткоживущем воркере, где запуск Chromium стоил бы куда дороже самого решения.
Одна оговорка про атрибут версии 3. В нём лежит либо URL, либо сам документ задачи, поэтому перед передачей проверьте, что именно вам досталось. Если значение начинается с фигурной скобки, а не со схемы, перед вами встроенная задача, и её место в опции документа из предыдущего раздела.
Типизация результата, если вы на TypeScript
Определения поставляются внутри пакета, поэтому отдельный пакет с типами ставить не нужно. Один тип результата покрывает все типы капчи, которые решает SDK, а значит, каждое поле, принадлежащее только одному из них, объявлено необязательным. token и number относятся к ALTCHA, поэтому компилятор типизирует token как string или undefined и не даст передать его туда, где ждут обычную строку.
// npm install capskip
import { CapSkip, SolveResult, AltchaOptions } from 'capskip';
const options: AltchaOptions = { challengeUrl: found[1] };
const result: SolveResult = await solver.altcha(PAGE, options);
// One check, right after the call, and the type is settled.
if (!result.token) throw new Error('no ALTCHA token on this result');
const token: string = result.token;Точно так же вас подталкивает user agent для Turnstile в руководстве по Turnstile для Node.js, но с более серьёзным последствием: отсутствующий user agent стоит вам отклонённой отправки, а отсутствующий token означает, что отправлять вообще нечего. Прибегайте к оператору non-null assertion, только если уверены, потому что он глушит ту единственную проверку, которая сообщает, что был вызван не тот метод.
Одного типы не поймают. В интерфейсе опций есть индексная сигнатура, поэтому любой лишний ключ, который вы напишете, компилятор примет. Опция с опечаткой поэтому собирается без ошибок и падает уже во время выполнения, потому что SDK отклоняет параметр, которого ALTCHA не принимает. Аннотация объекта опций, как выше, хотя бы проверяет те ключи, о которых она знает.
Шаг 3: отправляем токен обратно без изменений, пока он не истёк
Виджет отправляет свою полезную нагрузку в поле формы с именем altcha, значит туда же идёт и ваш токен. Именно на этом шаге всё тихо ломается.
// Send it exactly as it came back: no trimming,
// no re-encoding, no reordering.
const response = await fetch('https://example.com/signup', {
method: 'POST',
body: new URLSearchParams({
email: '[email protected]',
altcha: token,
}),
});Токен представляет собой base64 от документа JSON, поля которого покрыты HMAC-подписью самого сервера. Любое изменение делает его недействительным, поэтому всё, что похоже на приведение в порядок, сломает отправку: обрезка пробелов, декодирование с последующим кодированием, пересборка JSON с ключами в другом порядке. Некоторые интеграции читают полезную нагрузку из поля тела JSON, а не из поля формы, так что посмотрите, что отправляет собственная форма страницы, и повторите это.
Второй способ завалить этот шаг связан со временем. Окна задач короткие, а некоторые сайты закрывают их меньше чем за две минуты. Когда окно истекает, сайт отклоняет ответ сухой ошибкой проверки, которая выглядит ровно так же, как неверный ответ, и в ответе нет ничего, что подсказало бы, что именно произошло. Помогают три привычки: получать задачу непосредственно перед решением, а не в начале долгого прогона, отправлять token в той же единице работы, которая его получила, и никогда не держать token, пока человек заполняет форму.
Ограничивают вас вовсе не собственные тайм-ауты опроса у клиента, потому что окно задачи закрывается задолго до того, как истечёт любой из них. ALTCHA представляет собой работу процессора, а не сессию браузера, поэтому она укладывается в тайм-аут опроса по умолчанию, а не в более длинный тайм-аут для reCAPTCHA.
| Параметр конструктора | По умолчанию | Что он покрывает |
|---|---|---|
| defaultTimeout | 120 секунд | Опрос ALTCHA и графической капчи |
| recaptchaTimeout | 300 секунд | Опрос reCAPTCHA, Turnstile и GeeTest |
| pollingInterval | Максимум 5 секунд | Опрос начинается с 0,25 секунды и увеличивает интервал до этого значения |
Шаг 4: где работает решатель и какой режим подключения для этого нужен
В примерах выше используется 127.0.0.1, потому что так правильно, когда ваш процесс Node и решатель стоят на одной машине. Как только вызывающий код работает в другом месте, например в контейнере, на CI-раннере, на VPS или на управляемом хостинге, адрес обратной петли больше не указывает на решатель, и первое же решение падает с NetworkException.
Переключите CapSkip в Server mode, и он вместо этого будет слушать ваш сетевой адрес или публичный IP, так что любой из этих вариантов достучится до него по тому же HTTP API. Если маршрут идёт через интернет, рекомендуется статический публичный IP плюс правило файрвола, которое пропускает только ожидаемые адреса. Server mode меняет только то, где слушает решатель, и больше ничего: это по-прежнему ваше оборудование и по-прежнему без счётчика решений. Читайте хост и порт из окружения, чтобы одна сборка работала в обоих случаях. Клиент сам не читает ни CAPSKIP_HOST, ни CAPSKIP_PORT, поэтому передайте их в конструктор, как это сделано в полном примере ниже.
| Где выполняется процесс Node | Какой режим подключения |
|---|---|
| На машине с CapSkip, как скрипт или локальный сервер | Режим Local. 127.0.0.1 действительно верен |
| На другой машине в той же сети | Режим Server, по внутреннему адресу этой машины |
| В контейнере, на VPS или на управляемой платформе | Server mode со статическим публичным IP и правилом брандмауэра |
Одно замечание про прокси, специфичное для ALTCHA. Прокси здесь поддерживается, но используется только при получении challenge. Сессии браузера, которую нужно было бы маршрутизировать, тут нет, поэтому на само доказательство работы прокси не влияет.
Полный рабочий пример
// npm install capskip
import { CapSkip, ApiException, TimeoutException, NetworkException } from 'capskip';
const solver = new CapSkip({
host: process.env.CAPSKIP_HOST || '127.0.0.1',
port: Number(process.env.CAPSKIP_PORT || 8080),
});
export async function signUp(email: string) {
try {
// Fetch, solve and submit in one unit of work.
const result = await solver.altcha('https://example.com/signup', {
challengeUrl: 'https://example.com/altcha/challenge',
});
if (!result.token) throw new Error('not an ALTCHA result');
const response = await fetch('https://example.com/signup', {
method: 'POST',
body: new URLSearchParams({ email, altcha: result.token }),
});
console.log(response.status, 'after counter', result.number);
} catch (err) {
// ERROR_CAPTCHA_UNSOLVABLE here means Argon2id or scrypt.
if (err instanceof ApiException) console.log('refused:', err.message);
else if (err instanceof TimeoutException) console.log('gave up waiting');
else if (err instanceof NetworkException) console.log('solver unreachable');
else throw err;
}
}Остальные типы устроены так же, только метод другой. Вызов для reCAPTCHA принимает sitekey и URL страницы, Turnstile работает точно так же, GeeTest вместе с URL страницы принимает значение gt и challenge, а решение картинок принимает путь к файлу, URL или base64. Полный список методов есть на странице сервиса распознавания капчи для Node.js, и те же методы есть в каждом официальном пакете на странице SDK.
Turnstile остаётся единственным типом, которому нужен не только sitekey, когда он приходит полноценной страницей проверки. Его дополнительные значения разобраны в руководстве по Turnstile для Node.js.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| Компилятор не принимает token и говорит, что string или undefined не является string | Один тип результата покрывает все типы капчи, поэтому поля только для ALTCHA необязательны | Сузьте тип один раз после решения, затем используйте суженное значение |
| Опция с опечаткой собирается без ошибок и падает при запуске | В интерфейсе опций есть индексная сигнатура, поэтому неизвестные ключи проходят насквозь | Аннотируйте объект опций типом опций ALTCHA и проверьте написание |
| token во время выполнения читается как undefined | Это поле заполняется только для ALTCHA | Вызовите метод ALTCHA. В результате ALTCHA поле code содержит ту же строку |
| Пустая ошибка проверки от сайта при токене, который выглядит нормально | Challenge истёк до того, как форма была отправлена | Получайте challenge, решайте и отправляйте в одной единице работы |
| ERROR_CAPTCHA_UNSOLVABLE внутри ApiException, примерно через треть секунды | Challenge использует Argon2id или scrypt | Повторять нечего. Эти два отклоняются намеренно |
| ValidationException при вызове | Не передан ни один из двух параметров задачи, либо передан параметр, который ALTCHA не принимает | Передайте эндпоинт challenge или документ challenge, а всё остальное уберите |
| NetworkException на первом решении | CapSkip не запущен либо неверны хост и порт | Запустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server |
| Форма отклоняет токен, который по логам был решён | Что-то перекодировало, обрезало или переупорядочило полезную нагрузку | Передавайте строку напрямую, не трогая её |
FAQ
Нужен ли Puppeteer или Playwright для страницы с ALTCHA?
Нет, и это как раз самое полезное. ALTCHA выдаёт задачу на хеширование, а не что-то, на что надо смотреть, поэтому работа идёт только на процессоре и заканчивается за миллисекунды. Ни браузер, ни WebDriver, ни user agent тут не участвуют. Достаточно обычного скрипта с глобальным fetch, а значит, он спокойно работает внутри воркера, потребителя очереди или serverless-функции, где запускать Chromium было бы медленно и неудобно.
Достучится ли приложение на Node с хостинговой платформы до решателя?
Да. Переключите CapSkip в Server mode в настройках подключения, чтобы он слушал сетевой адрес, а не адрес обратной петли, и укажите этот адрес в переменной окружения с хостом. Контейнер, CI-раннер, VPS и управляемая платформа приложений подключаются одинаково, по тому же HTTP API. Если маршрут идёт через интернет, используйте статический публичный IP и ограничьте его правилом файрвола. Решатель во всех этих случаях остаётся на вашем собственном оборудовании, поэтому ни лицензия, ни количество решений не меняются.
Решает ли асинхронный клиент несколько задач ALTCHA быстрее?
Сам по себе нет. В пакете для Node асинхронный клиент представляет собой псевдоним обычного, а не вторую реализацию, поэтому его импорт ничего не меняет в том, как выполняется работа. Каждый метод и так возвращает промис, поэтому параллельность возникает, когда вы запускаете несколько вызовов вместе и дожидаетесь всего набора. При этом держите каждый запрос задачи рядом с её решением, потому что задачи истекают независимо друг от друга, и пакет, собранный заранее, устареет, пока первые из них ещё хешируются.
Обязательно ли использовать TypeScript, чтобы работать с SDK?
Нет. Определения поставляются внутри пакета, поэтому они доступны, если ваш проект их читает, и невидимы, если не читает. Обычный CommonJS работает ровно так, как показано в первом примере, и разница только в том, что необязательный token превращается в проверку во время выполнения, которую вы пишете сами, а не в ту, на которой настаивает компилятор. Писать эту проверку стоит в любом случае, потому что token со значением undefined представляет собой самый ясный сигнал, что был вызван не тот метод.
Коротко
Считайте эндпоинт задачи с виджета, передайте его в единственный метод ALTCHA вместе с URL страницы и отправьте token обратно в поле с именем altcha, ничего в нём не меняя. В типизированном проекте сузьте тип token один раз после решения, потому что один тип результата покрывает все типы капчи, а поля ALTCHA в нём необязательны. Держите запрос, решение и отправку в одном блоке, ведь окно задачи может закрыться быстрее чем за две минуты, а истёкшая задача выглядит ровно так же, как неверный ответ. Переключайтесь на Server mode в тот момент, когда процесс Node перестаёт делить машину с решателем.
- Что такое challenge и как работает этот тип: страница решения ALTCHA.
- Все остальные методы, которые предоставляет пакет для Node: страница сервиса распознавания капчи для Node.js.
И последнее, что меняет подход к повторным попыткам. Поскольку безлимитный сервис распознавания капчи вычисляет доказательство работы на машине, которой вы и так владеете, повторное решение истёкшего challenge стоит несколько миллисекунд вашего собственного CPU и больше ничего, так что вы вполне можете запросить свежий challenge вместо того, чтобы возиться с устаревшим.
