Cara Memecahkan reCAPTCHA di Symfony Panther (SDK PHP)

symfony panther captcha - How to Solve reCAPTCHA in Symfony Panther (PHP SDK)

Untuk memecahkan CAPTCHA Symfony Panther, baca sitekey dari halaman yang sedang dibuka Panther, lalu berikan sitekey itu beserta URL saat ini ke metode recaptcha dari SDK PHP CapSkip. Setelah itu, tulis token ke textarea g-recaptcha-response yang tersembunyi dengan executeScript, lalu kirim form dengan mengklik tombol milik form itu sendiri. Penulisan token inilah tempat kode Panther biasanya gagal. reCAPTCHA membiarkan textarea itu tetap display:none, sehingga setiap helper Panther yang seharusnya mengisinya, baik objek Form maupun sendKeys, melempar ElementNotInteractableException. Halaman yang mengirim form dari callback reCAPTCHA membutuhkan satu langkah lagi, karena halaman itu mengabaikan field tersebut sampai callback berjalan. Panduan ini membahas penyiapan di Windows, pemecahan, penulisan token, kedua jenis pengiriman, dan cara menunggu yang membuat proses tetap andal.

Apa yang Anda butuhkan

  • CapSkip yang berjalan di komputer Windows. Dalam mode Local, CapSkip menjawab di 127.0.0.1 hanya untuk perangkat itu; dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga Panther di mesin lain dapat memanggilnya melalui API. Keduanya diatur di pengaturan koneksi.
  • PHP 8.1 atau lebih baru, dengan ekstensi curl dan zip diaktifkan di php.ini. Panther 2.4 membutuhkan 8.1, Composer tidak akan menginstal php-webdriver tanpa zip, dan build PHP untuk Windows menyertakan kedua ekstensi itu dalam keadaan nonaktif.
  • Chrome, dan chromedriver dengan versi mayor yang sama. Alat bdi di bawah mengunduh versi yang tepat.
  • Halaman dengan widget reCAPTCHA v2, baik checkbox maupun invisible.
# PHP 8.1+; bdi fetches the chromedriver that matches your Chrome
composer require symfony/panther capskip/capskip
composer require --dev dbrekelmans/bdi
vendor\bin\bdi detect drivers

Baris terakhir adalah bentuk perintah untuk Windows; di Linux atau macOS, perintahnya vendor/bin/bdi. Perintah ini menyimpan chromedriver.exe di folder drivers di dalam proyek Anda. Panther mencari di folder itu dengan sendirinya, tetapi di Windows pencarian ini adalah jebakan. Panther menemukan file-nya, lalu gagal menjalankannya dari path relatif. Pesan error-nya berbunyi "Could not start chrome", diikuti pesan cmd.exe bahwa sebuah perintah tidak dikenali. Berikan path lengkap driver ke createChromeClient, seperti yang dilakukan setiap contoh di sini, atau tambahkan folder drivers ke PATH.

Langkah 1: buka halaman dan baca sitekey

Panther menjalankan Chrome headless melalui chromedriver dan memuat halaman. Setelah itu, api.js milik reCAPTCHA membangun widget dan menambahkan textarea dengan id g-recaptcha-response, jadi menunggu elemen itu adalah tanda yang andal bahwa widget sudah siap. Jangan menunggu elemen itu terlihat: elemen itu tidak pernah terlihat, dan waitForVisibility pada elemen itu akan timeout.

<?php
// composer require symfony/panther capskip/capskip
require __DIR__ . '/vendor/autoload.php';

use Symfony\Component\Panther\Client;

// The full path: on Windows, Panther finds drivers\chromedriver.exe on
// its own but then fails to start it from the relative path.
$client = Client::createChromeClient(__DIR__ . '/drivers/chromedriver.exe');
$client->request('GET', 'https://example.com/signup');

// api.js adds the hidden textarea once the widget has rendered.
$client->waitFor('#g-recaptcha-response', 20);
$crawler = $client->getCrawler();
$sitekey = $crawler->filter('[data-sitekey]')->attr('data-sitekey');

