Как решить DataDome CAPTCHA на C# с HttpClient

datadome captcha in c# - How to Solve DataDome CAPTCHA in C# With HttpClient

Чтобы решить DataDome CAPTCHA на C#, вы читаете страницу блокировки с кодом 403, собираете по ней адрес проверки и отправляете этот адрес в решатель CapSkip для DataDome через DataDomeAsync и повторяете запрос через HttpClient с возвращённым cookie. Ответом служит cookie, а не токен для формы, и в документации CapSkip сказано, что оно работает только с тем User-Agent и IP-адресом, с которыми было получено. В этом руководстве весь цикл собран в одном консольном проекте с пакетом CapSkip из NuGet и больше ни с чем. CapSkip работает на вашем собственном ПК с Windows без платы за каждое решение, поэтому повтор ничего не стоит.

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

  • CapSkip 3.2.0 или новее, запущенный на ПК с Windows. В этом выпуске появилось решение DataDome для ползунка и проверки устройства. Если вы запускаете CapSkip впервые, руководство по настройке проведёт вас через первый запуск.
  • Платформа .NET 8 SDK или новее и пакет CapSkip из NuGet версии 1.5.0 или новее. В более ранних версиях нет метода DataDomeAsync. Код ниже запускался на .NET SDK 10.0.100, на net10.0 и net8.0 с CapSkip 1.5.0.
  • Окно PowerShell. Команды написаны в синтаксисе PowerShell.
  • Страница, которой вы владеете или которую вам разрешено автоматизировать, за защитой DataDome.
  • Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства. В режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы программа на другом ПК, VPS или размещённой платформе могла обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.
  • Sticky-прокси, то есть прокси с постоянным IP-адресом, на каждую задачу, когда вы перейдёте от тестов к работе. Cookie привязано к IP-адресу, с которого оно получено, и это объясняет шаг 3.

Как это проверялось: код запускался на трёх имитациях серверов, которые я написал, а не на настоящем сайте с DataDome. Первая копирует проверки, перечисленные в разделе DataDome на странице CapSkip Документация API, вторая изображает защищённый сайт, который отдаёт страницу блокировки с кодом 403 и принимает только cookie, выданное CapSkip, вместе с тем User-Agent, для которого оно выдано, а третья изображает прокси-сервер. SDK был настоящим, пакет CapSkip 1.5.0 из NuGet. Приложение CapSkip на моём ПК не было запущено, поэтому здесь нет ничего, что показывало бы, сколько длится настоящее решение или что принимает живой сайт с DataDome. Правила для cookie взяты из документации CapSkip. Собственная документация DataDome по cookie сообщает, что cookie datadome действует один год, и предупреждает не менять его Domain и Path. К чему привязано cookie, в ней не сказано.

Шаг 1: создаём проект и читаем страницу блокировки

Создайте консольный проект и добавьте пакет. Шаблон по умолчанию включает implicit usings и nullable-типы, и код опирается на то и другое.

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

Когда DataDome сомневается в запросе, сайт отвечает статусом 403, иногда 401, и небольшой HTML-страницей вместо содержимого. На этой странице есть объект скрипта с именем dd, и она загружает проверку в iframe с geo.captcha-delivery.com. Именно адрес iframe нужен CapSkip, и в документации он называется captcha_url. Если iframe уже есть на странице, можно взять его src как есть. Большинство страниц блокировки строят iframe скриптом, поэтому первый блок собирает такой же адрес из объекта dd. Первый запрос уходит с обычным User-Agent от Chrome. Эта строка в CapSkip не передаётся, поэтому версия Chrome в ней не обязана ни с чем совпадать, и я не проверял, отдаёт ли живой сайт с DataDome ту же проверку при другом User-Agent. Замените содержимое Program.cs четырьмя блоками из этого руководства, один за другим, начиная с этого.

Значение rt выбирает путь. Значение c означает ползунок под /captcha/, а i означает проверку устройства под /interstitial/. Важно и значение t: fe означает проверку, а bv означает, что DataDome заблокировал IP-адрес, с которого была загружена страница.

