Как решить hCaptcha на C# с помощью HCaptchaAsync (.NET 8+)

hcaptcha in c# - How to Solve hCaptcha in C# With HCaptchaAsync (.NET 8+)

Чтобы решить hCaptcha на C#, вызовите HCaptchaAsync из пакета CapSkip NuGet, передав sitekey виджета и адрес страницы. Возвращаемый SolveResult содержит токен, respKey и User-Agent, с которым получен токен, а форму вы отправляете с этим токеном и этим User-Agent. Один и тот же метод подходит для виджетов с флажком, невидимых и Enterprise. Вызов уходит в решатель hCaptcha в CapSkip на вашем собственном ПК с Windows, а фиксированная лицензия означает, что вторая попытка ничего не стоит.

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

  • ПК с Windows, на котором запущен CapSkip 4.0.0 или новее, выпуск с поддержкой hCaptcha. Шаги установки описывает руководство по настройке.
  • SDK для .NET 8 или новее и пакет CapSkip NuGet версии 1.6.0 или новее, первый выпуск с HCaptchaAsync. Пакет ориентирован на netstandard2.0 и зависит от System.Text.Json 8.0.5. У меня установлен один SDK, 10.0.100, и всё запускалось с CapSkip 1.6.0 только на net10.0 и net8.0.
  • Окно PowerShell и страница, которую вам разрешено автоматизировать и на которой показан виджет hCaptcha, а также его sitekey.
  • Адрес для CapSkip. Режим Local отвечает только на 127.0.0.1, для программ на этом ПК. Режим Server слушает ещё и вашу сеть или публичный IP, поэтому программа на другой машине может вызывать тот же API. Выбор режима делается в разделе Настройки подключения и хранится там.

Тестовая установка: настоящий пакет CapSkip 1.6.0 из NuGet и две мои собственные имитации серверов, без настоящего сайта hCaptcha. Одна имитация изображает приложение CapSkip и повторяет раздел hCaptcha на странице CapSkip с названием Документация API. Вторая изображает страницу регистрации и может работать как прокси. Её правила (cookie сеанса, Referer или Origin, User-Agent, с которым выдан токен) выбрал я, а не hCaptcha. Материалы из документации для разработчиков hCaptcha описывают, как настоящий сайт проверяет токен. Я прочитал исходный код пакета на теге v1.6.0 и ничего не отправлял настоящему приложению CapSkip, поэтому времени настоящего решения у меня нет.

Шаг 1: создание проекта и установка CapSkip

Создайте консольный проект и установите пакет. Новый проект включает неявные using и ссылочные типы, допускающие null, и этому коду нужны оба. Без указания версии dotnet add package берёт самый новый выпуск, на момент моего запуска это 1.6.0.

dotnet new console -o hcaptcha-demo
cd hcaptcha-demo
dotnet add package CapSkip

Удалите всё в Program.cs, включая строку Console.WriteLine из шаблона, затем вставьте три блока C# ниже по порядку, начиная с шага 2. Строки using есть только в первом блоке, и ничего не запускается, пока не добавлен последний блок.

Шаг 2: вызов HCaptchaAsync

Sitekey находится на странице. На большинстве страниц он указан как data-sitekey у контейнера виджета, а виджет, построенный скриптом, передаёт его как параметр sitekey в hcaptcha.render(). HCaptchaAsync принимает sitekey, адрес страницы, словарь параметров и CancellationToken. Его типизированная перегрузка принимает bool с именем invisible перед параметрами: true отправляет invisible=1, а false ничего не отправляет. Сайт Enterprise, задающий rqdata, получает значение под ключом data, свежее для каждого задания. Прокси задаётся объектом Proxy с типом (HTTP, HTTPS, SOCKS5 или SOCKS5H) и строкой login:password@host:port. Порт указывайте явно, потому что Uri.Authority отбрасывает порт по умолчанию. В прогоне на имитации invisible, data и прокси дошли до in.php.

using System.Net;
using System.Text.RegularExpressions;
using CapSkip;
using SolverTimeout = CapSkip.TimeoutException;  // System.TimeoutException is in scope too (CS0104)

