Cara Memecahkan CAPTCHA DataDome di PHP (Guzzle CookieJar)

datadome captcha in php - How to Solve DataDome CAPTCHA in PHP (Guzzle CookieJar)

CAPTCHA DataDome di PHP berakhir dengan sebuah cookie, bukan token. Panduan ini membaca halaman blokir 403, membangun alamat challenge, lalu meneruskannya ke metode dataDome() dari paket capskip/capskip, yang berkomunikasi dengan pemecah DataDome CapSkip. Setelah itu, cookie yang dikembalikan disimpan di jar milik klien Guzzle yang mengirim permintaan berikutnya. Dokumentasi CapSkip menyebut bahwa cookie hanya berfungsi dengan User-Agent dan alamat IP yang memperolehnya, dan klien Guzzle mengunci keduanya.

Apa yang Anda butuhkan

  • CapSkip 3.2.0 atau lebih baru di PC Windows, versi pertama yang memecahkan DataDome (slider dan device check).
  • PHP 8.0.2 atau lebih baru dengan ekstensi curl dan json, serta Composer, keduanya ada di PATH Anda. Platform check milik Composer menyebut 8.0.2 pada instalasi 8.0.30 dan 8.1.0 pada 8.1.33 dan 8.4.15.
  • Folder kerja yang kosong. Jalankan composer require capskip/capskip guzzlehttp/guzzle di dalamnya dan simpan skrip di sana sebagai datadome-demo.php. Rilis capskip sebelum 1.5.0 tidak memiliki metode dataDome.
  • Sebuah CA bundle. PHP untuk Windows datang tanpa CA bundle, sehingga permintaan https pertama gagal dengan cURL error 60 sampai Anda mengunduh cacert.pem dari curl.se dan mengarahkan curl.cainfo di php.ini ke file itu.
  • Halaman yang dilindungi DataDome dan boleh Anda otomatisasi.
  • Sebuah host dan port untuk CapSkip. Dalam mode Local, CapSkip hanya menjawab di 127.0.0.1, untuk skrip di PC yang sama. Mode Server memungkinkan PHP di mesin lain memanggilnya lewat jaringan atau IP publik Anda. Anda memilih mode itu di pengaturan koneksi.
  • Untuk keperluan di luar pengujian, satu proxy sticky per task.

Metode pengujian: dua server mock yang saya tulis, hanya di Windows, tanpa situs DataDome yang aktif. Satu meniru pemeriksaan di bagian DataDome pada Dokumentasi API. Satunya lagi berperan sebagai situs terlindungi yang menjawab 403 dengan halaman blokir, hanya menerima cookie yang diterbitkan mock pertama, dan juga meneruskan permintaan sebagai proxy. Untuk https, mock ini memakai CA sekali pakai. Skrip tidak pernah menyentuh aplikasi CapSkip, jadi tidak ada pengukuran waktu untuk pemecahan sungguhan dan tidak ada putusan dari situs yang aktif.

Blok kode dijalankan di PHP 8.0.30, 8.1.33, 8.2.29, 8.3.28, dan 8.4.15 dengan capskip 1.6.0 dan Guzzle 8.3.1, serta di 8.0.30 dan 8.4.15 dengan capskip 1.6.0 dan Guzzle 7.16.1. Di PHP 8.5.0 proses tetap selesai, tetapi file Http.php di SDK mencetak pemberitahuan Deprecated tentang curl_close() untuk setiap permintaan yang dikirimnya ke CapSkip, tiga kali dalam run saya.

Langkah 1: satu klien Guzzle per identitas, lalu halaman blokir

Letakkan kelima blok kode di datadome-demo.php sesuai urutan yang ditampilkan. Blok pertama memuat semua baris use, jadi blok lainnya tidak menambahkannya.

