Symfony Panther’da reCAPTCHA Nasıl Çözülür (PHP SDK)

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

Symfony Panther’da CAPTCHA çözmek için sitekey’i Panther’ın açık tuttuğu sayfadan okuyun ve geçerli URL ile birlikte CapSkip PHP SDK’sının recaptcha metoduna verin. Ardından token’ı executeScript ile gizli g-recaptcha-response textarea’sına yazın ve formun kendi düğmesine tıklayarak gönderin. Panther kodu genellikle bu yazma adımında bozulur. reCAPTCHA bu textarea’yı display:none durumunda tutar; bu yüzden onu doldurabilecek her Panther yardımcısı, Form nesnesi de sendKeys de, ElementNotInteractableException fırlatır. Bir reCAPTCHA callback’inden gönderim yapan sayfalar bir adım daha gerektirir, çünkü callback çalışana kadar alanı yok sayarlar. Bu rehber Windows’taki kurulumu, çözümü, yazmayı, iki gönderim türünü ve çalıştırmayı güvenilir kılan beklemeleri ele alıyor.

Neye ihtiyacınız var

  • Bir Windows makinesinde çalışan CapSkip. Local modunda 127.0.0.1 üzerinden yalnızca o cihaza yanıt verir; Server modunda ise ağ adresinizi veya genel IP’nizi dinler, böylece başka bir makinedeki Panther onu API üzerinden çağırabilir. İkisi de şurada ayarlanır: bağlantı ayarları.
  • php.ini dosyasında curl ve zip eklentileri açık olan PHP 8.1 veya üzeri. Panther 2.4 için PHP 8.1 gerekir, Composer zip olmadan php-webdriver’ı kurmaz ve PHP’nin Windows derlemelerinde iki eklenti de kapalı gelir.
  • Chrome ve onunla aynı ana sürüme sahip bir chromedriver. Aşağıdaki bdi aracı doğru olanı indirir.
  • Onay kutulu ya da invisible bir reCAPTCHA v2 widget’ı olan bir sayfa.
# 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

Son satır komutun Windows biçimidir; Linux veya macOS’ta vendor/bin/bdi olur. Komut chromedriver.exe dosyasını projenizin içindeki bir drivers klasörüne kaydeder. Panther bu klasöre kendiliğinden bakar, ama Windows’ta bu arama bir tuzaktır. Dosyayı bulur, ardından onu göreli yoldan başlatamaz. Hata "Could not start chrome" şeklindedir ve ardından bir komutun tanınmadığını söyleyen bir cmd.exe mesajı gelir. Buradaki her örnekte olduğu gibi driver’ın tam yolunu createChromeClient’a verin ya da drivers klasörünü PATH’e ekleyin.

1. Adım: sayfayı açın ve sitekey’i okuyun

Panther, chromedriver aracılığıyla headless bir Chrome başlatır ve sayfayı yükler. Ardından reCAPTCHA’nın api.js dosyası widget’ı oluşturur ve id’si g-recaptcha-response olan bir textarea ekler; bu yüzden bu öğeyi beklemek, widget’ın hazır olduğunun güvenilir işaretidir. Öğenin görünür olmasını beklemeyin: hiçbir zaman görünür olmaz ve bu öğe üzerinde çağrılan waitForVisibility zaman aşımına uğrar.

<?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');

Çoğu sayfa widget’ı data-sitekey özniteliği taşıyan bir div ile tanımlar. Widget’ı kendi script’inden grecaptcha.render ile oluşturan bir sayfada böyle bir öznitelik yoktur, ama anahtar yine de sayfadadır: widget’ın iframe’inin adresinde. Bu sayfalarda 1. adımın son satırı yerine şu bloğu kullanın:

// 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'] ?? '';

2. Adım: PHP SDK ile çözün

CapSkip PHP SDK’sı, sitekey’e ve widget’ın çalıştığı sayfanın URL’sine ihtiyaç duyar. URL’yi kendi değişkeninizden değil, Panther’dan alın: bir yönlendirmeden sonra token’ın üretilmesi gereken sayfa, Panther’ın açık tuttuğu sayfadır.

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

Invisible bir widget iki şekilde karşınıza çıkar: data-size özniteliği invisible olarak ayarlanmış bir div ya da gönder düğmesinin kendisinde bir data-sitekey. Her iki durumda da üçüncü argüman olarak invisible değeri 1 olan bir seçenekler dizisi verin. Google’ın herkese açık test sitekey’inin gerçek bir CapSkip üzerinden yapılan deneme çözümü yaklaşık 13 saniye sürdü ve 1.678 karakterlik bir token döndürdü.