static class Solve
{
    // Local mode. In Server mode, use your server's address instead. If API Key
    // Validation is on in CapSkip, also pass apiKey: "YOUR_KEY" here.
    static readonly CapSkipClient Client = new(host: "127.0.0.1", port: 8080);

    public static Task<SolveResult> Run(string siteKey, string pageUrl, bool invisible,
                                        string? rqData, Uri? proxy, CancellationToken ct)
    {
        var options = new Dictionary<string, object?>();
        if (rqData is not null) options["data"] = rqData;  // Enterprise rqdata, fresh per task
        if (proxy is not null)
        {
            // Uri.Authority drops a default port such as 80, so build host:port by hand.
            var login = proxy.UserInfo.Length > 0 ? Uri.UnescapeDataString(proxy.UserInfo) + "@" : "";
            options["proxy"] = new Proxy(proxy.Scheme.ToUpperInvariant(), $"{login}{proxy.Host}:{proxy.Port}");
        }
        // invisible: false sends nothing, true sends invisible=1.
        return Client.HCaptchaAsync(siteKey, pageUrl, invisible, options, ct);
    }
}

Стоит знать четыре свойства результата. Token и Code содержат одну и ту же строку. RespKey содержит второе значение, которое hCaptcha передаёт виджету, то самое, что hcaptcha.getRespKey() возвращает в браузере. Большинство сайтов его не читает, а если значения нет, это пустая строка, а не null. UserAgent содержит строку User-Agent браузера, с которым получен токен. Если в ответе его нет, он остаётся null, и тогда шаг 3 не отправляет User-Agent, а моя имитация сайта отклоняла отправку.

SDK проверяет ваши аргументы перед любым запросом. Пустой sitekey, адрес страницы не в абсолютном виде с http или https, значение invisible, отличное от 0, 1, true или false, и любой параметр, которого не принимает API hCaptcha в CapSkip, включая domain из 2Captcha, вызывают ValidationException, и имитация не получила ни одного задания ни в одном из этих случаев. В остальном SDK отправляет задание и опрашивает сразу, затем через 0.25 секунды, потом с удваивающимся интервалом до 5 секунд. На имитации опросы пришлись с точностью до 0.15 секунды на 0, 0.25, 0.75, 1.75, 3.75, 7.75 и 12.75 секунды после отправки.

Ожидание заканчивается исключением TimeoutException после RecaptchaTimeout, то есть через 300 секунд, если вы не передали recaptchaTimeout в конструктор или параметр timeout в один из вызовов. hCaptcha получает этот более длинный тайм-аут, потому что виджет проходит проверку в настоящем браузере, а в документации сказано, что решение обычно занимает от 10 до 30 секунд. Срок проверяется между опросами: при тайм-ауте 22 секунды и имитации, которая так и не ответила, исключение пришло через 22.9 секунды после вызова.

Каждое исключение, которое выбрасывает SDK, наследует CapSkipError. ValidationException означает локальный отказ. NetworkException означает, что запрос не удался или CapSkip ответил не HTTP 200. ApiException означает, что CapSkip ответил ошибкой, а его сообщение содержит весь текст ответа (на имитации это JSON после опроса и голый текст после отправки задания), поэтому код находится внутри него. Отсюда две ловушки. TimeoutException существует и в System, а неявные using импортируют System, поэтому голый TimeoutException рядом с using CapSkip приводит к ошибке CS0104, неоднозначной ссылке. Первый блок задаёт для класса SDK псевдоним SolverTimeout. А CancellationToken, сработавший во время отправки задания, выходит как NetworkException, обёртывающее отмену, а сработавший во время опроса выходит как TaskCanceledException.

Шаг 3: отправка формы из отдельного обработчика

Пакет держит для CapSkip собственный HttpClient, поэтому ничто ниже его не использует. С отправкой формы иначе, потому что хранилище cookie принадлежит обработчику, а две регистрации не должны делить cookie. Site.SignUp создаёт обработчик с собственным CookieContainer, оборачивает его клиентом и в конце освобождает оба. Microsoft в своих рекомендациях по HttpClient называет отдельные контейнеры cookie причиной создавать больше одного экземпляра.