Tiga default Guzzle bertentangan dengan loop ini. Klien tidak menyimpan cookie kecuali Anda memberinya jar. Klien menyebut dirinya GuzzleHttp ditambah versi mayornya, yang terlihat oleh mock dari klien polos di Guzzle 7 dan 8. Dan klien melempar ClientException pada setiap jawaban 4xx, yang paling berpengaruh di sini, karena halaman blokir 403 adalah challenge yang perlu Anda baca. Dengan default itu dibiarkan aktif, run saya mati pada permintaan pertama dan mock CapSkip tidak pernah menerima task. Jadi session() mematikan http_errors. Fungsi ini juga menyetel jar, User-Agent, dan proxy bersama-sama, sehingga ketiganya tidak bisa saling menyimpang. Identitas baru berarti klien baru.

<?php
// composer require capskip/capskip guzzlehttp/guzzle
require __DIR__ . '/vendor/autoload.php';

use CapSkip\CapSkip;
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\CapSkipError;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Cookie\SetCookie;
use Psr\Http\Message\ResponseInterface;

const PAGE_URL = 'https://www.example.com/'; // a page behind DataDome
// For the first request only. The cookie comes back with its own User-Agent.
const BROWSER_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
    . '(KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36';

// Guzzle fixes its options when the Client is built, so one Client is one
// identity: a cookie jar, a User-Agent and a proxy.
function session(CookieJar $jar, string $userAgent, ?string $proxyUrl = null): Client
{
    return new Client([
        'cookies' => $jar,
        'headers' => ['User-Agent' => $userAgent],
        'proxy' => $proxyUrl,
        'http_errors' => false, // a 403 block page is an answer, not an exception
        'timeout' => 30,
    ]);
}

Permintaan yang dicurigai DataDome mendapat 403, atau kadang 401, dan halaman HTML pendek. Alamat challenge, yang oleh dokumentasi disebut captcha_url, bisa berupa src sebuah iframe dari geo.captcha-delivery.com atau sesuatu yang dirakit skrip dari objek dd. Fungsi di bawah menangani keduanya. Entri rt pada objek dd menentukan halaman challenge: huruf c memilih slider di /captcha/, dan huruf i memilih device check di /interstitial/. Dokumentasi mendefinisikan nilai t bv sebagai alamat yang diblokir dan menampilkan fe pada challenge biasa; tafsiran fe itu berasal dari dokumentasi mereka.

Permintaan pertama dikirim dengan User-Agent Chrome biasa yang tidak pernah dikirim ke CapSkip, jadi versinya tidak harus cocok dengan apa pun. Setiap halaman blokir adalah challenge baru, jadi pakai alamatnya segera.

// Build the challenge address from a DataDome block page, or null.
function captchaUrlFrom(ResponseInterface $resp): ?string
{
    if (!in_array($resp->getStatusCode(), [401, 403], true)) {
        return null;
    }
    $html = (string) $resp->getBody();
    // Some block pages carry the challenge iframe: its src is the address.
    if (preg_match('/<iframe[^>]+src="(https:\/\/[^"]*captcha-delivery\.com\/[^"]+)"/', $html, $found)) {
        return $found[1]; // an HTML-escaped &amp; is fine, CapSkip decodes it
    }
    // Most build it from the dd object in a script instead.
    if (!preg_match('/var\s+dd\s*=\s*(\{[^}]*\})/', $html, $found)) {
        return null;
    }
    $dd = json_decode(str_replace("'", '"', $found[1]), true);
    if (!is_array($dd)) {
        return null;
    }
    $query = ['initialCid' => $dd['cid'] ?? '', 'hash' => $dd['hsh'] ?? '', 'cid' => $dd['cookie'] ?? ''];
    if (!empty($dd['t'])) {
        $query['t'] = $dd['t']; // fe is a challenge, bv means the address is banned
    }
    $query['referer'] = PAGE_URL;
    $query['s'] = $dd['s'] ?? '';
    foreach (['e', 'b'] as $key) {
        if (!empty($dd[$key])) {
            $query[$key] = $dd[$key];
        }
    }
    $query['dm'] = 'cd';
    $path = ($dd['rt'] ?? '') === 'i' ? 'interstitial' : 'captcha';

    return 'https://' . ($dd['host'] ?? 'geo.captcha-delivery.com') . "/{$path}/?"
        . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
}