PHP SDK’sı senkron çalışır, bu yüzden betik token geri gelene kadar bloke olur ve bunda bir sakınca yoktur. Panther’ın tarayıcısı sayfada öylece bekler, çünkü php-webdriver’ın zaman aşımları komutlar arasındaki boşluklara değil, her bir komuta uygulanır. 103 saniyelik bir çözüm boyunca açık tutulan bir oturum, sonrasında token’ı yine normal şekilde enjekte edip formu gönderdi. SDK, varsayılanı 300 saniye olan recaptchaTimeout süresine kadar yoklama yapar; seçenekler dizisindeki bir timeout anahtarı bunu tek bir çağrı için değiştirir. Asıl süresi dolan şey token’dır. Bir reCAPTCHA token’ı verildikten sonra yaklaşık iki dakika geçerlidir, bu yüzden onu hemen yazın ve gönderin.

3. Adım: token’ı executeScript ile yazın

Bir Symfony Panther CAPTCHA çalıştırmasının işe yarayıp yaramayacağına karar veren adım budur. Akla ilk gelen hamle alanı Panther’ın Form nesnesi üzerinden ayarlamaktır ve bu başarısız olur. Panther alanları bir kullanıcının yapacağı gibi doldurur: WebDriver aracılığıyla önce temizler, sonra içine yazar. WebDriver ise kimsenin göremediği bir öğeye yazmayı reddeder. Alanı Form üzerinde ayarlamak, setValues çağırmak, alanı submit’e vermek ya da öğe üzerinde sendKeys çağırmak; hepsi aynı ElementNotInteractableException ile sonuçlanır. Bunun yerine değeri sayfanın içinde ayarlayın:

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

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

executeScript, PHP dizisinin her öğesini script’e arguments[0], arguments[1] vb. olarak iletir; bu yüzden token’ı hiçbir zaman kaçış karakterleriyle JavaScript dizesinin içine gömmeniz gerekmez. E-posta adresi gibi görünür alanlar ise yine Panther’ın normal form API’siyle doldurulur.

4. Adım: gerçek düğmeye tıklayarak gönderin

Nasıl gönderdiğiniz, sayfanın ne gördüğünü değiştirir. Formu düğmesinden oluşturun; Panther da onu bir ziyaretçinin yaptığı gibi o düğmeye tıklayarak gönderir:

// 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;

Diğer gönderme yolları bazı şeyleri atlar; bu da kendi script’i gönderim sırasında bir alan ekleyen ya da formu kontrol eden sayfalarda önem taşır:

Formun gönderilme şekliSayfanın submit işleyicileri çalışırHTML5 doğrulamasıDüğme adı ve değeri gönderilir
Düğmesinden oluşturulan bir form üzerinde Panther submitEvetEvetEvet
Form öğesinden oluşturulan bir form üzerinde Panther submitEvetHayırHayır
executeScript ile çalıştırılan form.submit()HayırHayırHayır

Doğrulamanın iki yüzü vardır. Boş bırakılmış zorunlu bir alan ya da bir e-posta alanının kabul etmeyeceği bir değer, PHP’de hiçbir hata vermeden tıklamayı durdurur ve hiçbir şey gönderilmez. Yukarıdaki bloğun yalnızca bir sonraki sayfada bulunan bir öğeyi beklemesinin nedenlerinden biri budur. Diğer neden ise Panther’ın crawler nesnesinin bir gönderimden sonra kısa bir süre daha eski sayfayı tarif edebilmesidir; bu yüzden getCrawler() sonucunu hemen okuyan kod formu yeniden görebilir.

5. Adım: callback’ten gönderim yapan sayfalar

Bazı sitelerde sıradan bir gönderim hiç yoktur. Widget bir data-callback özniteliği taşır ya da sayfa grecaptcha.render’a bir callback verir ve reCAPTCHA bu fonksiyona bir token verdiğinde fonksiyon formu gönderir. Invisible widget’lar neredeyse her zaman bu şekilde çalışır. Bu sayfalarda textarea’daki bir token, callback çalışana kadar hiçbir işe yaramaz; bu yüzden callback’i kendiniz çağırın. Önce textarea’yı doldurun, çünkü callback genellikle formu gönderir ve sunucu token’ı o alandan okur. Aşağıdaki script ikisini de yapar ve birden fazla widget bulunan bir sayfada bulduğu ilk callback’i çağırır:

// 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 her widget’ın ayarlarını ___grecaptcha_cfg adlı global bir nesnede tutar. Bir data-callback özniteliği orada fonksiyonun adı olarak, grecaptcha.render’a verilen bir callback ise fonksiyonun kendisi olarak yer alır; script ikisini de ele alır. Nesne belgelenmemiştir ve iç anahtar adları küçültülmüştür (minified); script’in sabit bir yolu izlemek yerine ayarları aramasının nedeni budur. Google bu yapıyı bir gün değiştirirse script hiçbir callback döndürmez; data-callback kullanan bir sayfada yine de özniteliği okuyup fonksiyonu o adla çağırabilirsiniz. Kaçınılması gereken tek hata, textarea’yı doldurmadan callback’i çağırmaktır: form boş bir g-recaptcha-response ile gider.

Tam çalışan örnek

İşte Symfony Panther CAPTCHA akışının tamamı tek bir betikte. Anahtarı her iki sayfa türünden de okur, çözer, token’ı yazar ve callback varsa gönderimi callback’e bırakır, yoksa düğmeye tıklar.

<?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();
}

Düğme etiketini, e-posta alanını ve beklediğiniz seçiciyi sayfanıza göre değiştirin. Invisible bir widget için 2. adımdaki seçenekler dizisini recaptcha çağrısına ekleyin. Hiç düğmesi olmayan bir callback sayfasında 1., 2. ve 5. adımlar işin tamamıdır. waitFor tıklamadan sonra pes ederse token’ı suçlamadan önce forma bakın: boş bırakılmış zorunlu bir alan, gönderimi hiçbir hata vermeden durdurur. Invisible ve Enterprise seçenekleri dahil SDK tarafının ayrıntılarını, recaptcha çağrısını tarayıcı olmadan ele alan PHP’de reCAPTCHA v2 çözme rehberinde bulabilirsiniz.

Panther ve CapSkip’i farklı makinelerde çalıştırmak

Panther test paketleri çoğu zaman bir CI runner’da ya da bir Linux makinesinde çalışır, CapSkip ise bir Windows uygulamasıdır. Bu düzen sorunsuz çalışır. CapSkip’i Server moduna alın; o zaman ağ adresinizi veya genel IP’nizi dinler, böylece herhangi bir makinedeki Panther onu aynı API üzerinden çağırabilir. Rota internetten geçiyorsa statik bir genel IP kullanın, API anahtarı doğrulamasını açın ve bir Windows Firewall kuralıyla portu yalnızca beklediğiniz adreslerle sınırlayın. Çözücü hâlâ sizin kendi Windows makinenizdir ve çözüm başına ücretlendirme hâlâ yoktur.

PHP SDK’sı ortam değişkenlerini kendiliğinden okumaz. Tam örnekte olduğu gibi CAPSKIP_HOST, CAPSKIP_PORT ve CAPSKIP_API_KEY değerlerini kodunuzda okuyup constructor’a verin; böylece aynı betik hem masanızda hem de CI’da çalışır. Host yanlışsa ya da CapSkip çalışmıyorsa ilk çağrı, bağlanamadığını söyleyen bir NetworkException ile başarısız olur. Portu dinleyen hiçbir şey yoksa bu yaklaşık iki saniye, bir güvenlik duvarı bağlantıyı düşürüyorsa SDK’nın bağlantı zaman aşımı olan 30 saniyeye kadar sürer.

Sık görülen hatalar ve anlamları

GördüğünüzNedenDüzeltme
ElementNotInteractableException: element not interactablePanther, token’ı gizli g-recaptcha-response textarea’sına yazmaya çalıştıDeğeri 3. adımdaki gibi executeScript ile yazın
Could not start chrome hatası ve bir komutun tanınmadığını söyleyen bir cmd.exe mesajıWindows: Panther, chromedriver’ı drivers klasöründe buldu ama onu göreli yoldan başlatamadıDriver’ın tam yolunu createChromeClient’a verin ya da drivers klasörünü PATH’e ekleyin
Could not start chrome hatası ve sistemin yolu bulamadığını söyleyen bir mesajcreateChromeClient’a verilen yolda chromedriver.exe yokProje klasöründe bdi detect drivers komutunu çalıştırın ya da yolu düzeltin
SessionNotCreatedException: This version of ChromeDriver only supports Chrome version NChrome kendini güncelledi, driver ise güncellenmedibdi detect drivers komutunu yeniden çalıştırın
RuntimeException: The port 9515 is already in use.Başka bir Panther istemcisi ya da betiği chromedriver’ın varsayılan portunu zaten kullanıyorcreateChromeClient’ın seçenekler dizisinde her istemciye kendi portunu verin
Token yazıldı ama hiçbir şey olmuyorSayfa gönderimi bir reCAPTCHA callback’inden yapıyorAlanı yazan ve callback’i çağıran 5. adımdaki script’i çalıştırın
waitFor tıklamadan sonra zaman aşımına uğruyor ve hiçbir şey gönderilmediZorunlu bir alan boştu ya da bir değer alanın doğrulamasından geçemediGöndermeden önce her zorunlu alanı geçerli bir değerle doldurun
NetworkException: Failed to connect to 127.0.0.1 port 8080CapSkip çalışmıyor ya da host ve port yanlışCapSkip’i başlatın, ardından Local modunda mı yoksa Server modunda mı olması gerektiğini kontrol edin
Bir çözüm sırasında Maximum execution time exceeded hatasıPanther bir web isteğinin içinde çalışıyor ve Windows’ta bu sınır beklemede geçen süreyi de sayıyorPanther’ı sınırın kapalı olduğu komut satırından ya da bir kuyruk worker’ından çalıştırın