using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
using CapSkip;

static class BlockPage
{
    // Build the challenge address from a DataDome block page, or return null.
    public static string? CaptchaUrl(HttpStatusCode status, string html, string pageUrl)
    {
        if (status != HttpStatusCode.Forbidden && status != HttpStatusCode.Unauthorized) return null;
        var found = Regex.Match(html, @"var\s+dd\s*=\s*(\{[^}]*\})");
        if (!found.Success) return null;

        using var doc = JsonDocument.Parse(found.Groups[1].Value.Replace('\'', '"'));
        var dd = doc.RootElement;
        string? Get(string name) => dd.TryGetProperty(name, out var v)
            ? (v.ValueKind == JsonValueKind.String ? v.GetString() : v.GetRawText())
            : null;

        var query = new List<(string Key, string? Value)>
        {
            ("initialCid", Get("cid")), ("hash", Get("hsh")), ("cid", Get("cookie")),
        };
        if (!string.IsNullOrEmpty(Get("t"))) query.Add(("t", Get("t")));  // fe challenge, bv banned
        query.Add(("referer", pageUrl));
        query.Add(("s", Get("s")));
        foreach (var key in new[] { "e", "b" })
            if (!string.IsNullOrEmpty(Get(key))) query.Add((key, Get(key)));
        query.Add(("dm", "cd"));

        var path = Get("rt") == "i" ? "interstitial" : "captcha";
        var qs = string.Join("&", query.Select(p => p.Key + "=" + Uri.EscapeDataString(p.Value ?? "")));
        return $"https://{Get("host") ?? "geo.captcha-delivery.com"}/{path}/?{qs}";
    }
}

Метод возвращает null для любого ответа, который не является страницей блокировки, чтобы вызывающий код мог отличить “решать нечего” от “решено”. Каждая страница блокировки несёт новую проверку. Используйте её адрес сразу и запрашивайте страницу заново для следующей задачи, а не берите старый.

Шаг 2: решение через DataDomeAsync

Метод SDK называется DataDomeAsync(captchaUrl, url, options) и следует порядку аргументов 2Captcha. Два адреса передаются по позиции, а userAgent и proxy идут в словарь options. В CapSkip оба необязательны. Если User-Agent не передать, CapSkip использует User-Agent своего браузера и вернёт его в результате как UserAgent. Если передать, это должен быть Chrome для Windows с той же основной версией, что и у браузера CapSkip, поэтому проще всего его не указывать. Метод ждёт ответа дольше, с тайм-аутом 300 секунд, как и для reCAPTCHA, потому что CapSkip проходит проверку в настоящем браузере. В документации API сказано, что решение обычно занимает от 3 до 20 секунд; я это время не замерял.

Прокси здесь тот же, которым будут пользоваться ваши следующие запросы. Передайте его, если программа выходит через прокси, и ничего не передавайте, если запросы уходят с машины, на которой работает CapSkip. Если CapSkip стоит на сервере, этой машиной будет сервер. Класс Proxy принимает тип и адрес, например HTTP и user:[email protected]:8080.

static class Solve
{
    // Local mode. In Server mode, use your server's address instead.
    static readonly CapSkipClient Solver = new(host: "127.0.0.1", port: 8080);

    public static Task<SolveResult> Run(string captchaUrl, string pageUrl, string? proxyUrl)
    {
        var options = new Dictionary<string, object?>();
        if (proxyUrl != null)
        {
            var i = proxyUrl.IndexOf("://", StringComparison.Ordinal);
            options["proxy"] = new Proxy(proxyUrl[..i].ToUpperInvariant(), proxyUrl[(i + 3)..]);
        }
        // No userAgent option: CapSkip uses its own browser's and reports it.
        return Solver.DataDomeAsync(captchaUrl, pageUrl, options);
    }
}

