PHP’de Guzzle ve Cookie Jar ile CaptchaFox Nasıl Çözülür

solve captchafox in php - How to Solve CaptchaFox in PHP With Guzzle and a Cookie Jar

PHP ile CaptchaFox çözmek için sayfayı çerezleri saklayan bir Guzzle istemcisiyle çekin ve sitekey’i widget’tan okuyun. Sitekey’i sayfa URL’siyle birlikte capskip/capskip paketinin captchafox metoduna verin. Ardından token’ı cf-captcha-response alanında gönderin; Referer olarak sayfayı, user agent olarak da CapSkip’in token’ın yanında döndürdüğü değeri kullanın. Guzzle’ın varsayılan ayarları bu adımların her birinde aleyhinize çalışır. Siz istemedikçe hiçbir çerez saklamaz, Referer göndermez ve her sunucuya user agent’ının GuzzleHttp/8 olduğunu söyler. 403 aldığında yanıtı döndürmek yerine bir istisna fırlatır. CapSkip, CaptchaFox desteğini 1.4.0 sürümünde ekledi. Bu rehber, Guzzle 7 ve 8 üzerinde sayfa isteğini, çağrıyı, gönderimi ve yeniden denemeleri ele alıyor.

Neye ihtiyacınız var

  • Bir Windows makinesinde çalışan CapSkip 1.4.0 veya üzeri. CaptchaFox desteği, Friendly Captcha ve Capy Puzzle ile birlikte bu sürümle geldi.
  • Dom\HTMLDocument ve querySelector metodu için PHP 8.4 veya üzeri. PHP 8.0 ile 8.3 arasındaki sürümlerde sayfayı bunun yerine DOMDocument ve DOMXPath ile ayrıştırın; nasıl yapılacağı şurada gösteriliyor: PHP ile Friendly Captcha çözme rehberi.
  • capskip/capskip paketinin 1.3.0 veya üzeri bir sürümü (captchafox metodunu içeren ilk sürüm) ve Guzzle 7 ya da 8. Buradaki her örnek Guzzle 7.15 ve 8.2 üzerinde hiçbir değişiklik yapılmadan çalışır.
  • Widget’ı gösteren sayfanın URL’si. Sitekey, form alanları ve widget derlemesi hep bu sayfadan elde edilir.
  • Çözücü için bir adres. Local modunda CapSkip 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 bir PHP uygulaması onu API üzerinden çağırabilir. İkisi de bağlantı ayarları bölümünde yer alır; ne zaman geçiş yapmanız gerektiğini 4. adım anlatıyor.
# PHP 8.4 or later, then the SDK and Guzzle 7 or 8
composer require capskip/capskip guzzlehttp/guzzle

Betiği komut satırından çalıştırın. CapSkip yoğun olduğunda bir çözüm dakikalar sürebilir; bu da bir web isteğinin genellikle açık kaldığı süreden daha uzundur.

1. Adım: sayfayı tek bir Guzzle istemcisiyle çekin ve sitekey’i okuyun

Bir Guzzle istemcisi, cookies seçeneği true olarak ayarlanmış şekilde oluşturulmadıkça hiçbir çerez saklamaz. Bu seçenekle istemci, gönderdiği her istek için tek bir cookie jar tutar; bir yönlendirmede ayarlanan çerez de buna dahildir. Böylece CSRF korumalı bir formun arkasındaki oturum çerezi, siz gönderimi yapana kadar korunur. İstemciyi bir kez oluşturun ve iki istek için de onu kullanın:

<?php
// composer require capskip/capskip guzzlehttp/guzzle
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\TransferStats;

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

// cookies: one jar for every request, redirects included.
// http_errors: a 403 comes back as a response, not an exception.
$http = new Client(['cookies' => true, 'http_errors' => false]);

$pageUrl = PAGE_URL;
$page = $http->get(PAGE_URL, [
    // Runs for every hop, so it ends on the address the page came from.
    'on_stats' => function (TransferStats $stats) use (&$pageUrl) {
        $pageUrl = (string) $stats->getEffectiveUri();
    },
]);
// With http_errors off, a blocked or missing page arrives as a response.
if ($page->getStatusCode() !== 200) {
    throw new RuntimeException("GET $pageUrl returned " . $page->getStatusCode());
}
$html = (string) $page->getBody();