Сначала загружается страница, поэтому её cookie возвращаются вместе с отправкой. Оба запроса несут User-Agent из результата. HttpClient не отправляет его, пока вы его не зададите: когда эта строка удалена, имитация сайта, которая сверяет его с токеном, видела пустой и отклоняла каждую отправку. Отправка также несёт Referer и Origin, потому что HttpClient не отправляет ни тот, ни другой. Моя имитация сайта требует один из них, это правило выбрал я, и я не измерял, сколько настоящих сайтов его требует. Токен кладётся в h-captcha-response и g-recaptcha-response. Он работает один раз и, по документации, живёт около двух минут, поэтому отправляйте его сразу.

Прокси необязателен, хотя в документации предупреждают, что постоянный поток решений с одного IP как раз и ловят лимиты частоты запросов hCaptcha. Uri, который вы передаёте, уходит в CapSkip через Solve.Run и в этот обработчик, потому что в документации для строгих сайтов требуют, чтобы форма отправлялась с того же адреса, что и решение. WebProxy игнорирует логин в адресе, поэтому обработчик переносит его в NetworkCredential. Имитация прокси, требовавшая учётные данные Basic, приняла его, даже со знаком процента в пароле. Участок до сайта через SOCKS я не запускал.

static class Site
{
    // One HttpClient per sign-up, because the cookie jar belongs to the handler and two
    // sign-ups must not share cookies. Dispose it when the sign-up is over.
    public static async Task<(HttpStatusCode Status, string Body)> SignUp(
        string pageUrl, SolveResult solved, string email, Uri? proxy, CancellationToken ct)
    {
        var handler = new HttpClientHandler { CookieContainer = new CookieContainer() };
        if (proxy is not null)
        {
            var address = new UriBuilder(proxy.Scheme, proxy.Host, proxy.Port).Uri;
            var login = proxy.UserInfo.Split(':', 2).Select(Uri.UnescapeDataString).ToArray();
            handler.Proxy = new WebProxy(address)
            {
                Credentials = login.Length == 2 ? new NetworkCredential(login[0], login[1]) : null,
            };
        }
        using var http = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
        // The User-Agent the token was earned with, on the page load and on the post.
        if (solved.UserAgent is not null)
            http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", solved.UserAgent);

        using (await http.GetAsync(pageUrl, ct)) { }  // the page's own cookies land in the jar

        var origin = new Uri(pageUrl).GetLeftPart(UriPartial.Authority);
        using var post = new HttpRequestMessage(HttpMethod.Post, pageUrl)
        {
            Content = new FormUrlEncodedContent(new Dictionary<string, string>
            {
                ["email"] = email,  // your form's own fields go here
                ["h-captcha-response"] = solved.Token!,  // always set on an hCaptcha result
                ["g-recaptcha-response"] = solved.Token!,  // the widget fills both
            }),
        };
        post.Headers.TryAddWithoutValidation("Referer", pageUrl);  // HttpClient sends none
        post.Headers.TryAddWithoutValidation("Origin", origin);
        using var response = await http.SendAsync(post, ct);
        return (response.StatusCode, await response.Content.ReadAsStringAsync(ct));
    }
}

Шаг 4: запуск всего цикла hCaptcha на C#

Последний блок содержит метод Main. Измените константы PageUrl и SiteKey (в том виде, как вставлено, он запрашивает example.com, где нет виджета), затем выполните dotnet run в папке проекта. Он делает до трёх попыток под одним CancellationTokenSource, и внутри каждой попытки есть собственное ожидание SDK в 300 секунд. По моему прочтению исходного кода CapSkip 4.1.0, одно задание может выполняться до 300 секунд, прежде чем CapSkip сочтёт его нерешаемым, после ожидания до 250 секунд свободного места в Max. Threads, поэтому при нагрузке увеличьте recaptchaTimeout.