Если проверку устройства пройти нельзя, DataDome переходит к ползунку. Согласно документации CapSkip, тот же вызов следует за этим переходом, так что отдельный код для двух проверок не нужен.

Шаг 3: отправка cookie с его User-Agent и IP-адресом

Результат представляет собой SolveResult с четырьмя важными свойствами. Свойство Code результата содержит cookie точно в том виде, в каком его установил DataDome, с Max-Age, Domain и другими атрибутами, а именно это нужно хранилищу cookie. Свойство Cookie содержит только пару имени и значения datadome, а именно это нужно заголовку Cookie. Свойство UserAgent содержит User-Agent, с которым cookie было получено. Свойство Proxy задано только тогда, когда вы не передавали прокси и CapSkip решал через собственный список прокси для DataDome. В документации CapSkip сказано, что DataDome отвергает cookie с другого IP-адреса или с другим User-Agent, поэтому отправляйте один и тот же User-Agent каждый раз, а если в результате указан прокси, отправляйте запрос через него. Свойство Proxy содержит только хост и порт, без схемы и без логина. Программа добавляет http://, поэтому предполагает прокси из списка без логина. Если у вашего логин есть, добавьте его.

Помощник ниже задаёт заголовок Cookie вручную и отключает UseCookies, чтобы HttpClient отправлял его как написано. Он также читает сменное cookie из ответа, потому что DataDome может прислать его в одном из следующих ответов, а браузер его сохранил бы. Чтобы код был короче, он создаёт HttpClient на каждый вызов. Для большого числа запросов держите вместо этого один клиент на прокси.

static class Web
{
    static HttpClient Client(string? proxyUrl)
    {
        // UseCookies is off, so the Cookie header set by hand goes out as it is.
        var handler = new HttpClientHandler { UseCookies = false };
        if (proxyUrl != null)
        {
            var uri = new Uri(proxyUrl);
            var proxy = new WebProxy($"{uri.Scheme}://{uri.Host}:{uri.Port}");
            if (uri.UserInfo.Length > 0)
            {
                var login = uri.UserInfo.Split(':', 2);
                proxy.Credentials = new NetworkCredential(
                    Uri.UnescapeDataString(login[0]),
                    login.Length > 1 ? Uri.UnescapeDataString(login[1]) : "");
            }
            handler.Proxy = proxy;
        }
        return new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
    }

    // Returns the status, the body and a replacement datadome cookie if the site sent one.
    public static async Task<(HttpStatusCode Status, string Body, string? Fresh)> Get(
        string url, string userAgent, string? cookie, string? proxyUrl)
    {
        using var http = Client(proxyUrl);
        using var request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Headers.TryAddWithoutValidation("User-Agent", userAgent);
        if (cookie != null) request.Headers.TryAddWithoutValidation("Cookie", cookie);
        using var response = await http.SendAsync(request);

        string? fresh = null;
        if (response.Headers.TryGetValues("Set-Cookie", out var lines))
            fresh = lines.Select(l => Regex.Match(l, "^datadome=[^;]+"))
                         .FirstOrDefault(m => m.Success)?.Value;
        return (response.StatusCode, await response.Content.ReadAsStringAsync(), fresh);
    }
}

Шаг 4: полный рабочий пример

Последний блок собирает весь процесс решения DataDome CAPTCHA на C# и добавляет повтор. Запустите его командой dotnet run. Каждая попытка заново запрашивает страницу, поэтому в каждой попытке новая проверка. Неудачное решение выбрасывает ApiException, и цикл пробует снова. Cookie, на которое всё равно пришёл 403, тоже отправляет цикл на новый круг, уже с новой страницей блокировки.

static class Program
{
    const string PageUrl = "https://www.example.com/";  // a page behind DataDome
    const string Browser = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
                           "(KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36";
    // The proxy your requests use, e.g. "http://user:[email protected]:8080". Null means
    // they leave from the PC that runs CapSkip.
    static readonly string? ProxyUrl = null;