FAQ

Headless Chrome, CAPTCHA’yı geçmeyi zorlaştırır mı?

Bu yaklaşımda hayır. Challenge, Panther’ın yönettiği Chrome’da değil, CapSkip’in kendi tarayıcısında çözülür; bu yüzden Panther’ın tarayıcısının onu hiçbir zaman geçmesi gerekmez. Panther yalnızca hazır token’ı forma taşır. Çalıştırmayı izlemek için PHP’yi başlatmadan önce ortamda PANTHER_NO_HEADLESS değerini 1 yapın. Panther bu değeri $_SERVER üzerinden okur, bu yüzden betiğin içindeki bir putenv() çağrısının hiçbir etkisi yoktur.

Kendi uygulamamın Panther testleri bir çözücü kullanmalı mı?

Genellikle hayır. Kontrol ettiğiniz bir uygulama için test ortamını Google’ın yayımladığı reCAPTCHA test anahtarları ile yapılandırın. Bu anahtarlar doğrulamadan her zaman geçer, dolayısıyla testlerin hiçbir çözüme ihtiyacı olmaz. Bir çözücü, Panther’ın kontrolünüzde olmayan sayfaları ya da gerçek anahtarlarını korumak zorunda olan bir staging sitesini yönettiği durumlarda yerini hak eder.

Aynı anda birkaç Panther tarayıcısı çalıştırabilir miyim?

Evet; her istemci kendi chromedriver portunu aldığı sürece, süreç başına bir tane ya da tek bir süreçte birkaç tane. createChromeClient seçeneklerinde farklı bir port vermediğiniz sürece her istemci kendi chromedriver’ını 9515 portunda başlatır ve aynı porttaki ikinci istemci ilk isteğinde başarısız olur. PHP SDK’sı senkron olduğu için her süreç aynı anda bir CAPTCHA çözer; CapSkip’in kendisi ise varsayılan olarak aynı anda 10 reCAPTCHA çözümü çalıştırır.

Aynı yaklaşım Cloudflare Turnstile için de işe yarar mı?

Yapı aynıdır. Turnstile kendi token’ını cf-turnstile-response adlı gizli bir input alanına koyar. Panther onu da dolduramaz: Form nesnesi geçersiz bir öğe durumu (invalid element state) bildirir, sendKeys öğenin etkileşime açık olmadığını bildirir, executeScript ise değeri ayarlar. Onu recaptcha yerine SDK’nın turnstile metoduyla çözün ve sitekey’i cf-turnstile div’inin data-sitekey özniteliğinden okuyun. 5. adımdaki script yalnızca reCAPTCHA callback’lerini ele alır.

Kısa özet

Symfony Panther CAPTCHA çözümü için Windows’ta driver’ın tam yolunu createChromeClient’a verin, sayfayı yükleyin ve g-recaptcha-response textarea’sını bekleyin, ardından sitekey’i data-sitekey özniteliğinden ya da widget iframe’inden okuyun. Panther’ın geçerli URL’sini kullanarak PHP SDK ile çözün. Token’ı asla Form nesnesiyle değil, executeScript ile yazın, ardından gerçek düğmeye tıklayın ya da callback’i olan sayfalarda callback’i çağırın. Bir sonraki sayfayı okumadan önce onu bekleyin ve Panther başka bir yerde çalışıyorsa CapSkip’i Server moduna alın.

Her sayfada bir widget ile karşılaşan bir Panther kazıyıcısı, captcha atlatma aracı kendi Windows makinenizde çalıştığında fazladan hiçbir şey ödemez. Üzerinde bir challenge var diye bir sayfayı atlamanız için hiçbir neden yok.