// PHP 8.4's HTML5 parser; it reads UTF-8 without a meta charset.
$doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR);

Buradaki asıl yükü iki seçenek ve bir callback taşır. http_errors false olarak ayarlandığında 403, bir ClientException olarak değil, okuyabileceğiniz bir yanıt olarak geri gelir. Kodun durum kodunu kendisinin kontrol etmesinin nedeni de budur: aksi hâlde bir engelleme sayfası ya da bir 404, formmuş gibi ayrıştırılırdı. on_stats, Guzzle’ın gönderdiği her istek için, yönlendirme adımları dahil, bir kez çalışır; böylece çağrıdan sonra sayfanın en son geldiği adresi tutar. Bu adresin CaptchaFox için neden bu kadar önemli olduğunu 2. adım açıklıyor. Dom\HTMLDocument, PHP 8.4 ile eklenen HTML5 ayrıştırıcısıdır. DOMDocument’in loadHTML metodunun aksine, meta charset içermeyen bir sayfayı UTF-8 olarak okur; böylece aksanlı metin içeren gizli bir alan sunucuya bozulmadan geri gider.

Sitekey herkese açıktır, her ziyaretçi için aynıdır ve geleneksel olarak sk_ ile başlar. Çoğu sayfa onu widget’ın kapsayıcısına koyar: captchafox sınıfına ve bir data-sitekey özniteliğine sahip bir div. Widget’ı kendi script’inden oluşturan bir sayfa ise anahtarı bunun yerine bir captchafox.render çağrısına verir. Bazı platformlar, cdn.captchafox.com adresindeki standart widget yerine s.uicdn.com/mampkg/ altından bir paket gömer. Bu siteler MAM_ ile başlayan bir token bekler ve sayfaları anahtarı o script’in kendi adresinde taşıyabilir.

// The container, matched on the whole class name.
$widget = $doc->querySelector('.captchafox');
// MAM pages load this package and may carry the key in its src.
$mamSrc = $doc->querySelector('script[src*="mampkg/@mamdev/core.frontend.libs.captchafox"]')?->getAttribute('src');

$sitekey = $widget?->getAttribute('data-sitekey');
if ($sitekey === null && $mamSrc !== null) {
    parse_str((string) parse_url($mamSrc, PHP_URL_QUERY), $query);
    $sitekey = $query['key'] ?? null;
}
if ($sitekey === null && preg_match('/captchafox\.render\([\s\S]{0,300}?["\']?sitekey["\']?\s*:\s*["\']([^"\']+)/', $html, $m)) {
    $sitekey = $m[1];
}
$form = $widget?->closest('form') ?? $doc->querySelector('form');
if ($sitekey === null || $form === null) {
    throw new RuntimeException('No sitekey or form in the HTML; find them in DevTools.');
}
echo $sitekey, $mamSrc !== null ? ' (MAM page)' : '', PHP_EOL;

querySelector sınıf adlarını bütün olarak eşleştirir, böylece captchafox-wrapper gibi bir sınıfa sahip bir sarmalayıcı yanlışlıkla seçilmez. MAM kontrolü host’a göre değil, captchafox paket yoluna göre eşleştirir, çünkü aynı CDN, aralarında bir sohbet widget’ının da bulunduğu ilgisiz paketler sunar. Dom\HTMLDocument öğelerinde getAttribute, bulunmayan bir öznitelik için null döndürür; yedek yolların birbirine zincirlenebilmesini sağlayan da budur. Paketlenmiş bir script dosyasındaki anahtar ise HTML’de hiç yer almaz. Bunun yerine DevTools’u açın: anahtar, api.captchafox.com adresine (MAM sayfalarında mam-api.captchafox.com adresine) giden bir istekte /captcha/ sonrasındaki yol parçasıdır. Kapsayıcı yoksa kod sayfadaki ilk formu alır; bu yüzden sayfada bir arama kutusu ya da ikinci bir form varsa kayıt formunu id’sine göre seçin.

2. Adım: captchafox çağrısı

PHP ile CaptchaFox çözmek için tek bir metoda ihtiyacınız var. Metot sitekey’i, sayfa URL’sini ve bir seçenekler dizisini alır; standart widget için ilk ikisi yeterlidir:

use CapSkip\CapSkip;