Langkah 2: pecahkan challenge dan masukkan cookie ke jar

Metodenya adalah dataDome(string $captchaUrl, string $url, array $options = []), dan datadome() dalam huruf kecil mengembalikan hasil yang sama dalam run saya. Array options menerima userAgent dan proxy, dan nilai null di dalamnya tidak dikirim. Proxy dimasukkan sebagai array dengan type dan uri, seperti di bawah, atau sebagai string biasa, yang dikirim SDK sebagai proxy HTTP.

Jangan sertakan userAgent. CapSkip lalu memakai User-Agent milik browsernya sendiri dan melaporkannya di hasil, dan Anda mengirim string itu seterusnya. User-Agent yang Anda berikan harus Chrome standar untuk Windows dengan versi mayor yang sama dengan browser itu: SDK menolak string Firefox secara lokal, dan mock menjawab ERROR_WRONG_USERAGENT untuk string Chrome 120. Metode ini menunggu hingga recaptchaTimeout, 300 detik secara default.

// Local mode. In Server mode, use your server's address instead.
$solver = new CapSkip(['host' => '127.0.0.1', 'port' => 8080]);

function solve(CapSkip $solver, string $captchaUrl, ?string $proxyUrl): array
{
    $options = [];
    if ($proxyUrl !== null) {
        [$scheme, $rest] = explode('://', $proxyUrl, 2);
        $options['proxy'] = ['type' => strtoupper($scheme), 'uri' => $rest];
    }
    // No userAgent option: CapSkip uses its own browser's and reports it.
    return $solver->dataDome($captchaUrl, PAGE_URL, $options);
}

Hasilnya adalah array dengan key captchaId, code, userAgent, dan cookie. Nilai code adalah cookie persis seperti yang diatur DataDome, lengkap dengan atributnya. Nilai cookie hanya berisi pasangan nama dan nilai datadome. Key proxy muncul hanya jika Anda tidak memberikan proxy dan CapSkip memakai proxy dari daftar DataDome miliknya sendiri. Key itu berisi host dan port tanpa skema atau login, jadi tambahkan sendiri loginnya jika entri daftar itu memerlukannya. Cookie itu kemudian hanya berfungsi melalui proxy tersebut.

Exception milik SDK berada di bawah CapSkip\Exceptions dan mewarisi CapSkipError, yang mewarisi Exception, bukan RuntimeException. Hanya ApiException, yaitu task yang ditolak, yang merupakan pemecahan gagal yang layak dicoba lagi.

Di sinilah skrip PHP berbeda dari skrip yang menyusun header Cookie. Nilai code langsung di-parse menjadi SetCookie milik Guzzle, lalu jar menerapkan aturan browser: jar mengirim cookie hanya ke host yang cocok dengan atribut Domain, untuk atribut Path yang cocok, sampai Max-Age habis, dan melalui https saja jika atribut Secure disetel. Jar juga menyimpan cookie pengganti. Mock mengirim cookie baru bersama halaman 200, dan permintaan berikutnya melalui klien yang sama membawanya.

Jar gagal secara diam-diam dengan empat cara, dan saya mengalami masing-masing. Cookie tanpa atribut Domain disimpan tetapi tidak pernah dikirim. Ketidakcocokan pada Domain atau Secure tidak mengirim header Cookie, sehingga situs menjawab 403 padahal jar menyimpan cookie itu. setCookie() mengembalikan false alih-alih melempar exception saat menolak sebuah cookie, misalnya cookie yang sudah kedaluwarsa. Dan cookie datadome yang disetel oleh halaman blokir itu sendiri tetap berada di jar di samping cookie hasil pemecahan, sehingga keduanya terkirim dan situs bisa membaca yang salah. Mock pertama saya tidak pernah menyetel cookie itu; begitu mock menyetelnya, ketiga percobaan gagal sampai pemanggilan clear() di bawah menghapusnya. Blok kode ini menjaga keempatnya.

