Как решить CaptchaFox на C# и не получить отклонённый токен

solve captchafox in c# - How to Solve CaptchaFox in C# Without a Rejected Token

Чтобы решить CaptchaFox на C#, вызовите CaptchaFoxAsync с sitekey и URL страницы, положите полученный токен в поле формы cf-captcha-response и отправьте этот запрос с тем user agent, который CapSkip возвращает вместе с токеном. Именно на последнем шаге ломается большинство интеграций. Токен CaptchaFox привязан к браузеру, который его создал, и несовпадающий user agent чаще всего становится причиной того, что верный токен отклоняют, хотя само решение прошло успешно. CapSkip добавил CaptchaFox в версии 1.4.0. В этом руководстве разобрано, как найти ключ, как сделать вызов, как отправить токен и какие немногие настройки решают, будет ли токен принят.

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

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

Шаг 1: находим sitekey и источник виджета

Всё, что нужно, чтобы решить CaptchaFox на C#, берётся с целевой страницы. Sitekey публичен, одинаков для всех посетителей и по соглашению начинается с sk_. Сайты кладут его в одно из трёх мест, и найти нужно только одно из них.

  • На элементе-контейнере, если виджет отрисовывается сам: div с классом captchafox и атрибутом data-sitekey. Скрытый режим, который ничего не показывает до отправки формы, использует тот же div с data-mode, установленным в hidden, так что ключ есть и там.
  • В опциях вызова captchafox.render, если страница строит виджет собственным скриптом. Вызов render тоже может включить скрытый режим, и ключ в этом случае лежит прямо рядом.
  • На вкладке Network, если ни того, ни другого нет в отданном HTML. Найдите запрос к api.captchafox.com; ключом будет сегмент пути после /captcha/.

Пока исходный код страницы открыт, посмотрите на тег script, который загружает виджет. Виджет попадает на страницы из двух мест, и от того, какое из них загружает сайт, зависит, в каком формате этот сайт ждёт токен.

Откуда страница загружает виджетВозвращаемый токенЧто передавать
https://cdn.captchafox.com/, стандартный виджет, который используют большинство сайтовОбычный токенНичего дополнительного; это вариант по умолчанию
Пакет в https://s.uicdn.com/mampkg/, который встраивают некоторые платформыТокен, который начинается с MAM_Полный путь к пакету из тега script, в опции api_server

Ошибка здесь проявляется тихо. Если передать не тот источник, решение всё равно пройдёт успешно и вернёт токен, только в формате, который сайт не примет, поэтому сбой обнаружится позже в виде отклонённой формы, а не ошибки, которую можно перехватить.

Шаг 2: вызов CaptchaFoxAsync

Метод принимает sitekey, URL страницы и необязательный словарь опций. Для стандартного виджета не нужно ничего, кроме первых двух.

// dotnet add package CapSkip
using CapSkip;

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

// The sitekey from the widget, and the page the widget runs on.
var result = await solver.CaptchaFoxAsync(
    "YOUR_SITEKEY",
    "https://example.com/signup");

Console.WriteLine(result.Token);       // goes in cf-captcha-response
Console.WriteLine(result.UserAgent);   // send this as the User-Agent

Читайте свойство Token. Свойство Code содержит ту же строку, но Token назван по имени поля, куда он попадает. UserAgent описывает браузер, который выпустил токен, то есть собственный браузер CapSkip, а не что-либо отправленное вами. Если решение не сообщило user agent, SDK оставляет свойство равным null, а не придумывает значение.

URL страницы здесь важнее, чем для большинства типов. CaptchaFox регистрирует каждый ключ на список разрешённых доменов и проверяет хост, прежде чем что-либо выдать, поэтому ключ, переданный с неправильной страницей, отклоняется всегда, а не время от времени. CapSkip сообщает о таком случае сразу, а не повторяет попытку, потому что повтор не поможет. Передавайте страницу, на которой виджет действительно работает, а не результат поиска, редирект или сокращённую ссылку.

Сайты, которые загружают пакет MAM

Если на шаге 1 нашёлся скрипт с s.uicdn.com, скопируйте путь к его пакету ровно так, как он записан на странице, и передайте его в api_server. Тогда токен вернётся с префиксом MAM_, которого ждёт сайт.

// Only for pages whose script tag loads the MAM build.
var result = await solver.CaptchaFoxAsync(
    "YOUR_SITEKEY",
    "https://example.com/signup",
    new Dictionary<string, object?>
    {
        ["api_server"] =
            "https://s.uicdn.com/mampkg/@mamdev/core.frontend.libs.captchafox/",
    });