// Copied from the script tag on MAM pages.
const MAM_PACKAGE = 'https://s.uicdn.com/mampkg/@mamdev/core.frontend.libs.captchafox/';

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

$result = $solver->captchafox($sitekey, $pageUrl, [
    // null is dropped, so the standard widget sends nothing extra.
    'api_server' => $mamSrc !== null ? MAM_PACKAGE : null,
]);

echo substr($result['token'], 0, 24), PHP_EOL;     // goes in cf-captcha-response
echo $result['userAgent'] ?? '(none)', PHP_EOL;    // the browser that minted it

Sonuç dizisi token’ı token anahtarı altında ve bir kez daha code altında tutar; CapSkip’in bu işe verdiği id captchaId altında, çözüm bir user agent bildirdiyse o da userAgent altında bulunur. Bu user agent, sizin gönderdiğiniz herhangi bir şeye değil, CapSkip’in çözümü yaptığı tarayıcıya aittir ve 3. adım ona dayanır. Metot, değeri null olan her seçeneği herhangi bir şey göndermeden önce çıkarır; api_server satırının her çağrıda çalışabilmesinin nedeni budur.

Widget’ın gerçekten çalıştığı sayfayı gönderin. CaptchaFox her anahtarı izin verilen alan adlarından oluşan bir listeye kaydeder ve herhangi bir şey vermeden önce host’u kontrol eder; bu yüzden başka bir alan adındaki bir sayfa URL’si her denemede reddedilir. 1. adımın yönlendirmelerden sonraki adresi izlemesinin nedeni budur. Bir www host’una ya da yerelleştirilmiş bir kayıt sayfasına yönlendiren bir bağlantı, widget’ın bulunduğu yer değildir.

Metodun kabul ettiği seçenekler

api_server widget derlemesini seçer; apiServer da onun takma adı olarak çalışır. type ve uri anahtarlarına sahip bir proxy dizisi, çözümü proxy’niz üzerinden yönlendirir. useragent kabul edilir ama bilinçli olarak uygulanmaz, çünkü CapSkip kendi kimliğine sahip gerçek bir tarayıcıda çözer. Saniye cinsinden bir timeout seçeneği, yalnızca o çağrı için istemcinin recaptchaTimeout değerinin yerini alır. Yoklama aralığının üst sınırı da çağrı başına ayarlanabilir, ama yalnızca snake case yazılan polling_interval olarak. camelCase yazılan pollingInterval constructor’a aittir ve çağrı başına seçenek olarak verildiğinde ValidationException fırlatır. Boş bir sitekey veya URL, metodun tanımadığı bir seçenek ve HTTP, HTTPS, SOCKS5 ya da SOCKS5H dışında bir proxy türü de, makinenizden daha hiçbir istek çıkmadan aynı şekilde ValidationException fırlatır.

3. Adım: token’ı, onu üreten user agent ile gönderin

PHP ile CaptchaFox çözerken sitenin token’ı kabul edip etmeyeceğine karar veren adım budur. Guzzle, user agent belirtmediğiniz her istekte user agent olarak GuzzleHttp/8 gönderir; eski ana sürümde ise GuzzleHttp/7 gönderir. Bu, token’ı üreten tarayıcı değildir ve token’ı üreten tarayıcıyı bekleyen bir site gönderimi reddeder. Dönen değeri, token’ı taşıyan isteğe kopyalayın:

use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Psr7\UriResolver;

// The fields a browser would send for this form, CSRF token included.
$fields = [];
foreach ($form->querySelectorAll('input[name]:not([disabled])') as $input) {
    $type = strtolower($input->getAttribute('type') ?? 'text');
    if (in_array($type, ['submit', 'button', 'image', 'file', 'reset'], true)) continue;
    $checkable = in_array($type, ['checkbox', 'radio'], true);
    if ($checkable && !$input->hasAttribute('checked')) continue;
    // A checked box with no value attribute submits "on".
    $fields[$input->getAttribute('name')] = $input->getAttribute('value') ?? ($checkable ? 'on' : '');
}
$fields['email'] = 'YOUR_EMAIL';
$fields['cf-captcha-response'] = $result['token'];

// Browsers send the page as Referer; strict servers want one.
$headers = ['Referer' => $pageUrl];
// Without this, Guzzle announces itself as GuzzleHttp/8 (or /7).
if (isset($result['userAgent'])) {
    $headers['User-Agent'] = $result['userAgent'];
}