function sendWithCookie(array $result, CookieJar $jar, ?string $proxyUrl): array
{
    // code is the cookie as DataDome set it, with Domain, Path and Max-Age.
    $cookie = SetCookie::fromString($result['code']);
    if (!$cookie->getDomain()) {
        // A jar keeps a cookie with no Domain but never sends it.
        $cookie->setDomain(parse_url(PAGE_URL, PHP_URL_HOST));
    }
    // The block page may have set a datadome cookie of its own. Drop it.
    $jar->clear(parse_url(PAGE_URL, PHP_URL_HOST), '/', 'datadome');
    if (!$jar->setCookie($cookie)) { // false, not an exception, for a cookie it rejects
        throw new RuntimeException('The cookie jar refused: ' . $result['code']);
    }
    // The cookie works only from the address that earned it.
    $via = $proxyUrl;
    if ($via === null && !empty($result['proxy'])) {
        // CapSkip solved through its own list. Add user:pass@ if that entry needs a login.
        $via = 'http://' . $result['proxy'];
    }
    $http = session($jar, $result['userAgent'], $via);

    return [$http, $http->get(PAGE_URL)];
}

Langkah 3: contoh lengkap dengan loop percobaan ulang untuk 403

Blok terakhir menyatukan semua langkah. Tambahkan ke datadome-demo.php dan jalankan php datadome-demo.php di folder kerja. Setiap percobaan dimulai dengan jar dan klien baru, jadi setiap percobaan membawa challenge baru dan tidak membawa cookie dari percobaan yang gagal. Pilihan itu berasal dari saya dan belum diuji terhadap DataDome.

Loop mencoba ulang ApiException seperti ERROR_CAPTCHA_UNSOLVABLE, dan cookie yang tetap mendapat 403. ApiException yang menyebut ERROR_PROXY_ langsung dilempar ulang, karena alamat yang sama akan gagal dengan cara yang sama. Respons 403 tanpa objek dd atau iframe menghentikan loop. Setiap percobaan ulang membuat situs menerima satu permintaan halaman blokir lagi, jadi tiga percobaan adalah batas yang masuk akal.

function fetchProtected(CapSkip $solver, ?string $proxyUrl = null): array
{
    for ($attempt = 1; $attempt <= 3; $attempt++) {
        $jar = new CookieJar(); // a clean jar for every attempt
        $http = session($jar, BROWSER_UA, $proxyUrl);
        $block = $http->get(PAGE_URL); // a fresh block page, a fresh challenge
        $captchaUrl = captchaUrlFrom($block);
        if ($captchaUrl === null) {
            if ($block->getStatusCode() >= 400) {
                throw new RuntimeException('HTTP ' . $block->getStatusCode() . ' without a DataDome block page');
            }
            return [$http, $block]; // nothing to solve here
        }
        try {
            $result = solve($solver, $captchaUrl, $proxyUrl);
        } catch (ApiException $e) {
            if (str_contains($e->getMessage(), 'ERROR_PROXY_')) {
                throw $e; // the address is the problem, so a retry fails the same way
            }
            echo "attempt {$attempt}: {$e->getMessage()}\n";
            continue;
        }
        [$http, $page] = sendWithCookie($result, $jar, $proxyUrl);
        if ($page->getStatusCode() === 200) {
            return [$http, $page];
        }
        echo "attempt {$attempt}: HTTP {$page->getStatusCode()} with the cookie\n";
    }
    throw new RuntimeException('DataDome not passed in 3 attempts');
}

try {
    [$http, $page] = fetchProtected($solver); // or ($solver, 'http://user:[email protected]:8080')
    echo $page->getStatusCode(), ' ', substr((string) $page->getBody(), 0, 40), PHP_EOL;
    // Same Client, so the same jar, User-Agent and proxy.
    echo $http->get(PAGE_URL)->getStatusCode(), PHP_EOL;
} catch (CapSkipError | RuntimeException $e) {
    fwrite(STDERR, get_class($e) . ': ' . $e->getMessage() . PHP_EOL);
    exit(1);
}

