Как решить ALTCHA в PHP внутри синхронного запроса

Чтобы решить ALTCHA в PHP, нужен один вызов и никакого браузера. ALTCHA построена на доказательстве работы, а не на распознавании: сайт выдаёт задачу, и клиент должен хешировать до тех пор, пока не найдёт счётчик, который ей удовлетворяет. Смотреть тут не на что, поэтому ни WebDriver, ни headless-браузер не участвуют, а ответ вычисляется, а не угадывается. CapSkip добавил этот тип в версии 1.2.6, и пакет для PHP предоставляет его одним методом. PHP остаётся единственным из четырёх SDK, у которого вообще нет истории про параллельность, и именно это определяет всё руководство: вызов блокирует ваш запрос, пока он выполняется, поэтому главное правильно сделать так, чтобы собственные ограничения PHP не оборвали запрос раньше, чем ответит решатель.
Что понадобится
- CapSkip 1.2.6 или новее на машине с Windows. Поддержка ALTCHA появилась именно в этом выпуске.
- PHP 8.0 или новее с расширениями curl и json, которые входят в большинство сборок. Других зависимостей во время выполнения у пакета нет, поэтому он одинаково ложится и в обычный скрипт, и в Laravel, и в Symfony.
- URL страницы, на которой стоит виджет, и эндпоинт, у которого виджет запрашивает свою задачу.
- Адрес решателя. Local mode отвечает на 127.0.0.1 только для этого устройства; Server mode слушает ваш сетевой адрес или публичный IP, чтобы до него могла достучаться другая машина. Какой вариант подходит, разбирается в шаге 4, и оба настраиваются в разделе Настройки подключения.
# composer require capskip/capskip composer require capskip/capskip
Шаг 1: вызов решения и откуда берётся задача
Один метод, два аргумента: URL страницы, а затем массив опций с задачей. Передайте ему эндпоинт, и CapSkip сам получит задачу.
// composer require capskip/capskip
require 'vendor/autoload.php';
use CapSkip\CapSkip;
$solver = new CapSkip(['host' => '127.0.0.1', 'port' => 8080]);
// CapSkip fetches the challenge, then hashes until the counter fits.
$result = $solver->altcha('https://example.com/signup', [
'challenge_url' => 'https://example.com/altcha/challenge',
]);
echo $result['token']; // base64 payload for the form field
echo $result['number']; // the counter that satisfied itДва ключа возвращаемого массива принадлежат только ALTCHA. В token лежит base64-полезная нагрузка, которую ждёт форма, а в number лежит счётчик, который решил задачу. Ключ code содержит ту же строку, что и token, поэтому подойдёт любой из них, но token назван по имени поля, в которое попадает, и в месте вызова читается лучше. Ключи GeeTest и user agent для Turnstile здесь отсутствуют.
number стоит записывать в лог. Он сообщается для обоих поколений ALTCHA, хотя их полезные нагрузки различаются: устаревший token несёт счётчик на верхнем уровне, а token второго поколения с доказательством работы этого не делает и держит счётчик внутри объекта solution. CapSkip достаёт его из объекта solution в собственном ответе сервера, поэтому оба поколения сообщаются одинаково.
Найдите эндпоинт, который запрашивает виджет
Откройте DevTools, перейдите на вкладку Network и перезагрузите страницу, на которой стоит виджет. Виджет делает один запрос за своей задачей, обычно по пути, в котором есть altcha. Именно URL этого запроса вы и передаёте, а JSON, который он возвращает, представляет собой документ задачи, и его можно передать вместо URL.
Не угадывайте атрибут, который его задаёт: он менялся от одного поколения виджета к другому. Читайте исходный код страницы.
| Поколение виджета | Атрибут, который задаёт challenge |
|---|---|
| v1 и v2 | challengeurl для эндпоинта плюс отдельный атрибут challengejson для встроенного challenge |
| v3 и новее | challenge, причём этот же атрибут принимает либо URL, либо сами данные challenge |
Три стиля отображения, native, checkbox и switch, различаются только внешне. Они отправляют одну и ту же полезную нагрузку, и разница никогда не доходит до решателя, поэтому разбираться, какой из них перед вами, не нужно. Атрибуты ALTCHA описывает в собственном руководстве по интеграции.
Как передать документ challenge вместо эндпоинта
Если ваш код уже получил задачу, передайте документ, и никакого сетевого запроса не будет вообще. Этот путь подходит, когда задача приходит встроенной в страницу, а не с эндпоинта, или когда для её получения нужны cookie, которые есть у вашего скрипта и которых нет у решателя.
// No fetch happens: the document is already here.
$result = $solver->altcha('https://example.com/signup', [
'challenge_json' => [
'algorithm' => 'SHA-256',
'challenge' => 'YOUR_CHALLENGE_HASH',
'salt' => 'YOUR_SALT',
'signature' => 'YOUR_SIGNATURE',
'maxnumber' => 1000000,
],
]);Эта опция принимает массив, который сериализуется за вас, либо строку JSON, если она у вас уже есть. Передавать одновременно и эндпоинт, и документ разрешено, и побеждает встроенный документ, потому что запрос по сети лишь повторно получил бы то, что вы только что передали. Но под нагрузкой эти два пути ведут себя по-разному. Встроенная задача, у которой уже истёк срок, отклоняется сразу, а не хешируется впустую, тогда как эндпоинт позволяет решателю получить свежую задачу, если первая умерла, пока задание стояло в очереди.
Какие алгоритмы покрывает решатель
Один и тот же метод обслуживает оба поколения. Старая схема закрыта алгоритмами SHA-1, SHA-256, SHA-384 и SHA-512, а доказательство работы v2 закрыто PBKDF2 и итеративным SHA. PBKDF2 стоит по умолчанию и рекомендован самой ALTCHA, так что этим покрыто подавляющее большинство живых сайтов.
Исключения составляют Argon2id и scrypt: их не пытаются посчитать, а отклоняют. Задание с одним из них возвращается примерно за треть секунды с ERROR_CAPTCHA_UNSOLVABLE и никогда не повторяется. Так сделано намеренно. Повторная попытка не помогает против функции, требовательной к памяти, поэтому быстрый отказ лучше имитации бурной деятельности. Для ALTCHA такой результат указывает на алгоритм, а не на нечитаемую картинку, а у самого кода ошибки есть отдельное руководство.
Шаг 2: уложите решение в лимит выполнения
Это та часть, которая специфична для PHP, и именно она даёт самый запутанный сбой. Вызов блокирующий. Фоновой задачи у PHP здесь нет, а асинхронный клиент в пакете представляет собой лишь псевдоним, оставленный для единообразия с другими SDK, поэтому, пока работает решатель, ваш запрос просто стоит. Теперь друг против друга тикают два таймера, и ломаются они очень по-разному.
Удачное решение ALTCHA занимает миллисекунды, поэтому при нормальной работе ни один из таймеров не важен. Они важны на плохом пути: решатель занят очередью заданий reCAPTCHA, и вызов ждёт. Собственный потолок клиента для ALTCHA составляет тайм-аут опроса по умолчанию в 120 секунд, и ALTCHA использует именно его, а не более длинный тайм-аут для reCAPTCHA, потому что это работа процессора, а не сессия браузера.
| Параметр конструктора | По умолчанию | Что он покрывает |
|---|---|---|
| defaultTimeout | 120 секунд | Опрос ALTCHA и графической капчи |
| recaptchaTimeout | 300 секунд | Опрос reCAPTCHA, Turnstile и GeeTest |
| pollingInterval | Максимум 5 секунд | Опрос начинается с 0,25 секунды и увеличивает интервал до этого значения |
С другой стороны, собственная настройка PHP max_execution_time по умолчанию равна 30 секундам при веб-запросе и нулю, то есть без ограничения, в командной строке. Сработает ли она во время решения, зависит от платформы, и именно на этом люди попадаются. В Unix-подобных системах она не считает время, которое скрипт проводит в ожидании сокета, поэтому долгое ожидание решателя может целиком проскользнуть мимо неё. В Windows та же настройка измеряется в реальном времени, поэтому она срабатывает. В любом случае над ней есть другие потолки, которым всё равно: у PHP-FPM есть request_terminate_timeout, а у веб-сервера перед ним есть собственный тайм-аут чтения.
Важна разница в том, что вы получаете, когда побеждает каждый из них. Если первым достигается тайм-аут SDK, вы получаете TimeoutException, который ваш блок catch обрабатывает и превращает в осмысленный ответ. Если побеждает любой из двух других, скрипт убивают сразу и ни один блок catch не отрабатывает. Но и эти два не одинаковы: собственный лимит PHP завершает запрос фатальной ошибкой, которая попадает в журнал ошибок, и при этом всё равно выполняет ваши функции завершения, тогда как менеджер процессов или веб-сервер, который убивает воркер, не выполняет ничего и оставляет посетителю голый 502 или 504, в котором нет ничего полезного. Поэтому задавайте потолок клиента осознанно, ниже того, что оборвёт запрос.
// Keep the client's ceiling under whatever kills the request.
$solver = new CapSkip([
'host' => '127.0.0.1',
'port' => 8080,
'defaultTimeout' => 20, // ALTCHA and image CAPTCHA polling
]);Двадцати секунд с запасом хватает типу, который обычно укладывается в миллисекунды, и под веб-лимитом по умолчанию в 30 секунд остаётся место на остальную часть запроса. В командной строке, где лимита выполнения нет, оставьте значение по умолчанию как есть. Если решение регулярно подбирается к любому из этих чисел, проблема не в тайм-ауте, а в том, что решатель недоступен или перегружен, и поднятие потолка лишь заставит запрос дольше висеть, прежде чем он об этом скажет.
Шаг 3: отправляем токен обратно без изменений, пока он не истёк
Виджет отправляет свою полезную нагрузку в поле формы с именем altcha, значит туда же идёт и ваш токен. Именно на этом шаге всё тихо ломается.
// Send it exactly as it came back: no trimming,
// no re-encoding, no reordering.
$body = http_build_query([
'email' => '[email protected]',
'altcha' => $result['token'],
]);
$ch = curl_init('https://example.com/signup');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);token представляет собой base64 от документа JSON, поля которого покрыты HMAC-подписью самого сервера. Любое изменение делает его недействительным, поэтому всё, что выглядит как наведение порядка, сломает отправку: обрезка пробелов, декодирование и повторное кодирование или пересборка JSON с другим порядком ключей. Осторожнее с благонамеренными фильтрами ввода во фреймворке: санитайзер, применённый к исходящим данным формы, с удовольствием вырежет символ и оставит вам полезную нагрузку, которая больше не совпадает со своей подписью. Некоторые интеграции читают полезную нагрузку из поля тела JSON, а не из поля формы, поэтому посмотрите, что отправляет собственная форма страницы, и повторите это.
Второй способ завалить этот шаг связан со временем. Окна задач короткие, а некоторые сайты закрывают их меньше чем за две минуты. Когда окно истекает, сайт отклоняет ответ сухой ошибкой проверки, которая выглядит ровно так же, как неверный ответ, и в ответе нет ничего, что подсказало бы, что именно произошло. Помогают три привычки: получать задачу непосредственно перед решением, а не в начале долгого прогона, отправлять token в том же запросе, в котором он получен, и никогда не держать token в сессии, пока человек заполняет форму.
Шаг 4: где работает решатель и какой режим подключения для этого нужен
В примерах выше используется 127.0.0.1, потому что так правильно, когда PHP и решатель стоят на одной машине. Как только код работает в другом месте, например в контейнере, на веб-хостинге, на VPS или на CI-раннере, адрес обратной петли больше не указывает на решатель, и первое же решение выбрасывает NetworkException.
Переключите CapSkip в Server mode, и он вместо этого будет слушать ваш сетевой адрес или публичный IP, так что любой из этих вариантов достучится до него по тому же HTTP API. Если маршрут идёт через интернет, рекомендуется статический публичный IP плюс правило файрвола, которое пропускает только ожидаемые адреса. Server mode меняет только то, где слушает решатель, и больше ничего: это по-прежнему ваше оборудование и по-прежнему без счётчика решений. Читайте хост и порт из окружения, чтобы одно развёртывание работало в обоих случаях. Клиент сам не читает ни CAPSKIP_HOST, ни CAPSKIP_PORT, поэтому передайте их в конструктор, как это сделано в полном примере ниже.
| Где выполняется PHP | Какой режим подключения |
|---|---|
| На машине с CapSkip, в локальном dev-сервере или CLI-скрипте | Режим Local. 127.0.0.1 действительно верен |
| На другой машине в той же сети | Режим Server, по внутреннему адресу этой машины |
| На виртуальном хостинге, VPS или контейнерной платформе | Server mode со статическим публичным IP и правилом брандмауэра |
Одно замечание про прокси, специфичное для ALTCHA. Прокси здесь поддерживается, но используется только при получении challenge. Сессии браузера, которую нужно было бы маршрутизировать, тут нет, поэтому на само доказательство работы прокси не влияет.
Полный рабочий пример
// composer require capskip/capskip
require 'vendor/autoload.php';
use CapSkip\CapSkip;
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\NetworkException;
use CapSkip\Exceptions\TimeoutException;
$solver = new CapSkip([
'host' => getenv('CAPSKIP_HOST') ?: '127.0.0.1',
'port' => (int) (getenv('CAPSKIP_PORT') ?: 8080),
'defaultTimeout' => 20,
]);
try {
// Fetch, solve and submit inside the one request.
$result = $solver->altcha('https://example.com/signup', [
'challenge_url' => 'https://example.com/altcha/challenge',
]);
$body = http_build_query([
'email' => '[email protected]',
'altcha' => $result['token'],
]);
// POST $body to the form here, while the challenge is still fresh.
echo 'solved at counter ' . $result['number'];
} catch (ApiException $e) {
// ERROR_CAPTCHA_UNSOLVABLE here means Argon2id or scrypt.
echo 'refused: ' . $e->getMessage();
} catch (TimeoutException $e) {
echo 'gave up waiting, before anything could kill the request';
} catch (NetworkException $e) {
echo 'solver unreachable: check the host and the connection mode';
}Все четыре исключения наследуются от общего базового класса, поэтому, перехватив его вместо них, вы обработаете любой сбой, который может выбросить SDK, в одном блоке. Перехватывайте конкретные исключения, когда ответ различается, как выше, и базовый класс, когда не различается.
Остальные типы устроены так же, только метод другой. Вызов для reCAPTCHA принимает sitekey и URL страницы, Turnstile работает точно так же, GeeTest вместе с URL страницы принимает значение gt и challenge, а решение картинок принимает путь к файлу, URL или base64. Полный список методов есть на странице сервиса распознавания капчи для PHP.
Turnstile остаётся единственным типом, которому нужен не только sitekey, когда он приходит полноценной страницей проверки. Его дополнительные значения разобраны в руководстве по Turnstile для PHP.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| 502 или 504, в логах пусто, и ни один блок catch не отработал | Менеджер процессов или веб-сервер убил запрос раньше, чем SDK сдался | Задайте defaultTimeout ниже тайм-аута завершения в FPM и тайм-аута чтения у веб-сервера |
| В логе PHP фатальная ошибка о превышении максимального времени выполнения, и ни один блок catch не отработал | Собственный лимит PHP завершил запрос первым | Задайте defaultTimeout ниже max_execution_time |
| TimeoutException с указанием секунд, которые он прождал | Решатель не ответил в пределах потолка клиента | Проверьте, что решатель запущен и не перегружен. Поднятие потолка лишь отложит тот же самый ответ |
| Пустая ошибка проверки от сайта при токене, который выглядит нормально | Challenge истёк до того, как форма была отправлена | Получайте задачу, решайте и отправляйте в одном запросе |
| ERROR_CAPTCHA_UNSOLVABLE внутри ApiException, примерно через треть секунды | Challenge использует Argon2id или scrypt | Повторять нечего. Эти два отклоняются намеренно |
| ValidationException при вызове | Не передан ни один из двух параметров задачи, либо передан параметр, который ALTCHA не принимает | Передайте эндпоинт challenge или документ challenge, а всё остальное уберите |
| NetworkException на первом решении | CapSkip не запущен либо неверны хост и порт | Запустите CapSkip, затем решите, нужен ему Local mode или Server mode |
| Ключа token нет в массиве | Этот ключ заполняется только для ALTCHA | Вызовите метод ALTCHA. В результате ALTCHA ключ code содержит ту же строку |
| Форма отклоняет токен, который по логам был решён | Что-то перекодировало, обрезало или переупорядочило полезную нагрузку | Передавайте строку напрямую, не трогая её |
FAQ
Нужен ли браузер, чтобы решить ALTCHA в PHP?
Нет, и именно это делает её удачным вариантом для PHP. ALTCHA выдаёт задачу на хеширование, а не что-то, на что надо смотреть, поэтому работа идёт только на процессоре и заканчивается за миллисекунды. Не нужно ставить WebDriver и держать Chromium рядом с веб-сервером, а ведь именно это делает неудобными типы капчи, которые требуют браузера, когда вы работаете из PHP. Достаточно обычного скрипта с curl.
Достучится ли PHP на виртуальном хостинге или VPS до решателя?
Да. Переключите CapSkip в Server mode в настройках подключения, чтобы он слушал сетевой адрес, а не адрес обратной петли, и укажите этот адрес в переменной окружения с хостом. Виртуальный хостинг, VPS, контейнерная платформа и CI-раннер подключаются одинаково, по тому же HTTP API. Если маршрут идёт через интернет, используйте статический публичный IP и ограничьте его правилом файрвола. Решатель во всех этих случаях остаётся на вашем собственном оборудовании, поэтому ни лицензия, ни количество решений не меняются.
Можно ли решать несколько задач ALTCHA одновременно в PHP?
Из одного скрипта нет. Клиент для PHP синхронный, а асинхронное имя в пакете представляет собой псевдоним, оставленный для того, чтобы четыре SDK читались одинаково, а не вторую реализацию, поэтому вызовы идут один за другим. Параллельность здесь означает запуск нескольких рабочих процессов, и именно так PHP решает такие задачи вообще. Для этого типа она редко важна, ведь решение занимает миллисекунды хеширования, но знать об этом стоит, прежде чем планировать вокруг него массовый прогон.
Решать во время веб-запроса или в задании из очереди?
Решайте в запросе, когда отправка происходит в том же запросе, а это обычный случай, потому что окно задачи короткое, а задание из очереди добавляет задержку без всякой пользы. Переносите решение в воркер, когда окружающая работа и так асинхронная, например когда парсер обходит много страниц. Чего делать точно нельзя, так это разделять эти два шага: если получить задачу в одном запросе, а решить её в более позднем задании, вы почти наверняка отдадите сайту истёкший ответ.
Коротко
Считайте эндпоинт задачи с виджета, передайте его в единственный метод ALTCHA вместе с URL страницы и отправьте token обратно в поле с именем altcha, ничего в нём не меняя. Задайте тайм-аут клиента по умолчанию ниже того, что первым убьёт запрос, потому что TimeoutException, который можно перехватить, стоит намного больше, чем запрос, завершённый менеджером процессов. Держите получение задачи, решение и отправку в одном запросе, ведь окно задачи может закрыться быстрее чем за две минуты, а истёкшая задача выглядит ровно так же, как неверный ответ. Переключайтесь на Server mode в тот момент, когда PHP перестаёт делить машину с решателем.
- Что такое challenge и как работает этот тип: страница решения ALTCHA.
- Все остальные методы, которые предоставляет пакет для PHP: страница сервиса распознавания капчи для PHP.
И последнее, что меняет подход к повторным попыткам. Поскольку такой обход капчи вычисляет доказательство работы на машине, которой вы и так владеете, повторное решение истёкшего challenge стоит несколько миллисекунд вашего собственного CPU и больше ничего, так что вы вполне можете запросить свежий challenge вместо того, чтобы возиться с устаревшим.