// Resolve the form's action against the page, as a browser would.
$action = UriResolver::resolve(new Uri($pageUrl), new Uri(trim($form->getAttribute('action') ?? '')));
$response = $http->post($action, ['form_params' => $fields, 'headers' => $headers]);
echo $response->getStatusCode(), PHP_EOL;

Döngü, bir tarayıcının bu form için göndereceklerini toplar: gizli CSRF token’ı, metin alanları ve işaretli kutular; devre dışı alanlar ve düğmeler hariç. Açılır menüleri ve çok satırlı metin alanlarını atlar; bu yüzden form bunları içeriyorsa adlarıyla kendiniz ekleyin, sunucu gönder düğmesinin adını kontrol ediyorsa onu da ekleyin. form_params alanları, bir tarayıcının bu formu gönderdiği gibi URL kodlamalı olarak gönderir. UriResolver, Guzzle’ın PSR-7 paketiyle birlikte gelir ve formun action değerini, boş bir action, yalın bir sorgu dizesi ve nokta segmentleri dahil, bir tarayıcının yaptığı gibi sayfaya göre çözümler.

Guzzle kendiliğinden bir Referer da göndermez. Django dahil bazı framework’ler, çerez ve token ne kadar doğru olursa olsun, Referer başlığı da Origin başlığı da olmayan bir HTTPS form gönderimini reddeder. Guzzle 8 ayrıca null bir başlık değerini bir InvalidArgumentException ile doğrudan reddeder; user agent’ın yalnızca çözüm bir user agent bildirdiğinde eklenmesinin nedeni budur.

İki kural daha, CaptchaFox’un token’ı site tarafında nasıl kontrol ettiğinden doğar. Her token yalnızca bir kez ve kısa bir süre için doğrulanır; bu yüzden hazır olduğunuzda çözün ve hemen gönderin. Ayrıca token’ı opak kabul edin: asla kırpmayın ya da yeniden kodlamayın. Bazı siteler token’ı kendi script’lerinden bir JSON gövdesi içinde gönderir; bu yüzden DevTools açıkken formu bir kez elle gönderin ve sayfanın gönderdiğini taklit edin. CaptchaFox’un başlangıç dokümantasyonu widget işaretlemesini ve sunucu tarafındaki kontrolü gösterir.

4. Adım: yeniden denemeler, zaman aşımları ve çözücünün nerede çalıştığı

CaptchaFox’un hangi challenge’ı çıkaracağını siz seçmezsiniz. Çözümlerin çoğunda hiçbir challenge çıkmaz; bir kaydırmalı bulmaca çıktığında ise CapSkip onu genellikle tamamlar. Görsel seçme ve ses challenge’ları nadirdir ve çözülmez: saniyeler içinde, mesajında ERROR_CAPTCHA_UNSOLVABLE geçen bir ApiException olarak geri dönerler. Yeni bir deneme genellikle başka bir challenge getirir, bu yüzden yeniden denemeye değer:

use CapSkip\Exceptions\ApiException;

function solveCaptchaFox(\CapSkip\CapSkip $solver, string $sitekey, string $url, array $options = [], int $attempts = 3): array
{
    for ($attempt = 1; ; $attempt++) {
        try {
            return $solver->captchafox($sitekey, $url, $options);
        } catch (ApiException $e) {
            // Select, audio and unreadable slide draws come back unsolvable;
            // redraw. Anything else, such as a wrong API key, is final.
            if (!str_contains($e->getMessage(), 'UNSOLVABLE') || $attempt === $attempts) {
                throw $e;
            }
        }
    }
}

İstisna mesajı CapSkip’in ham yanıtını taşır; bu yüzden kontrol, hata kodunu doğrudan bu mesajdan okur. Sitenin reddettiği bir anahtar da (örneğin yanlış alan adındaki bir anahtar) çözülemez olarak görünür ve yeniden denenir. Ancak böyle bir anahtar her denemede saniyeler içinde başarısız olur; onu ele veren işaret de budur. Diğer yol, CapSkip ayarlarının CaptchaFox satırındaki Retries ayarıdır. 0 ile 3 arasında bir değer alır ve varsayılanı 0’dır. Değeri yükseltirseniz CapSkip desteklenmeyen challenge’ları, süresi dolan denemeleri ve tarayıcı hatalarını kendi başına yeniden dener, ama reddedilen bir anahtarı asla yeniden denemez.