Sebagian besar halaman mendeklarasikan widget dengan div yang memiliki atribut data-sitekey. Halaman yang membangun widget dari skripnya sendiri dengan grecaptcha.render tidak memiliki atribut itu, tetapi key-nya tetap ada di halaman, yaitu di alamat iframe widget. Di halaman seperti itu, gunakan blok ini sebagai pengganti baris terakhir Langkah 1:

// grecaptcha.render() pages have no data-sitekey; the key is the
// k= parameter of the widget's iframe.
$src = $crawler->filter('iframe[title="reCAPTCHA"]')->attr('src');
parse_str((string) parse_url($src, PHP_URL_QUERY), $query);
$sitekey = $query['k'] ?? '';

Langkah 2: pecahkan dengan SDK PHP

SDK PHP CapSkip membutuhkan sitekey dan URL halaman tempat widget berjalan. Ambil URL dari Panther, bukan dari variabel Anda sendiri: setelah redirect, token harus diterbitkan untuk halaman yang sedang dibuka Panther.

use CapSkip\CapSkip;

$solver = new CapSkip(['host' => '127.0.0.1', 'port' => 8080]);

// The page Panther has open, after any redirect, so the token is minted
// for the URL the form really lives on.
$result = $solver->recaptcha($sitekey, $client->getCurrentURL());
$token = $result['code'];   // the result holds captchaId and code

Widget invisible muncul dalam salah satu dari dua bentuk: div dengan atribut data-size bernilai invisible, atau data-sitekey pada tombol submit itu sendiri. Untuk keduanya, berikan array opsi dengan invisible bernilai 1 sebagai argumen ketiga. Sebuah uji pemecahan terhadap sitekey uji publik milik Google melalui CapSkip sungguhan memakan waktu sekitar 13 detik dan menghasilkan token sepanjang 1.678 karakter.

SDK PHP bersifat sinkron, jadi skrip terblokir sampai token kembali, dan itu tidak masalah. Browser Panther hanya menunggu di halaman, karena timeout php-webdriver berlaku untuk setiap perintah, bukan untuk jeda di antara perintah. Sesi yang tetap terbuka selama pemecahan 103 detik tetap berhasil menyisipkan token dan mengirim form dengan normal sesudahnya. SDK melakukan polling hingga recaptchaTimeout-nya, 300 detik secara default, dan key timeout di array opsi mengubah nilai itu untuk satu pemanggilan. Yang justru bisa habis adalah tokennya. Token reCAPTCHA berlaku sekitar dua menit setelah diterbitkan, jadi tulis dan kirim token itu segera.

Langkah 3: tulis token dengan executeScript

Inilah langkah yang menentukan apakah proses CAPTCHA Symfony Panther berhasil. Langkah yang tampak jelas adalah mengisi field lewat objek Form milik Panther, dan cara itu gagal. Panther mengisi field seperti yang dilakukan pengguna, yaitu dengan mengosongkannya lalu mengetik ke dalamnya melalui WebDriver, dan WebDriver menolak mengetik ke elemen yang tidak bisa dilihat siapa pun. Menyetel field pada Form, memanggil setValues, memberikannya ke submit, atau memanggil sendKeys pada elemen itu, semuanya berakhir dengan ElementNotInteractableException yang sama. Sebagai gantinya, setel nilainya langsung di halaman:

// Throws ElementNotInteractableException: the textarea is display:none.
// $form['g-recaptcha-response'] = $token;

$client->executeScript(
    'document.getElementById("g-recaptcha-response").value = arguments[0];',
    [$token]
);

executeScript meneruskan setiap item dari array PHP ke skrip sebagai arguments[0], arguments[1], dan seterusnya, sehingga token tidak perlu di-escape ke dalam string JavaScript. Field yang terlihat, seperti alamat email, tetap diisi dengan API form Panther yang biasa.

Langkah 4: kirim dengan mengklik tombol yang sebenarnya

Cara Anda mengirim form mengubah apa yang dilihat halaman. Bangun form dari tombolnya, dan Panther akan mengirimnya dengan mengklik tombol itu, persis seperti yang dilakukan pengunjung:

// Built from its button, the form is sent by clicking that button, so the
// page's own submit handlers and HTML5 validation run as for a visitor.
$form = $client->getCrawler()->selectButton('Sign up')->form();
$form['email'] = 'YOUR_EMAIL';
$client->submit($form);

// Wait for something only the next page has; getCrawler() can lag.
$client->waitFor('#welcome', 15);
echo $client->getTitle(), PHP_EOL;

Cara pengiriman lainnya melewatkan sebagian hal, dan itu penting di halaman yang skripnya sendiri menambahkan field atau memeriksa form saat dikirim:

Cara form dikirimHandler submit halaman berjalanValidasi HTML5Nama dan nilai tombol ikut dikirim
Submit Panther pada form yang dibangun dari tombolnyaYaYaYa
Submit Panther pada form yang dibangun dari elemen formYaTidakTidak
form.submit() yang dijalankan melalui executeScriptTidakTidakTidak

Validasi ini bermata dua. Field wajib yang dibiarkan kosong, atau nilai yang tidak diterima field email, menghentikan klik tanpa error apa pun di PHP, dan tidak ada yang terkirim. Itulah salah satu alasan blok di atas menunggu elemen yang hanya ada di halaman berikutnya. Alasan lainnya, crawler Panther masih bisa menggambarkan halaman lama sesaat setelah submit, jadi kode yang langsung membaca getCrawler() mungkin melihat form itu lagi.

Langkah 5: halaman yang mengirim form dari callback

Beberapa situs sama sekali tidak memiliki submit biasa. Widget-nya membawa atribut data-callback, atau halaman memberikan callback ke grecaptcha.render, dan fungsi itulah yang mengirim form begitu reCAPTCHA menyerahkan token kepadanya. Widget invisible hampir selalu bekerja dengan cara ini. Di halaman seperti itu, token di textarea tidak berpengaruh apa pun sampai callback berjalan, jadi panggil sendiri callback itu. Tulis textarea lebih dulu, karena callback biasanya mengirim form dan server membaca token dari field itu. Skrip di bawah melakukan keduanya, dan di halaman dengan beberapa widget, skrip ini memanggil callback pertama yang ditemukannya:

// Writes the token into every reCAPTCHA textarea, then calls the widget's
// callback if it has one. ___grecaptcha_cfg is undocumented, so the walk
// looks for the object holding the sitekey instead of fixed key names.
const SUBMIT_TOKEN_JS = <<<'JS'
var token = arguments[0], found = null, seen = [];
document.querySelectorAll('textarea[name="g-recaptcha-response"]')
  .forEach(function (t) { t.value = token; });
function walk(o, depth) {
  if (found || !o || typeof o !== 'object' || depth > 6 || seen.indexOf(o) !== -1) return;
  if (o instanceof Node || o === window) return;
  seen.push(o);
  if (Object.prototype.hasOwnProperty.call(o, 'sitekey') && o.callback) { found = o.callback; return; }
  for (var k in o) {
    if (Object.prototype.hasOwnProperty.call(o, k)) { try { walk(o[k], depth + 1); } catch (e) {} }
  }
}
if (window.___grecaptcha_cfg) {
  Object.keys(___grecaptcha_cfg.clients).forEach(function (id) { walk(___grecaptcha_cfg.clients[id], 0); });
}
if (typeof found === 'string') found = window[found];
if (typeof found !== 'function') return 'no callback';
found(token);
return 'callback called';
JS;

echo $client->executeScript(SUBMIT_TOKEN_JS, [$token]), PHP_EOL;