Skrip lengkap CAPTCHA DataDome di PHP mencetak ini terhadap mock:

200 <html><body><h1>Real content</h1></body>
200

Setiap baris di bawah hasilnya sama pada semua kombinasi itu, kecuali satu detail: Guzzle 8 melempar ConnectException untuk cURL error 60, dan Guzzle 7 melempar RequestException.

Pengaturan mockYang dilakukan skrip
Halaman blokir slider, device check, atau iframeSatu task, lalu 200, dan 200 lagi saat dipakai ulang
Dua pemecahan pertama ditolak, atau ketiganya200 pada task ketiga, atau RuntimeException dan exit code 1
Halaman blokir dengan t=bvValidationException; mock tidak menerima apa pun
Hasil menyebut sebuah proxy, atau Anda memberikan proxy sendiriKedua permintaan yang membawa cookie memakai proxy itu dan berhasil
Pemecahan dijawab ERROR_PROXY_BANNEDApiException setelah satu task
Atribut Domain tidak cocok, atau Secure pada http biasaTiga percobaan, masing-masing 403 dengan cookie
Cookie tanpa atribut Domain200 dengan fallback di blok Langkah 2, tiga percobaan 403 tanpanya
Halaman blokir yang menyetel cookie datadome-nya sendiri200 dengan clear() di blok Langkah 2, tiga percobaan 403 tanpanya
Situs https dengan CA sekali pakaicURL error 60 tanpa curl.cainfo; dengan curl.cainfo, 200, juga untuk cookie Secure

Pengikatan cookie, dan situs mock yang membaca cookie datadome pertama dari dua, adalah aturan yang saya tulis setelah membaca dokumentasi CapSkip. Aturan itu tidak menyatakan apa pun tentang layanan yang sebenarnya.

Command line atau permintaan web?

SDK menunggu hingga 300 detik untuk satu pemecahan. Menurut manual PHP default max_execution_time adalah 30 detik, dan 0 di command line. Saya menahan task mock selama 8 detik, jadi skrip memakan waktu sekitar 13 detik, dan saya mengukur setiap run di PHP 8.4.15 dari pernyataan pertama sampai terakhir; PHP 8.0.30 memberi hasil yang sama dalam selisih 0.1 detik. php.ini di PC ini berisi 120, dan untuk batas 5 saya memakai salinannya. php.exe mengabaikan max_execution_time di php.ini, jadi hanya flag -d atau set_time_limit() yang membatasi run di konsol, sedangkan php-cgi.exe, build CGI yang bisa dipakai web server untuk menjalankan PHP, membaca file itu.

Cara menjalankannyaHasil
php.exe, tanpa pengaturan (php.ini berisi 120, php.exe membaca 0)Selesai: status 200 dua kali, sekitar 13 detik
php.exe, batas 5 di php.iniSelesai: batas diabaikan, sekitar 13 detik
php.exe -d max_execution_time=5Fatal error setelah 7.9 detik
php-cgi.exe, batas 5 di php.iniFatal error setelah 7.9 detik
php-cgi.exe -d max_execution_time=5 dengan set_time_limit(60) sebagai pernyataan pertamaSelesai setelah sekitar 13 detik
php-cgi.exe, tanpa pengaturan (membaca 120 dari php.ini)Selesai setelah sekitar 13 detik

Saya hanya menjalankan ini di Windows, tempat batas ikut menghitung waktu menunggu, sehingga skrip yang terblokir di loop polling milik SDK menghabiskannya. PHP melempar fatal error pada sekitar 8 detik, bukan 5, karena skrip sedang tertidur di antara polling saat batas terlewati. Skrip mati tanpa membaca cookie, dan CapSkip sudah memakai pemecahannya. Jalankan loop ini dari konsol, scheduled task, atau queue worker; untuk bentuk worker-nya, lihat panduan queue Laravel. Di dalam permintaan web, panggil set_time_limit() terlebih dahulu dengan nilai yang lebih besar daripada waktu yang bisa dihabiskan satu pemecahan.