    static async Task<int> Main()
    {
        for (var attempt = 1; attempt <= 3; attempt++)
        {
            var (status, body, _) = await Web.Get(PageUrl, Browser, null, ProxyUrl);  // fresh block page
            var captchaUrl = BlockPage.CaptchaUrl(status, body, PageUrl);
            if (captchaUrl is null)
            {
                Console.WriteLine($"{(int)status}: no DataDome block page on this response");
                return 0;
            }

            SolveResult result;
            try { result = await Solve.Run(captchaUrl, PageUrl, ProxyUrl); }
            catch (ApiException ex)
            {
                Console.WriteLine($"attempt {attempt}: {ex.Message}");  // the next try gets a new page
                continue;
            }

            // The cookie works only from the address and User-Agent that earned it.
            var via = ProxyUrl ?? (result.Proxy is null ? null : "http://" + result.Proxy);
            var cookie = result.Cookie;
            var (code, page, fresh) = await Web.Get(PageUrl, result.UserAgent!, cookie, via);
            if (code != HttpStatusCode.OK)
            {
                Console.WriteLine($"attempt {attempt}: HTTP {(int)code} with the cookie");
                continue;
            }
            cookie = fresh ?? cookie;  // DataDome may rotate the cookie; keep the newest
            Console.WriteLine($"{(int)code} {page[..Math.Min(40, page.Length)]}");
            var (again, _, _) = await Web.Get(PageUrl, result.UserAgent!, cookie, via);  // reuse it
            Console.WriteLine((int)again);
            return 0;
        }
        Console.WriteLine("DataDome not passed in 3 attempts");
        return 1;
    }
}

SDK отказывается принимать captchaUrl с t=bv, ничего не отправив, потому что решение с такого адреса пройти не может. Это ValidationException, а не ApiException, поэтому цикл его не перехватывает. Дайте ему подняться: это сигнал сменить адрес и начать заново. Блокировка, о которой вместо этого сообщает CapSkip как ERROR_PROXY_BANNED, приходит как ApiException, и цикл повторяет попытку с тем же адресом, поэтому если вы видите эту ошибку в выводе, смените прокси перед следующим запуском. Смотрите также: руководство по ротации прокси (о смене прокси) и детектор капчи на C# (о том, как определить, какую капчу использует страница, до выбора метода).

В запуске на имитациях программа вела себя так:

Настройка имитацииЧто сделала программа
Страница блокировки с ползунком, на каждое решение пришёл ответОдна задача, затем статус 200 с cookie и снова 200 при повторном использовании
Страница блокировки с проверкой устройстваВыполнился тот же код: captchaUrl указывал на /interstitial/, и результат был тем же
Первые два решения отклоненыДве строки попыток, затем статус 200 на третьем задании
Первые три решения отклоненыТри строки с попытками, затем сообщение о том, что DataDome не пройден, и код выхода 1
Результат указывает прокси, как это делает собственный список CapSkipЗапрос с cookie прошёл через этот прокси и получил 200
Ваш собственный прокси, заданный в ProxyUrlin.php получил прокси и его тип, и запросы использовали его
Страница блокировки с t=bvНеобработанный ValidationException, и имитация CapSkip не получила ни одного запроса

Одно наблюдение из запуска с прокси: текст после схемы уходит в CapSkip в том виде, как вы его написали. Логин, записанный в ProxyUrl с процентным кодированием, например pa%24s для пароля со знаком доллара, пришёл в in.php всё ещё закодированным. Не используйте в логине символы, которые требуют кодирования, или соберите аргумент Proxy из декодированных значений сами.

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