Согласно документации, только при ERROR_CAPTCHA_UNSOLVABLE может помочь немедленное новое задание. ERROR_NO_SLOT_AVAILABLE, который получает ключ, превысивший лимит частоты запросов, повторяется после паузы в 5 секунд. Любой другой код завершает прогон, как и ValidationException, поскольку те же аргументы будут отклонены снова. Сайт, ответивший 400 или выше, получает новое задание и новый токен. Ответ 200 не всегда означает успех, потому что многие сайты помещают ошибку внутрь страницы, поэтому замените проверку статуса чем-то, что бывает только при успехе. Когда истекает 30-секундный HttpClient.Timeout отправки формы, об этом сообщает последний catch.

static class Program
{
    const string PageUrl = "https://example.com/signup";  // the page that shows the widget
    const string SiteKey = "YOUR_SITEKEY";  // the data-sitekey of its h-captcha div
    static readonly Uri? ProxyUri = null;  // for example new Uri("http://user:[email protected]:8080")

    // The reply body is the exception message, JSON on a poll and bare text on a submit.
    static string ErrorCode(ApiException ex)
    {
        var found = Regex.Match(ex.Message, "ERROR_[A-Z_]+");
        return found.Success ? found.Value : ex.Message;
    }

    static async Task<int> Main()
    {
        // One budget for every attempt. Cancelling it stops a wait or a request at once.
        using var budget = new CancellationTokenSource(TimeSpan.FromMinutes(6));
        try
        {
            for (var attempt = 1; attempt <= 3; attempt++)
            {
                SolveResult solved;
                try
                {
                    solved = await Solve.Run(SiteKey, PageUrl, invisible: false, rqData: null, ProxyUri, budget.Token);
                }
                catch (ApiException ex) when (ErrorCode(ex) is "ERROR_CAPTCHA_UNSOLVABLE" or "ERROR_NO_SLOT_AVAILABLE")
                {
                    Console.WriteLine($"attempt {attempt}: {ErrorCode(ex)}, new task");
                    // The second code means the key is over its rate limit, so pause first.
                    if (ErrorCode(ex) == "ERROR_NO_SLOT_AVAILABLE") await Task.Delay(TimeSpan.FromSeconds(5), budget.Token);
                    continue;
                }

                var (status, body) = await Site.SignUp(PageUrl, solved, "[email protected]", ProxyUri, budget.Token);
                if ((int)status < 400)
                {
                    Console.WriteLine($"{(int)status} {body[..Math.Min(40, body.Length)]}");
                    return 0;
                }
                Console.WriteLine($"attempt {attempt}: the site answered {(int)status}, new token");
            }
            Console.WriteLine("no accepted form after 3 attempts");
        }
        // A cancelled submit surfaces as a NetworkException, a cancelled poll as a
        // TaskCanceledException, so test the budget itself.
        catch (Exception) when (budget.IsCancellationRequested) { Console.WriteLine("the 6 minute budget ran out"); }
        catch (ApiException ex) { Console.WriteLine($"CapSkip answered {ErrorCode(ex)}"); }
        catch (SolverTimeout ex) { Console.WriteLine($"no token in time: {ex.Message}"); }
        catch (CapSkipError ex) { Console.WriteLine($"{ex.GetType().Name}: {ex.Message}"); }
        catch (HttpRequestException ex) { Console.WriteLine($"request failed: {ex.Message}"); }
        catch (TaskCanceledException) { Console.WriteLine("the site did not answer in 30 seconds"); }  // HttpClient.Timeout
        return 1;
    }
}

На имитациях, которые ничего не доказывают о настоящем сайте, каждая строка получилась одинаковой на net10.0 и net8.0.