Словарь опций принимает api_server, proxy и useragent, а также proxytype и таймаут на один вызов в секундах. Опция useragent существует для совместимости с другими сервисами и не применяется, потому что CapSkip решает в настоящем браузере, у которого собственный, внутренне согласованный набор признаков. Неизвестный ключ, а также пустой sitekey или URL страницы вызывают ValidationException ещё до отправки запроса.

Шаг 3: отправляем под тем user agent, с которым выпущен токен

Именно этот шаг отделяет принятый токен от отклонённого. CaptchaFox оценивает браузер, в котором работает виджет, и выданный токен принадлежит этому браузеру. HttpClient вообще не отправляет заголовок User-Agent, если вы его не добавите, а жёстко прописанная десктопная строка ничуть не лучше, поэтому скопируйте ту, что вернул CapSkip, в запрос, который несёт токен.

// http is your HttpClient; result comes from Step 2.
var request = new HttpRequestMessage(HttpMethod.Post, "https://example.com/signup")
{
    Content = new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["email"] = "[email protected]",
        ["cf-captcha-response"] = result.Token!,
    }),
};

// The token is bound to the browser that produced it.
if (result.UserAgent is { } ua)
    request.Headers.TryAddWithoutValidation("User-Agent", ua);

var response = await http.SendAsync(request);

TryAddWithoutValidation выбран намеренно. Более строгий UserAgent.ParseAdd сверяет строку с грамматикой заголовка и бросает FormatException, если они расходятся, а это значение должно уйти байт в байт, что бы в нём ни было. Задавайте его для каждого запроса, а не в заголовках клиента по умолчанию, чтобы один HttpClient мог нести токены от нескольких решений.

Ещё два правила следуют из того, как CaptchaFox проверяет токен на стороне сайта. В собственной документации CaptchaFox сказано, что каждый токен можно проверить только один раз и только в течение короткого времени, поэтому решайте тогда, когда вот-вот будете отправлять, отправляйте токен один раз и решайте заново, если форму бросили, а потом к ней вернулись. И считайте токен непрозрачным: его сверяют с сессией, которая его создала, поэтому обрезка или перекодирование его ломают. Некоторые интеграции отправляют его в теле JSON, а не обычной отправкой формы, поэтому один раз отправьте форму вручную с открытыми DevTools и повторите ровно то, что отправляет страница.

Шаг 4: прокси, типы заданий и таймауты

CaptchaFox оценивает не только браузер, но и сеть, в которой работает виджет. Одного адреса хватает для тестов и редких решений, но повторные решения с него подталкивают этот адрес сначала к интерактивным заданиям, а затем и к отказам. Как только вы начинаете решать в сколько-нибудь заметном объёме, настройте пул прокси в CapSkip или передавайте прокси на каждый запрос. В этом случае и форму отправляйте через тот же выходной адрес, чтобы токен и форма приходили из одной сети. Проверка CaptchaFox позволяет сайту передать вместе с токеном IP-адрес посетителя, и это ещё одна причина держать их на одном маршруте.

// using System.Net; for WebProxy and NetworkCredential.
// Same exit address for the solve and the submit.
var result = await solver.CaptchaFoxAsync("YOUR_SITEKEY", pageUrl,
    new Dictionary<string, object?>
    {
        ["proxy"] = new Proxy("HTTP", "login:[email protected]:8080"),
    });

var http = new HttpClient(new HttpClientHandler
{
    Proxy = new WebProxy("http://1.2.3.4:8080")
    {
        Credentials = new NetworkCredential("login", "password"),
    },
});

Какое задание появится, выбираете не вы. Чаще всего при решении не рисуется вообще ничего, потому что браузерных признаков хватает самих по себе, а если CaptchaFox просит пройти слайдер, CapSkip справляется и с ним. Два редких запасных варианта не решаются.

Тип заданияКак частоРешено
Невидимая проверка, ничего не рисуетсяОбычноДа
Слайдер-пазлИногдаДа
Выбор изображенийРедкоНет, помечается как нерешаемая
АудиоРедкоНет, помечается как нерешаемая

Сообщение о нерешаемой капче приходит быстро, в виде ApiException, а не после истечения таймаута. При повторной попытке обычно выпадает другое задание, так что считайте это поводом отправить капчу на решение заново, а не признаком сломанного ключа. Полный пример ниже повторяет попытку в этом случае дважды и ни в каком другом, поскольку ApiException охватывает и ошибки, которые повтор не исправит, например неверный API-ключ.