ОшибкаПричинаИсправить
ValidationExceptioncaptchaUrl, который не является адресом проверки DataDome, страница блокировки с t=bv, url не с http или https, или userAgent не от Chrome для Windows. Сообщение говорит, какой из этих случаев имеет местоСоберите адрес заново из свежей страницы блокировки, смените адрес, если он заблокирован, и уберите параметр userAgent
ApiException с ERROR_WRONG_USERAGENTUser-Agent относится к Chrome для Windows, но его основная версия отличается от версии браузера CapSkipНе передавайте userAgent и используйте тот, который указан в результате
ApiException с ERROR_CAPTCHA_UNSOLVABLEПроверка не была пройденаЗапросите страницу заново и отправьте её новый captchaUrl
ApiException с ERROR_PROXY_BANNEDCapSkip отказывается принимать задачу, если на странице блокировки t=bv, согласно документации APIСмените прокси и запросите страницу заново
ApiException с ERROR_PROXY_CONNECT_REFUSEDCapSkip сообщает об этом, когда не может подключиться через прокси. Воспроизвести это мне не удалосьПроверьте адрес, логин и тип прокси
CapSkip.TimeoutExceptionCapSkip не дал ответа за 300 секунд ожидания. Это не ApiException, поэтому цикл его не перехватывает. Пишите полное имя, потому что в System тоже есть TimeoutExceptionПроверьте, что CapSkip запущен и решает, затем запустите программу снова
NetworkExceptionCapSkip не запущен либо неверно указаны хост или портЗапустите CapSkip и проверьте настройки подключения
Всё равно 403 при отправке cookieЗапрос ушёл с другого IP-адреса, с другим User-Agent, или cookie истеклоКаждый раз отправляйте UserAgent и Proxy из результата или запустите цикл заново с новой страницей блокировки

Запуск решателя на сервере

Программе нужны от CapSkip две вещи: хост и порт. В режиме Local это 127.0.0.1 и ваш собственный ПК. В режиме Server CapSkip слушает ваш сетевой адрес или публичный IP, поэтому программа на другом ПК, VPS или размещённой платформе может обращаться к нему через тот же API. Смените хост и порт в конструкторе CapSkipClient, и больше ничего не меняется. Для стабильного удалённого доступа рекомендуется статический публичный IP. Режим Server всё равно работает на вашем оборудовании и всё равно без счётчика. Он открывает порт для любого адреса, поэтому добавьте в Windows Firewall блокирующее правило для адресов, которым доступ к нему не нужен. Помните, что решение выполняется на этой машине, поэтому cookie получено с IP-адреса сервера. Если вы не передаёте прокси, запрос с cookie тоже должен уходить с этого сервера. Если прокси есть, передавайте один и тот же и в решение, и в свои запросы. Подробности смотрите в разделе Настройки подключения с описанием переключения режимов.

FAQ

Возвращает ли DataDomeAsync токен?

Нет. Он возвращает cookie datadome. Это cookie вы отправляете в заголовке Cookie того запроса, который был заблокирован, вместе с тем же User-Agent. Поля формы для заполнения нет.

Нужен ли мне прокси для DataDome?

Для проверки нет. Для первого запуска хватит одного IP-адреса. Согласно документации CapSkip, cookie работает только с IP-адреса, с которого оно получено, поэтому когда запросов станет много, планируйте один sticky-прокси на задачу и передавайте один и тот же прокси и в решение, и в свои запросы.

Может ли размещённая платформа запускать это через мой CapSkip?

Да, через режим Server. Настройте CapSkip на прослушивание вашей сети или публичного IP, направьте конструктор CapSkipClient на этот хост и порт, и цикл работает так же, с одним изменением: решение теперь выполняется на сервере, поэтому давайте решению и своим запросам один и тот же прокси. Решатель остаётся на вашем оборудовании.

Коротко

Решение DataDome CAPTCHA на C# состоит из четырёх действий: прочитать страницу блокировки, собрать captchaUrl, вызвать DataDomeAsync и повторить запрос с cookie вместе с User-Agent и IP-адресом, к которым оно относится. На вашей собственной машине повтор бесплатен, и это особенно важно, когда защищённый сайт раз за разом выдаёт новые проверки. Выбирайте распознавание капчи и повторяйте попытки столько раз, сколько нужно. Об остальных возможностях SDK читайте на странице сервиса распознавания капчи для C#.