Настройка имитацииЧто сделала программа
Токен готов на 7-м опросе, настройки SDK по умолчаниюОдно задание, опросы с точностью до 0.15 секунды на 0, 0.25, 0.75, 1.75, 3.75, 7.75 и 12.75 секунды после отправки, затем 200
Два отказа (ERROR_CAPTCHA_UNSOLVABLE или ERROR_NO_SLOT_AVAILABLE с паузой), затем успех; либо отказы каждый раз200 на третьем задании; либо три строки с попытками, код выхода 1 и ни одного запроса к сайту
Сайт отклоняет первую отправку формы с кодом 403Новое задание и токен, затем 200
Пустой sitekeyСтрока ValidationException и код выхода 1, имитация не получила ни одного задания
ERROR_ZERO_BALANCE, ERROR_PROXY_CONNECT_REFUSED или ERROR_RECAPTCHA_INVALID_SITEKEYКаждый остановил прогон после одного задания
Вызов с invisible, вызов для Enterprise, прокси с логином и без негоin.php получил invisible=1, значение data или proxytype HTTP вместе с адресом, а запросы к сайту пришли через имитацию прокси
apiKey в конструкторе, проверка ключа включенаКлюч дошёл до отправки задания и до каждого опроса. Ключ по умолчанию получил ERROR_KEY_DOES_NOT_EXIST
recaptchaTimeout, равный 22, и имитация, которая никогда не отвечаетTimeoutException через 22.9 секунды после вызова
Бюджет времени 2 секунды, во время паузы между опросами или при зависшем in.phpСообщение о бюджете через 2.0 секунды в обоих случаях
Никто не слушает ни у CapSkip, ни у сайта; страница сайта, которая никогда не отвечаетNetworkException; запрос не удался; сообщение про 30 секунд через 30 секунд
Три регистрации одновременноТри задания, три сеанса, три User-Agent в отправках, три ответа 200

Остальное покрывают два фрагмента. Первый заменяет вызов для невидимого виджета или виджета Enterprise. Второй регистрирует несколько аккаунтов одновременно: замените им строки между фигурными скобками внешнего try в Main и оставьте строки catch. Каждая регистрация создаёт собственный обработчик, а имитация даёт каждому заданию свой User-Agent, поэтому путаница провалила бы отправки. Фрагмент не передаёт rqData, поэтому для сайта Enterprise читайте свежее значение внутри лямбды.

// Snippet, not part of Program.cs. In Main, replace the Solve.Run line with one of these.
// An invisible widget (data-size="invisible"):
solved = await Solve.Run(SiteKey, PageUrl, invisible: true, rqData: null, ProxyUri, budget.Token);
// An Enterprise widget. Read a fresh rqdata value from the page before every attempt,
// because it is single use. The literal below stands in for that code.
solved = await Solve.Run(SiteKey, PageUrl, invisible: false, rqData: "RQDATA_FROM_THE_PAGE", ProxyUri, budget.Token);
// Snippet, not part of Program.cs. In Main, replace the lines between the braces of the outer try
// with these, and keep the catch lines. Three accounts sign up at once on one SDK client.
var emails = new[] { "[email protected]", "[email protected]", "[email protected]" };
var codes = await Task.WhenAll(emails.Select(async email =>
{
    var solved = await Solve.Run(SiteKey, PageUrl, false, null, ProxyUri, budget.Token);
    var (status, _) = await Site.SignUp(PageUrl, solved, email, ProxyUri, budget.Token);
    return (int)status;
}));
Console.WriteLine(string.Join(" ", codes));
return codes.All(c => c < 400) ? 0 : 1;

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