CaptchaFoxAsync опрашивает результат с таймаутом recaptchaTimeout, по умолчанию 300 секунд, потому что это настоящая браузерная сессия, которая длится дольше, если выпадает слайдер. У CapSkip есть и собственный таймер: задание может ждать одного из 10 потоков CaptchaFox (Max. Threads) до 250 секунд (Wait Timeout), а на одну попытку отводится 150 секунд (Row Timeout). Поэтому попытка, которую замедлил прокси, сначала завершается неудачей внутри CapSkip, и более длинный таймаут SDK не даёт ей дополнительного времени. Если Retries стоит на значении по умолчанию 0 и поток свободен, эта неудача доходит до вас в виде ApiException задолго до того, как истекут 300 секунд SDK; если задание долго стоит в очереди, SDK может первым исчерпать свой лимит и вместо этого бросить CapSkip.TimeoutException.

Увеличивать таймаут SDK нужно только тогда, когда вы поднимаете Retries (0-3) в настройках CaptchaFox в CapSkip, потому что каждый дополнительный повтор может добавить к одному вызову ещё до одного Row Timeout. В этом случае передайте более длинный timeout в словаре опций или поднимите recaptchaTimeout в конструкторе.

Запуск решателя на другой машине

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

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

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

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

// Copied by hand from the script tag, for pages that load the MAM build.
const string MamPackage = "https://s.uicdn.com/mampkg/@mamdev/core.frontend.libs.captchafox/";

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

// Read the sitekey off the captchafox container, in either attribute order.
var html = await http.GetStringAsync(pageUrl);
var widget = Regex.Match(html,
    "<[^>]*class=\"(?:[^\"]*\\s)?captchafox(?:\\s[^\"]*)?\"[^>]*>").Value;
var sitekey = Regex.Match(widget, "data-sitekey=\"([^\"]+)\"").Groups[1].Value;
// MAM pages may carry the key in the script src instead.
if (sitekey.Length == 0)
    sitekey = Regex.Match(html,
        "captchafox[^\"]*/api\\.js\\?key=([^\"&]+)").Groups[1].Value;
if (sitekey.Length == 0)
    throw new InvalidOperationException("No sitekey in the HTML; find it in DevTools.");

// The same CDN serves other packages, so match the captchafox one.
var options = new Dictionary<string, object?>();
if (html.Contains("mampkg/@mamdev/core.frontend.libs.captchafox"))
    options["api_server"] = MamPackage;

try
{
    // Image-select and audio come back unsolvable; a retry redraws.
    // Other API errors, such as a wrong key, are not worth repeating.
    SolveResult? result = null;
    for (var attempt = 1; result is null; attempt++)
    {
        try
        {
            result = await solver.CaptchaFoxAsync(sitekey, pageUrl, options);
        }
        catch (ApiException ex) when (attempt < 3 && ex.Message.Contains("UNSOLVABLE"))
        {
            Console.WriteLine($"attempt {attempt}: {ex.Message}");
        }
    }

    var request = new HttpRequestMessage(HttpMethod.Post, pageUrl)
    {
        Content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["email"] = "[email protected]",
            ["cf-captcha-response"] = result.Token!,
        }),
    };
    if (result.UserAgent is { } ua)
        request.Headers.TryAddWithoutValidation("User-Agent", ua);

    // Post wherever the form's action attribute points.
    var response = await http.SendAsync(request);
    Console.WriteLine($"{(int)response.StatusCode}, UA sent: {result.UserAgent is not null}");
}
catch (CapSkip.ValidationException ex)
{
    // An option the method does not take.
    Console.WriteLine($"not sent: {ex.Message}");
}
catch (CapSkip.TimeoutException)
{
    Console.WriteLine("gave up waiting; recaptchaTimeout is 300 seconds");
}
catch (CapSkipError ex)
{
    // The third unsolvable result, a refused key, or CapSkip unreachable.
    Console.WriteLine($"gave up: {ex.Message}");
}

Регулярное выражение ограничивает имя класса пробельными символами, поэтому элемент вроде captchafox-wrapper не совпадёт случайно, и оно ожидает атрибуты в двойных кавычках, как их пишет собственный сниппет CaptchaFox. Если решение мгновенно проваливается при каждом запуске, первым делом проверьте URL страницы: ключ, использованный вне зарегистрированных для него доменов, каждый раз отказывает одинаково. Если регулярное выражение ничего не находит, значит, страница отрисовывает виджет из скрипта, и ключ находится в вызове render или на вкладке Network, как описано на шаге 1. Сырой эндпоинт, который стоит за этим методом, описывает справочник API, а все остальные методы, которые предоставляет пакет, перечислены на странице сервиса распознавания капчи для C#.

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