reCAPTCHA menyimpan pengaturan setiap widget di objek global bernama ___grecaptcha_cfg. Atribut data-callback tersimpan di sana sebagai nama fungsi, sedangkan callback yang diberikan ke grecaptcha.render tersimpan di sana sebagai fungsi itu sendiri, jadi skrip ini menangani keduanya. Objek ini tidak didokumentasikan dan nama key di dalamnya di-minify, itulah sebabnya skrip mencari pengaturan tersebut alih-alih mengikuti path yang tetap. Jika Google suatu saat mengubah strukturnya, skrip tidak mengembalikan callback, dan di halaman data-callback Anda tetap bisa membaca atribut itu dan memanggil fungsi dengan nama tersebut. Memanggil callback tanpa menulis textarea adalah satu-satunya kesalahan yang harus dihindari: form terkirim dengan g-recaptcha-response yang kosong.

Contoh lengkap yang berfungsi

Berikut seluruh proses CAPTCHA Symfony Panther dalam satu skrip. Skrip ini membaca key dari kedua jenis halaman, memecahkan CAPTCHA, menulis token, lalu membiarkan callback mengirim form jika ada callback, atau mengklik tombol jika tidak ada.

<?php
// composer require symfony/panther capskip/capskip
// signup.php: run from the project folder with php signup.php
require __DIR__ . '/vendor/autoload.php';

use CapSkip\CapSkip;
use CapSkip\Exceptions\CapSkipError;
use CapSkip\Exceptions\ValidationException;
use Symfony\Component\Panther\Client;

const PAGE_URL = 'https://example.com/signup';

// Writes the token into every reCAPTCHA textarea, then calls the widget's
// callback if it has one. ___grecaptcha_cfg is undocumented, so the walk
// looks for the object holding the sitekey instead of fixed key names.
const SUBMIT_TOKEN_JS = <<<'JS'
var token = arguments[0], found = null, seen = [];
document.querySelectorAll('textarea[name="g-recaptcha-response"]')
  .forEach(function (t) { t.value = token; });
function walk(o, depth) {
  if (found || !o || typeof o !== 'object' || depth > 6 || seen.indexOf(o) !== -1) return;
  if (o instanceof Node || o === window) return;
  seen.push(o);
  if (Object.prototype.hasOwnProperty.call(o, 'sitekey') && o.callback) { found = o.callback; return; }
  for (var k in o) {
    if (Object.prototype.hasOwnProperty.call(o, k)) { try { walk(o[k], depth + 1); } catch (e) {} }
  }
}
if (window.___grecaptcha_cfg) {
  Object.keys(___grecaptcha_cfg.clients).forEach(function (id) { walk(___grecaptcha_cfg.clients[id], 0); });
}
if (typeof found === 'string') found = window[found];
if (typeof found !== 'function') return 'no callback';
found(token);
return 'callback called';
JS;

$solver = new CapSkip([
    'apiKey' => getenv('CAPSKIP_API_KEY') ?: 'capskip',
    'host' => getenv('CAPSKIP_HOST') ?: '127.0.0.1',
    'port' => (int) (getenv('CAPSKIP_PORT') ?: 8080),
]);
// The full path: on Windows, Panther finds drivers\chromedriver.exe on
// its own but then fails to start it from the relative path.
$client = Client::createChromeClient(__DIR__ . '/drivers/chromedriver.exe');

try {
    $client->request('GET', PAGE_URL);
    // api.js adds the hidden textarea once the widget has rendered.
    $client->waitFor('#g-recaptcha-response', 20);
    $crawler = $client->getCrawler();

    $box = $crawler->filter('[data-sitekey]');
    if ($box->count() > 0) {
        $sitekey = $box->attr('data-sitekey');
    } else {
        // grecaptcha.render() pages: the key is the k= parameter of the widget iframe.
        $src = $crawler->filter('iframe[title="reCAPTCHA"]')->attr('src');
        parse_str((string) parse_url($src, PHP_URL_QUERY), $query);
        $sitekey = $query['k'] ?? '';
    }

    // Visible fields take typing as usual.
    $form = $crawler->selectButton('Sign up')->form();
    $form['email'] = 'YOUR_EMAIL';

    $token = $solver->recaptcha($sitekey, $client->getCurrentURL())['code'];

    if ($client->executeScript(SUBMIT_TOKEN_JS, [$token]) !== 'callback called') {
        // No callback: click the real button, as a visitor would.
        $client->submit($form);
    }
    // Wait for something only the next page has; getCrawler() can lag.
    $client->waitFor('#welcome', 15);
    echo 'Submitted: ', $client->getTitle(), PHP_EOL;
} catch (ValidationException $e) {
    echo 'Not sent: ', $e->getMessage(), PHP_EOL;
} catch (CapSkipError $e) {
    echo 'Solve failed: ', get_class($e), ': ', $e->getMessage(), PHP_EOL;
} finally {
    $client->quit();
}