Kesalahan umum

KesalahanPenyebabPerbaiki
cURL error 60: SSL certificate problemPHP untuk Windows tidak memiliki CA bundleUnduh cacert.pem dari curl.se dan setel curl.cainfo di php.ini
ValidationExceptionSDK menolak masukan: alamat di luar captcha-delivery.com, halaman blokir dengan t=bv, URL halaman yang bukan http atau https, atau userAgent yang bukan Chrome standar untuk Windows. Pesannya menyebut yang manaBangun ulang alamat dari halaman blokir baru, ganti proxy jika diblokir, hapus userAgent
ApiException dengan ERROR_PROXY_BANNED, ERROR_PROXY_CONNECT_REFUSED, atau ERROR_WRONG_USERAGENTAlamat diblokir, CapSkip tidak bisa terhubung melalui proxy (tidak direproduksi), atau userAgent Anda memiliki versi mayor Chrome yang berbeda dari browser yang dipakai CapSkip untuk memecahkanGanti atau uji proxy, atau jangan kirim userAgent
TimeoutException atau NetworkExceptionWaktu tunggu 300 detik habis, atau tidak ada yang menjawab di host dan port yang Anda berikanJalankan CapSkip dan bandingkan host dan port Anda dengan pengaturan koneksinya
HTTP 403 dengan cookie padahal jar menyimpannyaAtribut Domain tidak cocok dengan host yang Anda minta, atau atribut Secure disetel dan URL-nya httpCetak isi jar dan bandingkan dengan PAGE_URL
Fatal error: Maximum execution time of 30 seconds exceededPermintaan web mencapai batas waktu PHP saat SDK melakukan polling; angkanya adalah batas AndaJalankan skrip dari konsol atau worker

FAQ

Bisakah saya menjalankan CAPTCHA DataDome di PHP terhadap CapSkip di sebuah server?

Bisa, itulah fungsi mode Server. CapSkip lalu menerima panggilan dari jaringan atau IP publik Anda (IP statis direkomendasikan), dan Anda hanya mengubah host dan port di constructor. Mesin Windows itu tetap milik Anda dan tanpa meteran. Mode Server membuka port untuk semua alamat saat dimulai, jadi jaga agar tetap pribadi dengan aturan blokir Windows Firewall untuk mesin yang tidak Anda harapkan. Pemecahan berjalan di server, jadi cookie milik alamat server itu: kirim permintaan Guzzle Anda dari mesin tersebut, atau berikan proxy yang sama untuk pemecahan dan klien.

Mengapa cookie jar dan bukan header Cookie?

Header juga berfungsi. Contoh datadome.php milik SDK sendiri mengirim nilai cookie di header dari handle cURL biasa; saya tidak menjalankannya. Jar menambahkan pencocokan atribut Domain dan Path, masa berlaku, dan cookie pengganti, dengan harga berupa kegagalan diam-diam di Langkah 2.

Apakah proxy diperlukan?

Tidak untuk pengujian pertama. Dokumentasi CapSkip menyebut cookie hanya berfungsi dari alamat yang memperolehnya, jadi pada volume besar Anda memerlukan satu proxy sticky per task. Untuk merotasinya, baca panduan rotasi proxy.

Versi singkatnya

Memecahkan CAPTCHA DataDome di PHP memerlukan satu klien Guzzle per identitas, halaman blokir yang dibaca dengan http_errors dimatikan, satu panggilan dataDome(), dan cookie yang dikembalikan di jar milik klien. Mencoba ulang tidak memakan biaya pada sebuah pemecah captcha lokal yang Anda jalankan sendiri, jadi biarkan loop meminta challenge baru alih-alih memakai ulang yang sudah basi. Untuk panggilan SDK pada tipe CAPTCHA lainnya, lihat halaman pemecah CAPTCHA PHP.