Как решить виджет AWS WAF CAPTCHA в Node.js

aws waf captcha in node.js - How to Solve an AWS WAF Widget CAPTCHA With Node.js

Чтобы решить AWS WAF CAPTCHA в Node.js на странице с виджетом, считайте со страницы apiKey виджета и адрес jsapi.js и отправьте их в решатель CapSkip для AWS WAF с методом amazonWaf, обменяйте полученную пару voucher на токен и отправьте этот токен со следующим запросом. Виджет является второй формой, которую использует AWS WAF. Форма или страница входа рисует пазл сама, поэтому страницы с HTTP 405 нет и читать gokuProps не нужно. В этом руководстве весь цикл выполняется с помощью CapSkip Node.js SDK и fetch, который поставляется с Node.js. Для страницы 405 есть отдельное руководство по Python.

Что понадобится

  • CapSkip 3.0.0 или новее, запущенный на ПК с Windows. В этом выпуске появилось решение AWS WAF. Пошаговое руководство по настройке проведёт вас через первый запуск.
  • Node.js 18 или новее (этого требует пакет) и пакет capskip версии 1.4.0 или новее. В более ранних версиях метода amazonWaf нет. Код ниже запускался на Node.js 24.22.0 с capskip 1.4.0. Другие версии Node.js здесь не пробовали.
  • Окно PowerShell. Команды написаны в синтаксисе PowerShell.
  • Страница, которой вы владеете или которую вам разрешено автоматизировать, с виджетом AWS WAF CAPTCHA на ней, и браузер с инструментами разработчика, чтобы один раз её осмотреть.
  • Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства. В режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы скрипт на другом ПК, VPS или размещённой платформе мог обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.

Как это проверялось: код запускался на трёх серверах-имитациях, которые я написал, а не на настоящем сайте с AWS WAF. Один копирует проверки AWS WAF, перечисленные в разделе Документация API, второй изображает сайт с виджетом на странице контактов, а третий изображает хост токенов AWS. Использовался настоящий SDK capskip 1.4.0. Этот код не запускался на настоящем приложении CapSkip, и настоящий сайт с AWS WAF в проверке не участвовал, поэтому ни одно число ниже не является временем решения, и я не могу сказать, что конкретный живой сайт принимает токен, который даёт эта схема.

Шаг 1: настраиваем проект и находим значения виджета

Создайте папку, инициализируйте проект и установите capskip:

npm init -y
npm install capskip

Страница с виджетом загружает скрипт jsapi.js и вызывает AwsWafCaptcha.renderCaptcha с объектом параметров. Согласно документации AWS WAF, apiKey в этом объекте является зашифрованным ключом, в котором перечислены клиентские домены, которым разрешено использовать виджет, поэтому он принадлежит одному сайту. CapSkip нужны две вещи со страницы: этот apiKey, который он называет sitekey, и полный адрес jsapi.js. На странице с виджетом нет gokuProps, поэтому вы не отправляете ни iv, ни context. Именно отправка адреса jsapi.js без них помечает запрос как решение виджета.

Первый блок читает оба значения из HTML страницы. Некоторые сайты хранят apiKey в отдельном пакете JavaScript. Тогда функция возвращает null, и нужно поискать apiKey в скриптах, которые загружает страница.

'use strict';
const { CapSkip, ApiException } = require('capskip');

const PAGE_URL = 'https://www.example.com/contact'; // the page that draws the widget
// The folder that the page's challenge.js loads from (see the Network tab).
const TOKEN_BASE = 'https://3f2a9c7d41b0.edge.sdk.awswaf.com/3f2a9c7d41b0/8e51d0c7a2f4/';

async function readWidget() {
  const res = await fetch(PAGE_URL, { headers: { Accept: 'text/html' } });
  const html = await res.text();
  const src = /<script[^>]+src="([^"]*\/jsapi\.js)"/i.exec(html);
  const key = /apiKey\s*:\s*["']([^"']+)["']/.exec(html);
  if (!src || !key) return null; // not in the HTML: look in the page's scripts
  return { sitekey: key[1], jsapiScript: new URL(src[1], PAGE_URL).href };
}

Адрес скрипта на странице может быть относительным или начинаться с двух слэшей, поэтому функция превращает его в полный адрес с помощью URL страницы. TOKEN_BASE в начале блока является константой, которую вы заполняете вручную, а почему, объясняет следующий шаг.

Шаг 2: один раз находим хост токенов

Пара voucher обменивается на токен на собственном хосте AWS. На странице с виджетом этого хоста нет в загруженном вами HTML. Откройте страницу в браузере, откройте вкладку Network, перезагрузите страницу и найдите запрос к challenge.js, который jsapi.js делает за вас. Нужна его папка, оканчивающаяся слэшем. В документации CapSkip сказано заменить challenge.js на voucher, и именно это делает код следующего шага с TOKEN_BASE.