Что вы видитеПричинаИсправить
ValidationExceptionОтклонено до отправки: пустой sitekey, адрес страницы не в абсолютном виде с http или https, неверное значение invisible или параметр вроде domain, которого не принимает API hCaptchaИсправьте аргумент. Повтор не поможет
ERROR_RECAPTCHA_INVALID_SITEKEYSitekey отклонён для этой страницыСкопируйте data-sitekey заново и проверьте адрес страницы
ERROR_CAPTCHA_UNSOLVABLECapSkip не смог пройти виджетЦикл отправляет новое задание, до трёх раз
ERROR_PROXY_FORMAT или ERROR_PROXY_CONNECT_REFUSEDCapSkip не смог разобрать значение прокси или достучаться через него до цели (второй случай я лишь смоделировал)Передайте Uri со схемой, хостом и портом и проверьте прокси отдельно
ERROR_WRONG_METHODCapSkip старше 4.0.0 может не знать метод hCaptcha (не проверялось)Обновите CapSkip до 4.0.0 или новее
ERROR_NO_SLOT_AVAILABLE, ERROR_ZERO_BALANCE или ERROR_IP_NOT_ALLOWEDТолько при включённой API Key Validation. По моему прочтению исходного кода 4.1.0: у ключа превышен лимит частоты запросов, закончились кредиты, либо ключ для одного устройства используется в другом местеЦикл делает паузу и повторяет первую. Для остальных измените настройки ключа в CapSkip
ERROR_WRONG_USER_KEY или ERROR_KEY_DOES_NOT_EXISTAPI Key Validation включена, а apiKey в вызове конструктора отсутствует или неверенПередайте действительный apiKey в строке с CapSkipClient
TimeoutExceptionТокен не пришёл за recaptchaTimeout. Пустой ответ считается «не готово», поэтому SDK продолжает опросНайдите задание на вкладке Monitor в CapSkip, затем увеличьте recaptchaTimeout
NetworkException в начале вызоваПо этому хосту и порту ни одна программа не отвечает. Внутреннее исключение: HttpRequestExceptionЗапустите CapSkip, затем скопируйте его хост и порт из настроек подключения в вызов конструктора
Ошибка CS0104: неоднозначная ссылкаTimeoutException есть и в System, и в CapSkip. ValidationException конфликтует так же, как только вы импортируете System.ComponentModel.DataAnnotationsЗадайте псевдоним для класса SDK, как делает первый блок, или пишите его полное имя
Токен, который выглядит верным, отклоняетсяВ документации названы повторно использованный токен, токен старше примерно двух минут и User-Agent, отличный от User-Agent из результата. Моя имитация также отклоняла запросы без User-Agent, Referer, Origin или cookie сеанса. Код 407 означает, что прокси требует логинОтправляйте каждый токен один раз и сразу, из Site.SignUp

Использование CapSkip на сервере

В режиме Server CapSkip слушает адрес вашей сети или публичный IP, поэтому тот же код hCaptcha на C# работает с другого ПК, VPS с Windows или размещённой платформы. Режиму Server нужно, чтобы CapSkip был запущен от имени администратора, а руководство по настройке рекомендует статический публичный IP. Замените хост в строке с CapSkipClient адресом сервера, и больше ничего не изменится.

Когда запускается режим Server, CapSkip открывает свой порт для всех адресов, поэтому чтобы оставить доступ только нужным машинам, добавьте блокирующее правило Windows Firewall для остальных и включите API Key Validation, указав apiKey в строке с CapSkipClient. Теперь решение выполняется на сервере, поэтому hCaptcha видит сеть этой машины.

FAQ

Как решить невидимую hCaptcha или hCaptcha Enterprise на C#?

Тем же вызовом HCaptchaAsync. Для виджета, у которого data-size равен invisible, передайте true в bool invisible. Для сайта Enterprise, который задаёт rqdata, передайте значение под ключом data и читайте новое для каждого задания, потому что в документации сказано, что сайт выдаёт его для одной проверки. Оба случая показывает первый фрагмент в шаге 4.

Можно ли зарегистрировать SDK через IHttpClientFactory?

Регистрировать нечего. Пакет выполняет свои вызовы к CapSkip через один собственный статический HttpClient. Site.SignUp тоже не отдавайте фабрике: в рекомендациях Microsoft предупреждают, что обработчики из пула делят свой CookieContainer.

Почему сайт отклоняет токен, который мне дал CapSkip?

Строгий сайт может отклонить токен, который hCaptcha оценила как рискованный, даже если виджет был пройден, и чаще всего так бывает на сайтах Enterprise. В документации советуют High score mode в настройках hCaptcha. Ему нужен Google Chrome на ПК с CapSkip, а по моему прочтению исходного кода 4.1.0 он включён по умолчанию. Проверить его на строгом сайте мне было негде.

Решение hCaptcha на C# сводится к одному вызову HCaptchaAsync, циклу, который повторяет только те коды, которые стоит повторять, и отправке формы из обработчика с правильными cookie и User-Agent. CapSkip даёт локальный обход капчи по фиксированной лицензии, поэтому новая попытка стоит лишь затраченного времени. На странице сервиса распознавания капчи для C# описаны остальные типы CAPTCHA в .NET, а в демо hCaptcha видно, как виджет проходит проверку.