Как решить Friendly Captcha на C# (виджеты v1 и v2)

solve friendly captcha in c# - How to Solve Friendly Captcha in C# (v1 and v2 Widgets)

Чтобы решить Friendly Captcha на C#, вызовите FriendlyCaptchaAsync с sitekey, URL страницы и версией виджета, а затем отправьте токен в то поле формы, которое ждёт эта версия. Всё дело в версии. Под одним названием Friendly Captcha поставляет два разных протокола, и у них общее пространство sitekey, поэтому по самому ключу не понять, какой из них работает на сайте. Решите не тот, и вы получите корректно сформированный токен, который сайт отклонит, не объясняя причин. CapSkip добавил Friendly Captcha в версии 1.4.0. В этом руководстве разобрано, как отличить одну версию от другой, как сделать вызов и как отправить токен.

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

  • CapSkip 1.4.0 или новее на машине с Windows. Поддержка Friendly Captcha появилась именно в этом выпуске, вместе с CaptchaFox и Capy Puzzle.
  • Пакет CapSkip для .NET версии 1.3.0 или новее: именно в этом выпуске появился FriendlyCaptchaAsync. Он собран под .NET Standard 2.0, так что подходят .NET Framework 4.6.1 и выше, .NET Core 2.0 и выше, а также .NET 6 и новее. В примерах используются операторы верхнего уровня, поэтому запускайте их на .NET 6 или новее.
  • Три значения с целевой страницы: sitekey на элементе виджета, URL самой страницы и адрес скрипта виджета, который загружает страница. Где лежит каждое из них, показывает шаг 1.
  • Адрес решателя. Режим Local отвечает на 127.0.0.1 и только для этого устройства; режим Server слушает ваш сетевой адрес или публичный IP, чтобы другая машина могла обращаться к нему через API. Оба режима задаются в разделе Настройки подключения, а какой из них нужен именно вам, разбирает шаг 4.
# dotnet add package CapSkip
dotnet add package CapSkip

Шаг 1: отличаем Friendly Captcha v1 от v2

Обе версии выводят один и тот же элемент: div с классом frc-captcha и атрибутом data-sitekey. Поэтому элемент говорит, где лежит sitekey, но ничего не говорит о протоколе. Версию выдаёт тег script, который загружает виджет, потому что v1 и v2 представляют собой разные пакеты с разными именами файлов.

Что сравниваемВерсия 1Версия 2
Пакет, который загружает страницаfriendly-challenge@friendlycaptcha/sdk
Файл скриптаwidget.module.min.js, а в качестве запасного варианта widget.min.jssite.min.js, а в качестве запасного варианта site.compat.min.js
Поле формы, куда идёт токенfrc-captcha-solutionfrc-captcha-response
Как выглядит токенЧетыре части, разделённые точками, длиной в несколько сотен символовОдна непрозрачная строка, которая начинается с AQQA и точки, примерно шесть килобайт

Откройте исходный код страницы, найдите frc-captcha, чтобы отыскать элемент, а затем прочитайте теги script, которые его загружают. Ту же проверку Friendly Captcha описывает на собственной странице о двух версиях. Если сайт хостит виджет у себя, имя пакета может пропасть из URL, поэтому ориентируйтесь на имя файла. Не гадайте по возрасту сайта: обе версии живы, и Friendly Captcha заявляет, что продолжит поддерживать v1 ещё несколько лет.

Шаг 2: вызов FriendlyCaptchaAsync

Один метод, три аргумента: sitekey, URL страницы и словарь опций. Передайте версию, которую вы нашли на шаге 1.

// dotnet add package CapSkip
using CapSkip;

var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);

// Tell the solver which protocol the site runs.
var result = await solver.FriendlyCaptchaAsync(
    "YOUR_SITEKEY",
    "https://example.com/signup",
    new Dictionary<string, object?> { ["version"] = "v2" });

Console.WriteLine(result.Token);   // goes in frc-captcha-response

Опция version принимает v1 или v2, подойдут и просто 1 или 2. Любое другое значение SDK отклоняет с ValidationException ещё до отправки запроса. Так задумано: догадка вернулась бы токеном, который выглядит корректным, но сайт его выбросит, а это хуже ошибки. При этом значение null не отклоняется. Оно просто отбрасывается, и решатель откатывается к v1, как описано ниже.