Это один поиск на сайт, поэтому храните его в конфигурации. Я не могу сказать, как долго он остаётся прежним на настоящем сайте. Если обмен на следующем шаге начнёт завершаться ошибкой, сначала снова проверьте вкладку Network, прежде чем подозревать что-то другое.

Шаг 3: решаем и обмениваем voucher

Метод SDK называется amazonWaf. Обязательны только sitekey и URL страницы, а адрес jsapi.js передаётся в параметрах. Адрес скрипта должен быть https-адресом на хосте awswaf.com, а SDK отклоняет любой другой ещё до отправки запроса. Ответ приходит как объект с captcha_voucher и existing_token, а также с необработанным ответом в code и номером задания в captchaId. Метод ждёт с более длинным таймаутом в 300 секунд, как и для reCAPTCHA, потому что CapSkip решает пазл в настоящем браузере. Время я не замерял.

Обмен выполняется POST-запросом на эндпоинт voucher, пара передаётся в теле JSON. В документации CapSkip тело отправляется как text/plain с заголовком Origin, который соответствует сайту. Собственный код приложения CapSkip отправляет ещё и Referer, поэтому код отправляет оба. fetch в Node.js позволяет задать Origin, чего браузер не позволил бы. В документации SDK сказано, что пара живёт недолго, поэтому обменивайте её сразу и никогда не храните.

// Local mode. In Server mode, use your server's address instead.
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });

async function getToken(widget) {
  // The answer is a voucher pair, not a token.
  const result = await solver.amazonWaf(widget.sitekey, PAGE_URL, {
    jsapiScript: widget.jsapiScript,
  });
  const reply = await fetch(new URL('voucher', TOKEN_BASE), {
    method: 'POST',
    headers: {
      'Content-Type': 'text/plain;charset=UTF-8',
      Origin: new URL(PAGE_URL).origin,
      Referer: new URL(PAGE_URL).origin + '/',
    },
    body: JSON.stringify({
      captcha_voucher: result.captcha_voucher,
      existing_token: result.existing_token,
    }),
  });
  if (!reply.ok) throw new Error(`voucher exchange failed: HTTP ${reply.status}`);
  return (await reply.json()).token; // the aws-waf-token value
}

Чтобы направить решение через прокси, добавьте в вызов параметр proxy: объект, в котором type равен HTTP, а uri равен user:[email protected]:8080. При запуске на имитации в in.php это пришло как proxy и proxytype. Для проверки одного адреса достаточно, но сайты за AWS WAF часто сочетают CAPTCHA с ограничением частоты запросов, поэтому постоянный поток решений с одного адреса вызывает более жёсткие меры. В разделе Документация API предлагается список прокси в настройках CapSkip или прокси на отдельный вызов, когда токен должен прийти из определённой сети. Собственный решатель CapSkip отправляет загрузки страниц, пазл и обмен voucher через один прокси, поэтому AWS видит один адрес. Если вы передаёте прокси в решение, отправляйте собственные запросы через тот же. Эту часть я проверить не смог.

Шаг 4: отправляем токен так, как это делает страница

На странице с виджетом токен тот же, который получает собственный коллбэк onSuccess страницы. Страницы по-разному передают его дальше. Многие отправляют его как cookie aws-waf-token, другие как заголовок x-aws-waf-token, поэтому один раз отправьте форму вручную с открытой вкладкой Network и скопируйте то, что несёт запрос страницы. Функция ниже отправляет cookie и показывает заголовок как альтернативу. Поля формы здесь являются заглушками для ваших.

function submitForm(token) {
  return fetch(PAGE_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Cookie: `aws-waf-token=${token}`, // or 'x-aws-waf-token': token
    },
    body: new URLSearchParams({ email: '[email protected]', message: 'Hello' }),
  });
}

Шаг 5: полный рабочий пример

Последний блок собирает части в готовый решатель AWS WAF CAPTCHA в Node.js с повтором и точкой входа. Сохраните все четыре блока кода в один файл, один за другим, и запустите его командой node. Неудачный пазл вызывает ApiException, и цикл пробует снова, каждый раз получая новый пазл. Любая другая ошибка выбрасывается дальше, поэтому неверный хост или отклонённый обмен не повторяются молча.

async function main() {
  const widget = await readWidget();
  if (!widget) throw new Error('no jsapi.js script or apiKey in the page HTML');
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      const token = await getToken(widget); // exchange at once, never store it
      const res = await submitForm(token);
      console.log(res.status, await res.text());
      return;
    } catch (err) {
      if (!(err instanceof ApiException)) throw err;
      console.log(`attempt ${attempt}: ${err.message}`); // each retry draws a new puzzle
    }
  }
  throw new Error('AWS WAF CAPTCHA not solved in 3 attempts');
}

main().catch((err) => {
  console.error(`${err.constructor.name}: ${err.message}`);
  process.exitCode = 1;
});

При запуске на имитации скрипт вёл себя так:

