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

Чтобы решить 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, как только вызывающий код покидает машину решателя.
- Как работает этот тип и что покрывает решатель: страница решения CaptchaFox.
- Все остальные методы, которые предоставляет пакет .NET: страница решения капчи для C# и .NET.
Ещё одно обстоятельство, которое определяет, как устроены повторные попытки. Когда решение возвращается ни с чем из-за задания с выбором изображений, лекарство простое: ещё одна попытка, а безлимитный сервис распознавания капчи на вашей собственной машине превращает такой повтор в несколько секунд, а не в очередное оплачиваемое решение.