Читайте свойство Token. Свойство Code несёт ту же строку, но Token назван по имени поля, куда он попадает. User agent здесь остаётся null, потому что его возвращают только Turnstile и CaptchaFox.

Пусть версию определит адрес скрипта

Если у вашего кода уже есть адрес скрипта виджета, передайте его вместо версии, и CapSkip определит версию по сборке, которую сайт действительно загружает: надёжнее сигнала не бывает. Решатель проверяет всё в фиксированном порядке и останавливается на первом ответе: сначала опция version, затем адрес скрипта, затем значение по умолчанию v1.

// Instead of the call above: copy the src straight off the page's module script tag.
var result = await solver.FriendlyCaptchaAsync(
    "YOUR_SITEKEY",
    "https://example.com/signup",
    new Dictionary<string, object?>
    {
        ["module_script"] =
            "https://cdn.jsdelivr.net/npm/@friendlycaptcha/[email protected]/site.min.js",
    });

Опция nomodule_script делает то же самое с запасным скриптом, если у вас именно он. Но обратите внимание на последний шаг этого порядка. Если нет ни версии, ни адреса скрипта, решатель считает, что перед ним v1, поэтому сайт на v2, решённый по одному только sitekey, ломается ровно так, как предупреждает всё это руководство.

Сайты на эндпоинте EU

Friendly Captcha продаёт опцию резидентности данных, при которой головоломки раздаются только из Германии. Виджет v2 на таком сервисе несёт атрибут data-api-endpoint, обычно со значением eu, и если вы его видите, передайте то же значение в опции api_server. Эта опция принимает global (значение по умолчанию), eu или полный URL. Оба эндпоинта выпустят токен для одного и того же sitekey, поэтому ошибку здесь поймает только собственная проверка сайта, а это снова тихий вид сбоя.

Шаг 3: отправляем токен в правильное поле

На имени поля спотыкаются во вторую очередь, обычно сразу после того, как рабочую интеграцию с v1 направляют на сайт с v2. Виджет пишет свой токен в скрытый input, и ваш запрос должен положить его туда же.

// version holds what you passed in Step 2; http is your HttpClient.
// v1 reads frc-captcha-solution, v2 reads frc-captcha-response.
var isV2 = version is "v2" or "V2" or "2";
var field = isV2 ? "frc-captcha-response" : "frc-captcha-solution";

var body = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["email"] = "[email protected]",
    [field] = result.Token!,
});

var response = await http.PostAsync("https://example.com/signup", body);

Сверху ещё две детали. Сайт может переименовать поле атрибутом на элементе виджета: data-form-field-name в v2 и data-solution-field-name в v1, так что проверьте, есть ли он, и если есть, используйте его значение. А некоторые интеграции отправляют токен в теле JSON, а не обычной отправкой формы, поэтому откройте DevTools, один раз отправьте форму вручную и повторите ровно то, что отправляет страница.

С v2 важен размер. Токен примерно в шесть килобайт спокойно помещается в тело POST, но может превысить лимит сервера на длину строки запроса или обрезаться узкой колонкой базы данных, а обрезанный токен проваливает проверку так же, как неверный. Держите его целым и отправляйте один раз. Проверка Friendly Captcha в обеих версиях отклоняет ответ, который истёк или уже был использован, так что токен годится для одной отправки и уходить должен без промедления.

Шаг 4: таймауты и где работает решатель

Friendly Captcha построена на доказательстве работы, но не из тех, что решаются за миллисекунды. Сервис решает, сколько работы стоит каждый запрос, в тот момент, когда запрос сделан, и повышает эту планку для адресов, которые уже видел много раз. К тому же CapSkip решает каждый виджет v2 в настоящем браузере. Поэтому метод работает на удлинённом таймауте опроса для reCAPTCHA, а не на обычном.

Параметр конструктораПо умолчаниюЧто он покрывает
recaptchaTimeout300 секундОпрос Friendly Captcha, CaptchaFox, reCAPTCHA и GeeTest
defaultTimeout120 секундОпрос графической капчи, ALTCHA и Capy
pollingIntervalМаксимум 5 секундОпрос начинается с 0,25 секунды и увеличивает интервал до этого значения