Ubah label tombol, field email, dan selector yang Anda tunggu agar sesuai dengan halaman Anda. Untuk widget invisible, tambahkan array opsi dari Langkah 2 ke pemanggilan recaptcha. Di halaman callback yang sama sekali tidak memiliki tombol, Langkah 1, 2, dan 5 sudah mencakup seluruh pekerjaan. Jika waitFor menyerah setelah klik, periksa form sebelum menyalahkan token: field wajib yang dibiarkan kosong menghentikan pengiriman tanpa error sama sekali. Untuk sisi SDK lebih lanjut, termasuk opsi invisible dan Enterprise, panduan memecahkan reCAPTCHA v2 di PHP membahas pemanggilan recaptcha tanpa browser.

Menjalankan Panther dan CapSkip di mesin yang berbeda

Suite Panther sering berjalan di CI runner atau mesin Linux, sedangkan CapSkip adalah aplikasi Windows. Itu tidak masalah. Alihkan CapSkip ke mode Server dan ia akan mendengarkan di alamat jaringan atau IP publik Anda, sehingga Panther di mesin mana pun dapat memanggilnya melalui API yang sama. Gunakan IP publik statis jika rutenya melewati internet, aktifkan validasi API key, dan batasi port hanya untuk alamat yang Anda harapkan dengan aturan Windows Firewall. Pemecah CAPTCHA tetap berupa mesin Windows milik Anda sendiri, dan pemecahannya tetap tanpa kuota.

SDK PHP tidak membaca variabel lingkungan dengan sendirinya. Baca CAPSKIP_HOST, CAPSKIP_PORT dan CAPSKIP_API_KEY di kode Anda dan berikan ke konstruktor, seperti pada contoh lengkap, agar skrip yang sama bisa berjalan di komputer Anda maupun di CI. Jika host-nya salah atau CapSkip tidak berjalan, pemanggilan pertama gagal dengan NetworkException yang menyatakan tidak dapat terhubung. Ini memakan waktu sekitar dua detik jika tidak ada yang mendengarkan di port itu, dan hingga 30 detik, yaitu timeout koneksi milik SDK, jika firewall memblokir koneksi.

Kesalahan umum dan artinya

Apa yang Anda lihatPenyebabPerbaiki
ElementNotInteractableException: element not interactablePanther mencoba mengetik token ke textarea g-recaptcha-response yang tersembunyiTulis nilainya dengan executeScript, seperti di Langkah 3
Could not start chrome, dengan pesan cmd.exe bahwa sebuah perintah tidak dikenaliWindows: Panther menemukan chromedriver di folder drivers, tetapi tidak bisa menjalankannya dari path relatifBerikan path lengkap driver ke createChromeClient, atau tambahkan folder drivers ke PATH
Could not start chrome, dengan pesan bahwa sistem tidak dapat menemukan pathTidak ada chromedriver.exe di path yang diberikan ke createChromeClientJalankan bdi detect drivers di folder proyek, atau perbaiki path-nya
SessionNotCreatedException: This version of ChromeDriver only supports Chrome version NChrome memperbarui dirinya sendiri, tetapi driver tidakJalankan bdi detect drivers lagi
RuntimeException: The port 9515 is already in use.Klien atau skrip Panther lain sudah memakai port default chromedriverBeri setiap klien port tersendiri di array opsi createChromeClient
Token sudah ditulis, tetapi tidak terjadi apa-apaHalaman mengirim form dari callback reCAPTCHAJalankan skrip dari Langkah 5, yang menulis field dan memanggil callback
waitFor mengalami timeout setelah klik dan tidak ada yang terkirimAda field wajib yang kosong, atau sebuah nilai gagal dalam validasi fieldIsi setiap field wajib dengan nilai yang valid sebelum Anda mengirim
NetworkException: Failed to connect to 127.0.0.1 port 8080CapSkip tidak berjalan, atau host dan port-nya salahJalankan CapSkip, lalu periksa apakah ia harus berada di mode Local atau mode Server
Maximum execution time exceeded saat pemecahan berlangsungPanther berjalan di dalam permintaan web, dan di Windows batas itu ikut menghitung waktu yang dihabiskan untuk menungguJalankan Panther dari baris perintah, tempat batas itu tidak berlaku, atau dari queue worker