captchafox, istemcinin varsayılanı 300 saniye olan recaptchaTimeout değeriyle yoklama yapar, çünkü bu gerçek bir tarayıcı oturumudur. CapSkip de kendi zamanlayıcısını çalıştırır: bir görev, 10 CaptchaFox thread’inden (Max. Threads) biri için en fazla 250 saniye (Wait Timeout) bekleyebilir ve tek bir deneme 150 saniye (Row Timeout) alır. Boş bir thread varsa başarısız bir deneme, SDK’nın 300 saniyesi dolmadan çok önce size bir ApiException olarak ulaşır. Uzun bir kuyruktan sonra ise SDK önce kendi sınırına ulaşabilir ve CapSkip hâlâ çalışırken TimeoutException fırlatabilir. Bu yüzden çok sayıda çözümü kuyruğa aldığınızda ya da Retries değerini yükselttiğinizde daha uzun bir timeout verin, çünkü her yeniden deneme bir Row Timeout daha ekleyebilir.

Çok sayıda kayıt işlemi için Max. Threads değerine uyacak şekilde aynı anda en fazla on çözümü işlemde tutun ve cookie jar’lar hiç karışmasın diye her çalıştırmaya kendi Guzzle istemcisini verin. captchafox token gelene kadar bloklar; bu yüzden tek bir PHP süreci çözümleri SDK’nın send ve getResult metotlarıyla yan yana çalıştırır. Bu döngü, Friendly Captcha rehberinin 4. adımında yer alıyor. CaptchaFox için görevi method değeri captchafox olacak şekilde gönderin ve getResult metodunun döndürdüğü diziden yalnızca request alanını değil, userAgent alanını da saklayın, çünkü 3. adım gönderimi ikisiyle birlikte yapar.

Örnekler 127.0.0.1 kullanır, çünkü PHP ile CapSkip aynı Windows bilgisayarı paylaştığı sürece doğru adres budur. Betiği bir VPS’e, paylaşımlı bir hosting sunucusuna ya da Linux üzerindeki bir Laravel uygulamasına taşıdığınızda loopback o makineyi gösterir; bu yüzden ilk çağrı Failed to connect ile bir NetworkException fırlatır. CapSkip’i Server moduna alın; o zaman ağ adresinizi veya genel IP’nizi dinler, böylece bunların hepsi ona aynı API üzerinden ulaşabilir. Rota internetten geçiyorsa statik bir genel IP kullanın. CapSkip, Server modu başladığında portunu her adrese açar; bu yüzden erişimi beklediğiniz makinelerle sınırlı tutmak için diğer adreslere yönelik bir Windows Firewall engelleme kuralı ekleyin. Çözücü hâlâ sizin kendi makinenizdir ve çözüm başına ücretlendirme hâlâ yoktur. İstemci ortam değişkenlerini kendiliğinden okumaz; bu yüzden tam örnekte olduğu gibi CAPSKIP_HOST değerini getenv ile okuyun ve istemciye verin.

Tam çalışan örnek

İşte çalıştırmanın tamamı tek bir dosyada. signUp sayfayı kendi istemcisiyle çeker, anahtarı okur, yeniden denemelerle çözer ve formu çerezleriyle, bir Referer ile ve token’ı üreten user agent ile gönderir.

<?php
// composer require capskip/capskip guzzlehttp/guzzle
// signup.php: run with php signup.php (PHP 8.4 or later)
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use CapSkip\CapSkip;
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\CapSkipError;
use CapSkip\Exceptions\TimeoutException;
use CapSkip\Exceptions\ValidationException;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Psr7\UriResolver;
use GuzzleHttp\TransferStats;

const PAGE_URL = 'https://example.com/signup';
// Copied from the script tag on MAM pages.
const MAM_PACKAGE = 'https://s.uicdn.com/mampkg/@mamdev/core.frontend.libs.captchafox/';

function solveCaptchaFox(CapSkip $solver, string $sitekey, string $url, array $options = [], int $attempts = 3): array
{
    for ($attempt = 1; ; $attempt++) {
        try {
            return $solver->captchafox($sitekey, $url, $options);
        } catch (ApiException $e) {
            // Select, audio and unreadable slide draws come back unsolvable.
            if (!str_contains($e->getMessage(), 'UNSOLVABLE') || $attempt === $attempts) {
                throw $e;
            }
            echo "attempt $attempt: ", $e->getMessage(), PHP_EOL;
        }
    }
}