Что вы видитеПричинаИсправить
Сайт отклоняет токен, который CapSkip вернул как решённыйЗапрос ушёл с другим User-Agent, чем тот, с которым выпущен токенОтправьте result.UserAgent в запросе с формой, байт в байт
Отклоняется, хотя user agent совпадаетСайт загружает пакет MAM, а решение использовало виджет по умолчанию, или наоборотПрочитайте тег script и задайте api_server в соответствии с ним
Токен, начинающийся с MAM_, который сайт отклоняетapi_server задан для страницы, которая загружает стандартный виджетУберите опцию и используйте значение по умолчанию
ApiException сразу же, каждый раз, для одного ключаURL страницы находится вне доменов, для которых зарегистрирован ключПередавайте страницу, на которой работает виджет, а не редирект или результат поиска
Время от времени ApiException с сообщением, что капча нерешаемаCaptchaFox выдал задание с выбором изображений или аудиозаданиеОтправьте на решение повторно; при следующей попытке обычно выпадает другое задание
По ходу прогона заданий всё больше, а затем идут отказыВсе решения идут с одного адреса, а CaptchaFox оценивает сетьРаспределите решения по пулу прокси и отправляйте форму через тот же выходной адрес
Токен, который сработал один раз, не проходит при второй отправкеКаждый токен проходит проверку один раз и быстро истекаетРешайте заново для каждой отправки и отправляйте сразу
ValidationException ещё до какой-либо отправкиSitekey или URL страницы были пустыми либо в опциях был ключ, который метод не принимаетПроверьте, что регулярное выражение нашло элемент, и уберите опцию, названную в сообщении
NetworkException на первом решенииCapSkip не запущен либо неверны хост и портЗапустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server
Сборка падает на неоднозначном TimeoutExceptionЭто короткое имя определено и в CapSkip, и в SystemПишите CapSkip.TimeoutException полностью или перехватывайте CapSkipError

FAQ

Почему CaptchaFox нужен user agent, если большинству типов капчи он не нужен?

Потому что он оценивает браузер, а не просит человека что-то прочитать. Токен представляет собой вердикт сервиса об одном конкретном браузере, поэтому смысл он имеет, только когда приходит из этого же браузера. Так же устроены страницы проверки Cloudflare Turnstile, и сторона C# для них разобрана в руководстве по странице проверки Turnstile. CapSkip возвращает user agent ровно для этих двух типов, и это надёжная подсказка о том, где он важен.

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

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

Чем это отличается от решения Friendly Captcha на C#?

Оба типа появились в CapSkip 1.4.0 и оба используют таймаут в 300 секунд, и этим сходство в основном исчерпывается. У Friendly Captcha главный вопрос в том, какую версию протокола использует сайт, а токен к user agent не привязан. У CaptchaFox вопросов два: из какого источника сайт загружает виджет и с каким user agent уходит токен. Сторона Friendly Captcha разобрана в руководстве по Friendly Captcha на C#.

Можно ли решать несколько токенов CaptchaFox одновременно?

Да. Клиент асинхронный, поэтому Task.WhenAll над несколькими вызовами CaptchaFoxAsync запускает их бок о бок. По умолчанию CapSkip выполняет 10 решений CaptchaFox одновременно (Max. Threads в его настройках CaptchaFox), а остальные ждут свободного потока до 250 секунд, заданных в Wait Timeout, так что пакет из пятидесяти вызовов при обычном времени решения проходит нормально; для более крупных пакетов поднимите Max. Threads или отправляйте вызовы небольшими группами. Заранее учтите ещё две вещи: каждый токен идёт в паре со своим user agent, поэтому держите результат вместе с запросом, к которому он относится, а параллельность с одного адреса быстрее поднимает частоту заданий, поэтому добавляйте прокси по мере добавления параллельных решений.

Коротко

Чтобы решить CaptchaFox на C#, считайте sitekey с контейнера captchafox, из вызова render или с вкладки Network и проверьте, какой скрипт загружает виджет. Вызовите CaptchaFoxAsync с sitekey и настоящим URL страницы, добавляя api_server только для пакета MAM. Отправьте result.Token в cf-captcha-response с result.UserAgent, один раз и сразу же, и повторите решение, если вернулось нерешаемое задание. Добавляйте прокси по мере роста объёма и переходите в режим Server, как только вызывающий код покидает машину решателя.

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