Если на долгом прогоне решения замедляются, вероятная причина именно в этой растущей сложности, а лечится она большим количеством адресов. Метод принимает прокси на каждый запрос, а пул прокси, настроенный в CapSkip, распределяет нагрузку за вас. Здесь и то и другое стоит завести раньше, чем для большинства типов.

В примерах стоит 127.0.0.1, потому что это верно, пока ваш код и решатель делят одну машину. Как только вызывающий код запускается где-то ещё, например в контейнере, на сборочном агенте или на VPS, локальная петля указывает не на ту машину, и первое же решение бросает NetworkException. Переключите CapSkip в режим Server, и он начнёт слушать ваш сетевой адрес или публичный IP, так что любой из этих вариантов сможет обратиться к нему через API. Если маршрут идёт через интернет, используйте статический публичный IP с правилом файрвола для ожидаемых адресов. Это по-прежнему ваше железо, и по-прежнему без платы за каждое решение.

Клиент сам не читает переменные окружения. Читайте CAPSKIP_HOST в своём коде и передавайте в конструктор, как это делает полный пример, чтобы одна и та же сборка работала и у вас на столе, и на сервере.

Полный рабочий пример

// dotnet add package CapSkip
using System.Text.RegularExpressions;
using CapSkip;

var pageUrl = "https://example.com/signup";
var http = new HttpClient();
var solver = new CapSkipClient(
    host: Environment.GetEnvironmentVariable("CAPSKIP_HOST") ?? "127.0.0.1",
    port: 8080);

// Read the sitekey off the widget element, in either attribute order.
var html = await http.GetStringAsync(pageUrl);
var widget = Regex.Match(html,
    "<[^>]*class=\"(?:[^\"]*\\s)?frc-captcha(?:\\s[^\"]*)?\"[^>]*>").Value;
var sitekey = Regex.Match(widget, "data-sitekey=\"([^\"]+)\"").Groups[1].Value;

// The package name in the script URL decides the version.
var v2 = html.Contains("@friendlycaptcha/sdk");
var v1 = html.Contains("friendly-challenge");
if (v1 == v2)
    throw new InvalidOperationException("Read the script tag and set the version by hand.");
var version = v2 ? "v2" : "v1";

try
{
    var result = await solver.FriendlyCaptchaAsync(sitekey, pageUrl,
        new Dictionary<string, object?> { ["version"] = version });

    // A site can rename the field on the widget element.
    var renamed = Regex.Match(widget,
        "data-(?:form|solution)-field-name=\"([^\"]+)\"").Groups[1].Value;
    var field = renamed.Length > 0 ? renamed
        : v2 ? "frc-captcha-response" : "frc-captcha-solution";
    var body = new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["email"] = "[email protected]",
        [field] = result.Token!,
    });

    // Post wherever the form's action attribute points.
    var response = await http.PostAsync(pageUrl, body);
    Console.WriteLine($"{(int)response.StatusCode} with a {version} token");
}
catch (CapSkip.ValidationException ex)
{
    // An empty sitekey or an unknown version, refused before any request.
    Console.WriteLine($"not sent: {ex.Message}");
}
catch (CapSkip.TimeoutException)
{
    Console.WriteLine("gave up waiting; recaptchaTimeout is 300 seconds");
}

Версия определяется один раз и используется дважды: для решения и для имени поля, поэтому они никогда не разойдутся. Если регулярное выражение ничего не находит, обычно это значит, что виджет создан из JavaScript, а не записан в HTML, и sitekey нужно читать с отрендеренной страницы или из скрипта, который его создаёт. Сырой эндпоинт, который стоит за этим методом, со всеми параметрами, которые он принимает, описывает справочник API, а все остальные методы, которые предоставляет пакет, перечислены на странице сервиса распознавания капчи для C#.

Частые ошибки и что они означают