Настройка имитацииЧто сделал скрипт
Каждый пазл решёнОдно задание, один обмен voucher, затем статус 200 и текст Message sent
Первые два пазла отклоненыДве строки попыток, затем статус 200 на третьем задании
Первые три пазла отклоненыТри строки попыток, затем строка с ошибкой и код выхода 1

Cookie действует столько же, сколько время иммунитета сайта. По умолчанию AWS задаёт 300 секунд, а владелец сайта может выбрать от 60 секунд до трёх суток. Это описано в документации AWS WAF подробно. Если вы отправляете много запросов, сохраните токен и используйте его, пока он не истечёт, а не решайте капчу на каждый запрос.

Частые ошибки

ОшибкаПричинаИсправить
в HTML страницы нет ни скрипта jsapi.js, ни apiKeyСтраница строит виджет из пакета, поэтому в HTML этих значений нетНайдите apiKey и jsapi.js в загруженных скриптах и передайте значения вручную
ValidationExceptioniv без context или адрес скрипта, который не является полным https-адресом на хосте awswaf.comДля виджета не отправляйте iv и context и копируйте адрес jsapi.js точно так, как он записан на странице
ApiException с ERROR_CAPTCHA_UNSOLVABLEПазл не удалось решитьОтправьте снова. В разделе Документация API сказано, что каждая попытка получает новый пазл
ERROR_TASK_NOT_SUPPORTEDДокументация API указывает это для типа задачи, который отключён в CapSkipПроверьте настройки AWS WAF в приложении CapSkip
ERROR_PROXY_FORMATСтроку прокси не удалось разобратьИспользуйте user:pass@ip:port
ERROR_NO_SLOT_AVAILABLEУ API-ключа есть ограничение частоты запросов, и вы его превысилиПодождите несколько секунд и отправьте снова
NetworkExceptionCapSkip не запущен либо неверно указаны хост или портЗапустите CapSkip и проверьте настройки подключения
voucher exchange failed: HTTP 4xxСкорее всего, пара устарела или не совпадает, либо неверен TOKEN_BASE. Код приложения CapSkip ожидает от AWS HTTP 400 для недействительного existing_token. Воспроизвести это на настоящем AWS я не смогОбменивайте сразу, проверьте папку на вкладке Network и отправляйте новое задание
Отправка формы всё ещё отклоняетсяТокен истёк, либо страница передаёт его в заголовке, а вы отправили cookieСкопируйте то, что несёт собственный запрос страницы

Запуск решателя на сервере

Скрипту от CapSkip нужны две вещи: хост и порт. В режиме Local это 127.0.0.1 и ваш собственный ПК. В режиме Server CapSkip слушает ваш сетевой адрес или публичный IP, поэтому скрипт на другом ПК, VPS или размещённой платформе может обращаться к нему через тот же API. Смените хост и порт в конструкторе CapSkip, и больше в коде ничего не меняется. Для стабильного удалённого доступа рекомендуется статический публичный IP. Режим Server всё равно работает на вашем оборудовании и всё равно без счётчика. Но он открывает порт для любого адреса, поэтому добавьте в Windows Firewall блокирующее правило для адресов, которым доступ к нему не нужен. Смотрите раздел Настройки подключения с описанием переключения режимов.

FAQ

Как отличить страницу с виджетом от страницы проверки с кодом 405?

Страница проверки отвечает на запрос HTTP 405 и определяет gokuProps. Страница с виджетом является обычной страницей, которая загружает jsapi.js и вызывает renderCaptcha, обычно на форме. Оба случая обрабатывает один и тот же метод amazonWaf, только с разными значениями.

Можно ли решить AWS WAF CAPTCHA в Node.js без браузера?

Скрипту браузер не нужен. Он отправляет обычные HTTP-запросы к CapSkip и к сайту, а пазл решает CapSkip в настоящем браузере на той машине, где он запущен, как описано в документации SDK. Вам самим браузер нужен только один раз, чтобы прочитать значения со страницы.

Возвращает ли SDK токен напрямую?

Нет. Он возвращает captcha_voucher и existing_token, а обменять их нужно на эндпоинте voucher. JSON API createTask тоже принимает AWS WAF, и несколько его форматов возвращают токен сразу. В разделе Документация API они перечислены.

Может ли размещённая платформа запускать это через мой CapSkip?

Да, через режим Server. Настройте CapSkip на прослушивание вашей сети или публичного IP, направьте конструктор CapSkip на этот хост и порт, и цикл работает так же, как на одном ПК. Решатель остаётся на вашем оборудовании.

Коротко

AWS WAF CAPTCHA в Node.js решается так же, как на странице 405: пазл тот же, но расположен в другом месте. Прочитайте apiKey и адрес jsapi.js, вызовите amazonWaf, обменяйте пару на эндпоинте voucher и отправьте токен так, как это делает страница. CapSkip обеспечивает обход капчи на вашей собственной машине, поэтому пазл, на который он не смог ответить, стоит вам повтора, а не платы. Об остальных возможностях SDK читайте на странице сервиса распознавания капчи для Node.js.