function signUp(CapSkip $solver, string $url, string $email): int
{
    // A new client per run, so its cookie jar holds this session only.
    $http = new Client(['cookies' => true, 'http_errors' => false]);
    $pageUrl = $url;
    $page = $http->get($url, [
        'on_stats' => function (TransferStats $stats) use (&$pageUrl) {
            $pageUrl = (string) $stats->getEffectiveUri();
        },
    ]);
    if ($page->getStatusCode() !== 200) {
        throw new RuntimeException("GET $pageUrl returned " . $page->getStatusCode());
    }
    $html = (string) $page->getBody();
    $doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR);

    $widget = $doc->querySelector('.captchafox');
    $mamSrc = $doc->querySelector('script[src*="mampkg/@mamdev/core.frontend.libs.captchafox"]')?->getAttribute('src');
    $sitekey = $widget?->getAttribute('data-sitekey');
    if ($sitekey === null && $mamSrc !== null) {
        parse_str((string) parse_url($mamSrc, PHP_URL_QUERY), $query);
        $sitekey = $query['key'] ?? null;
    }
    if ($sitekey === null && preg_match('/captchafox\.render\([\s\S]{0,300}?["\']?sitekey["\']?\s*:\s*["\']([^"\']+)/', $html, $m)) {
        $sitekey = $m[1];
    }
    $form = $widget?->closest('form') ?? $doc->querySelector('form');
    if ($sitekey === null || $form === null) {
        throw new RuntimeException('No sitekey or form in the HTML; find them in DevTools.');
    }

    $result = solveCaptchaFox($solver, $sitekey, $pageUrl, [
        'api_server' => $mamSrc !== null ? MAM_PACKAGE : null,
    ]);

    $fields = [];
    foreach ($form->querySelectorAll('input[name]:not([disabled])') as $input) {
        $type = strtolower($input->getAttribute('type') ?? 'text');
        if (in_array($type, ['submit', 'button', 'image', 'file', 'reset'], true)) continue;
        $checkable = in_array($type, ['checkbox', 'radio'], true);
        if ($checkable && !$input->hasAttribute('checked')) continue;
        $fields[$input->getAttribute('name')] = $input->getAttribute('value') ?? ($checkable ? 'on' : '');
    }
    $fields['email'] = $email;
    $fields['cf-captcha-response'] = $result['token'];

    $headers = ['Referer' => $pageUrl];
    if (isset($result['userAgent'])) {
        $headers['User-Agent'] = $result['userAgent'];
    }
    $action = UriResolver::resolve(new Uri($pageUrl), new Uri(trim($form->getAttribute('action') ?? '')));

    return $http->post($action, ['form_params' => $fields, 'headers' => $headers])->getStatusCode();
}

$solver = new CapSkip([
    'apiKey' => getenv('CAPSKIP_API_KEY') ?: 'capskip',
    'host' => getenv('CAPSKIP_HOST') ?: '127.0.0.1',
    'port' => (int) (getenv('CAPSKIP_PORT') ?: 8080),
]);

try {
    $status = signUp($solver, PAGE_URL, 'YOUR_EMAIL');
    echo $status, PHP_EOL;
    // A 4xx from the site means the post was refused.
    exit($status < 400 ? 0 : 1);
} catch (ValidationException $e) {
    fwrite(STDERR, 'not sent: ' . $e->getMessage() . PHP_EOL);
    exit(1);
} catch (TimeoutException $e) {
    fwrite(STDERR, 'gave up after recaptchaTimeout' . PHP_EOL);
    exit(1);
} catch (CapSkipError | RuntimeException $e) {
    // Guzzle's transfer errors are RuntimeExceptions too.
    fwrite(STDERR, get_class($e) . ': ' . $e->getMessage() . PHP_EOL);
    exit(1);
}