Что вы видитеПричинаИсправить
Сайт отклоняет токен, который CapSkip вернул как решённыйРешена не та версия: часто к сайту на v2 применяется v1 по умолчаниюПрочитайте тег script и передайте версию либо передайте адрес скрипта
Сайт отклоняет токен, хотя версия вернаяТокен попал в поле другой версии, или сайт переименовал полеИспользуйте поле этой версии или имя из data-form-field-name в v2 либо data-solution-field-name в v1
Сайт отклоняет токен, а у виджета задан data-api-endpointСайт работает на региональном эндпоинте, а при решении использовался эндпоинт по умолчаниюПередайте значение атрибута в опции api_server
ValidationException ещё до какой-либо отправкиSitekey или URL страницы были пустыми, версия не была v1, v2, 1 или 2, либо в опциях был ключ, который метод не принимаетПроверьте, что регулярное выражение нашло элемент, исправьте значение версии и уберите неизвестную опцию
В скачанном HTML нет элемента frc-captchaСтраница создаёт виджет из JavaScriptПрочитайте sitekey с отрендеренной страницы или из скрипта, который его строит
Токен, который сработал один раз, не проходит при второй отправкеFriendly Captcha принимает каждый ответ один раз, и ответы истекаютРешайте заново для каждой отправки и отправляйте сразу
Решения замедляются по ходу долгого прогонаСервис повышает объём работы для адреса, который уже видел много разРаспределите решения по пулу прокси
NetworkException на первом решенииCapSkip не запущен либо неверны хост и портЗапустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server
Сборка падает на неоднозначном TimeoutExceptionЭто короткое имя определено и в CapSkip, и в SystemПишите CapSkip.TimeoutException полностью или перехватывайте CapSkipError

FAQ

Нужен ли мне браузер на своей стороне, чтобы решить Friendly Captcha на C#?

Нет. Вашему коду нужен HttpClient и больше ничего. CapSkip выполняет работу на той машине, где он запущен, и для v2 это означает запуск виджета в настоящем браузере прямо там: отчасти поэтому метод и использует удлинённый таймаут. Ваша сторона отправляет sitekey, URL и версию и получает обратно строку для отправки, так что код спокойно работает внутри worker-сервиса или задачи по расписанию.

Чем это отличается от решения ALTCHA?

Обе построены на доказательстве работы, и на этом сходство заканчивается. С ALTCHA вы передаёте challenge или эндпоинт, с которого он приходит, хеширование занимает миллисекунды, а метод работает на обычном таймауте в 120 секунд. С Friendly Captcha сложность каждого запроса задаёт сервис и повышает её для слишком активных адресов, v2 решается в браузере, а метод работает на таймауте в 300 секунд. К тому же у ALTCHA одно имя поля, а у Friendly Captcha два. Сторона ALTCHA разобрана в руководстве по ALTCHA на C#.

Может ли приложение .NET на хостинг-платформе достучаться до решателя?

Да. Переведите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю, прочитайте этот адрес из CAPSKIP_HOST в своём коде и передайте его клиенту. Хост контейнеров, VPS, агент CI и управляемый сервис приложений подключаются одинаково, через один и тот же HTTP API. Если маршрут идёт через интернет, используйте статический публичный IP с правилом файрвола. Решатель остаётся на вашем собственном железе, поэтому ни лицензия, ни счётчик решений не меняются.

Можно ли нарешать пачку токенов заранее?

Толку от этого не будет. Каждый ответ принимается один раз и истекает, и Friendly Captcha сообщает сайту об обоих случаях как о провалах. Запас превращается в кучу отказов. Решайте тогда, когда вот-вот будете отправлять, а работу пусть делает параллельность: клиент асинхронный, поэтому Task.WhenAll над несколькими решениями запускает их бок о бок.

Коротко

Чтобы решить Friendly Captcha на C#, найдите скрипт виджета, определите по нему v1 или v2 и передайте эту версию в FriendlyCaptchaAsync вместе с sitekey и URL страницы. Положите токен в frc-captcha-solution для v1 или в frc-captcha-response для v2, если сайт не переименовал поле, и отправьте его один раз, сразу же. Если виджет использует эндпоинт EU, укажите его и вы, рассчитывайте на решения за секунды, а не за миллисекунды, и переходите в режим Server, как только вызывающий код покидает машину решателя.

Ещё одно обстоятельство, которое определяет, как устроены повторные попытки. Когда токен возвращается отклонённым, честный выход почти всегда один: решить заново с правильной версией, а локальный сервис распознавания капчи превращает такой повтор в несколько секунд на машине, которой вы уже владеете, а не в очередную строку в чьём-то счёте.