FAQ

Apakah Chrome headless membuat CAPTCHA lebih sulit dilewati?

Tidak untuk pendekatan ini. Challenge dipecahkan di browser milik CapSkip sendiri, bukan di Chrome yang dikendalikan Panther, jadi browser Panther tidak pernah harus melewatinya. Panther hanya membawa token yang sudah jadi ke dalam form. Untuk menonton prosesnya, setel variabel lingkungan PANTHER_NO_HEADLESS ke 1 sebelum Anda menjalankan PHP. Panther membacanya dari $_SERVER, jadi pemanggilan putenv() di dalam skrip tidak berpengaruh.

Haruskah pengujian Panther untuk aplikasi saya sendiri memakai pemecah CAPTCHA?

Biasanya tidak. Untuk aplikasi yang Anda kendalikan, konfigurasikan lingkungan pengujian dengan kunci uji reCAPTCHA resmi dari Google, yang selalu lolos verifikasi, sehingga pengujian sama sekali tidak memerlukan pemecahan. Pemecah CAPTCHA baru berguna ketika Panther dipakai pada halaman yang tidak Anda kendalikan, atau pada situs staging yang harus tetap memakai kunci aslinya.

Bisakah saya menjalankan beberapa browser Panther sekaligus?

Bisa, satu per proses atau beberapa dalam satu proses, asalkan setiap klien mendapat port chromedriver sendiri. Setiap klien menjalankan chromedriver-nya sendiri di port 9515 kecuali Anda memberikan port lain di opsi createChromeClient, dan klien kedua di port yang sama gagal pada permintaan pertamanya. SDK PHP bersifat sinkron, jadi setiap proses memecahkan satu CAPTCHA pada satu waktu, dan CapSkip sendiri menjalankan 10 pemecahan reCAPTCHA sekaligus secara default.

Apakah pendekatan yang sama berlaku untuk Cloudflare Turnstile?

Polanya sama. Turnstile menaruh tokennya di input tersembunyi bernama cf-turnstile-response. Panther juga tidak bisa mengisinya: objek Form melaporkan status elemen yang tidak valid, sendKeys melaporkan bahwa elemen tidak bisa diinteraksikan, sedangkan executeScript berhasil menyetelnya. Pecahkan dengan metode turnstile milik SDK, bukan recaptcha, dan baca sitekey dari atribut data-sitekey pada div cf-turnstile. Skrip Langkah 5 hanya menangani callback reCAPTCHA.

Versi singkatnya

Untuk CAPTCHA Symfony Panther, berikan path lengkap driver ke createChromeClient di Windows, muat halaman dan tunggu textarea g-recaptcha-response, lalu baca sitekey dari data-sitekey atau iframe widget. Pecahkan dengan SDK PHP menggunakan URL saat ini dari Panther. Tulis token dengan executeScript, jangan pernah dengan objek Form, lalu klik tombol yang sebenarnya, atau panggil callback di halaman yang memilikinya. Tunggu halaman berikutnya sebelum membacanya, dan alihkan CapSkip ke mode Server jika Panther berjalan di tempat lain.

Scraper Panther yang menemui widget di setiap halaman tidak dikenai biaya tambahan apa pun ketika alat bypass captcha berjalan di mesin Windows Anda sendiri. Tidak ada alasan untuk melewatkan sebuah halaman hanya karena ada challenge di dalamnya.