Her çalıştırma saniyeler içinde başarısız oluyorsa her şeyden önce sayfa URL’sini kontrol edin, çünkü kayıtlı alan adlarının dışında kullanılan bir anahtar her seferinde aynı şekilde başarısız olur. Sitekey kontrolü hata fırlatıyorsa sayfa widget’ı paketlenmiş bir script’ten oluşturuyordur ve anahtar, 1. adımda anlatıldığı gibi Network sekmesindedir. Bu metodun arkasındaki ham endpoint şurada belgelenmiştir: API referansının CaptchaFox bölümü.

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

GördüğünüzNedenDüzeltme
Site, CapSkip’in çözüldü olarak döndürdüğü bir token’ı reddediyorGönderim, token’ı üreten tarayıcı olarak değil, GuzzleHttp/8 veya GuzzleHttp/7 olarak gittiO istekte User-Agent başlığı olarak sonucun userAgent değerini gönderin
User agent eşleştiği hâlde reddediliyorSayfa MAM paketini yüklüyor ama çözümde standart widget kullanıldı ya da tam tersiScript etiketini okuyun ve api_server değerini ona göre ayarlayın
Token ve user agent geçerli olduğu hâlde gönderimde 403 hatasıİstemci cookies true olarak ayarlanmadan oluşturuldu ya da gönderimi ikinci bir istemci yaptı, bu yüzden CSRF alanı artık eşleşmiyor; ya da gönderimde Referer yoktuGönderimi 1. adımdaki istemciyle yapın ve sayfa URL’sini Referer olarak gönderin
Gönderimde GuzzleHttp\Exception\ClientExceptionhttp_errors varsayılan olarak açıktır, bu yüzden her 4xx yanıtı bir istisna fırlatırİstemciyi http_errors değeri false olacak şekilde oluşturun ve durum kodunu okuyun
headers.User-Agent adını içeren InvalidArgumentExceptionGuzzle 8 null bir başlık değerini reddeder ve çözüm bir user agent bildirmediisset kontrolünü koruyun; site Guzzle’ın kendi adını reddediyorsa güncel bir masaüstü tarayıcı user agent’ını kendiniz gönderin
Tek bir anahtar için her seferinde saniyeler içinde gelen bir ApiExceptionSayfa URL’si anahtarın kayıtlı olduğu alan adlarının dışında (çoğu zaman yönlendirmeden önceki adres gönderildiği için) ya da anahtarın kendisi yanlışon_stats ile kaydedilen adresi gönderin ve anahtarı yeniden kontrol edin
Ara sıra gelen, ERROR_CAPTCHA_UNSOLVABLE içeren bir ApiExceptionCaptchaFox bir görsel seçme veya ses challenge’ı ya da CapSkip’in okuyamadığı bir kaydırmalı bulmaca çıkardıKodda yeniden deneyin ya da CapSkip’in CaptchaFox ayarlarında Retries değerini yükseltin
Sayfa GET isteğinin 403 döndürdüğünü söyleyen RuntimeExceptionSite sayfa isteğini reddetti; çoğu zaman Guzzle’ın kendi user agent’ı yüzündenSayfa isteği için istemcide güncel bir masaüstü tarayıcı user agent’ı ayarlayın; gönderim yine token’ı üreten user agent’ı taşır
Daha hiçbir şey gönderilmeden ValidationExceptionBoş bir sitekey veya URL, metodun kabul etmediği bir seçenek (örneğin çağrı başına verilen pollingInterval) ya da CapSkip’in kabul etmediği bir proxy türüSitekey’in bulunduğunu kontrol edin ve mesajda adı geçen seçeneği düzeltin ya da çıkarın
Beş dakika sonra TimeoutExceptionBoş bir CaptchaFox thread’ini bekleme süresi ile çözümün kendisi toplamda SDK’nın 300 saniyesini aştı ya da CapSkip çözümün ortasında durduAynı anda en fazla on çözümü işlemde tutun ya da o çağrı için daha uzun bir timeout verin
Class "Dom\HTMLDocument" not foundPHP sürümü 8.4’ten eskiPHP’yi yükseltin ya da DOMDocument ve DOMXPath ile ayrıştırın
Fatal error: Cannot use GuzzleHttp\Exception\NetworkException as NetworkExceptionGuzzle 8 kendine ait bir NetworkException sınıfı ekledi ve ikisi de aynı kısa ad altında içe aktarıldıBunlardan birini GuzzleNetworkException gibi bir takma adla içe aktarın
NetworkException: Failed to connect to 127.0.0.1 port 8080CapSkip çalışmıyor, host ya da port yanlış veya PHP başka bir makinede çalışıyorCapSkip’i başlatın, ardından Local modunda mı yoksa Server modunda mı olması gerektiğini kontrol edin

FAQ

PHP ile CaptchaFox çözmek için tarayıcıya ihtiyacım var mı?

Hayır. Betiğinizin yalnızca sayfanın HTML’sine ihtiyacı var ve onu Guzzle çeker. Tarayıcı işi CapSkip içinde gerçekleşir: CapSkip gerçek widget’ı kendi tarayıcısında çalıştırır ve token’ı o tarayıcının user agent’ıyla birlikte geri verir. PHP kodunuz hiçbir zaman JavaScript çalıştırmaz; bu yüzden yalnızca oturumu ve başlıkları doğru şekilde taşıması gerekir.

Guzzle yerine ham cURL kullanabilir miyim?

Evet. CURLOPT_COOKIEFILE değeri boş bir dizeye ayarlanmış tek bir cURL handle’ı kullanın; bu ayar cURL’ün bellek içi çerez motorunu açar. Gönderimi de aynı handle üzerinden yapın. Ham cURL, siz ayarlamadıkça hiçbir User-Agent başlığı göndermez; bu yüzden gönderimden önce CURLOPT_USERAGENT değerini sonucun userAgent değerine, CURLOPT_REFERER değerini de sayfaya ayarlayın. PHP için Friendly Captcha rehberi bu akışın tamamını cURL ve DOMXPath ile kuruyor.

Token’ı bunun yerine Symfony Panther’dan gönderebilir miyim?

Evet, token’ı gönderen tarayıcı CapSkip’in döndürdüğü user agent ile çalıştığı sürece. Önce çözün, ardından Panther’ın Chrome’unu user agent argümanı bu değere ayarlanmış şekilde başlatın, formu yükleyin, token’ı cf-captcha-response alanına yazın ve gönderin. Betiğinizin başka hiçbir şey için tarayıcıya ihtiyacı yoksa Guzzle yolu daha hafiftir. Symfony Panther rehberi PHP’den bir sayfaya token’ın nasıl yazılacağını gösteriyor.

Bir VPS’teki PHP uygulaması, Windows bilgisayarımdaki çözücüyü kullanabilir mi?

Evet. Bağlantı ayarlarından CapSkip’i Server moduna alın; böylece loopback yerine bir ağ adresini dinler. Ardından uygulamanın çalıştığı yerde CAPSKIP_HOST değişkenini ayarlayın ve tam örnekte olduğu gibi istemciye verin. VPS, paylaşımlı hosting ya da konteyner fark etmez: hepsi aynı HTTP API üzerinden bağlanır. Rota internetten geçiyorsa statik bir genel IP kullanın ve porta ulaşmaması gereken adresler için bir Windows Firewall engelleme kuralı ekleyin. Çözücü sizin sahip olduğunuz donanımda kalır, dolayısıyla daha fazla çözüm çalıştırmak ödediğiniz tutarı asla değiştirmez.

Kısa özet

PHP ile CaptchaFox çözmek için sayfayı cookies açık ve http_errors kapalı olarak oluşturulmuş tek bir Guzzle istemcisiyle çekin ve son adresi on_stats ile kaydedin. Sitekey’i captchafox kapsayıcısından, render çağrısından ya da MAM script’inden okuyun ve sayfanın hangi derlemeyi yüklediğini not edin. captchafox metodunu sitekey ve bu adresle çağırın; api_server seçeneğini yalnızca MAM sayfaları için ekleyin. Token’ı form_params ile, Referer olarak sayfayla ve User-Agent başlığı olarak sonucun userAgent değeriyle birlikte cf-captcha-response alanında bir kez ve hemen gönderin. Çözülemeyen challenge’ları yeniden deneyin, aynı anda Max. Threads değerinden (varsayılanı 10) fazla çözümü işlemde tutmayın ve betik çözücünün makinesinden ayrıldığında Server moduna geçin.

Bu türün genellikle ihtiyaç duyduğu tek yeniden deneme, yeni bir challenge çıkarılmasıdır. Kendi Windows makinenizde çalışan bir yerel captcha çözücü ile her yeni challenge, faturalanan bir çözüm daha yerine o makinenin yalnızca birkaç saniyesine mal olur.