Cara Memecahkan CaptchaFox di PHP dengan Guzzle dan Cookie Jar

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

Untuk memecahkan CaptchaFox di PHP, ambil halaman dengan klien Guzzle yang menyimpan cookie, lalu baca sitekey dari widget. Berikan sitekey itu bersama URL halaman ke metode captchafox dari paket capskip/capskip. Kemudian kirim token di field cf-captcha-response, dengan halaman itu sebagai Referer dan dengan user agent yang dikembalikan CapSkip bersama token. Pengaturan bawaan Guzzle merugikan Anda di setiap langkah itu. Guzzle tidak menyimpan cookie kecuali Anda memintanya, tidak mengirim Referer, dan memberi tahu setiap server bahwa user agent-nya adalah GuzzleHttp/8. Saat menerima 403, Guzzle melempar exception alih-alih mengembalikan respons. CapSkip menambahkan CaptchaFox pada versi 1.4.0. Panduan ini membahas pengambilan halaman, pemanggilan, pengiriman, dan percobaan ulang, di Guzzle 7 dan 8.

Apa yang Anda butuhkan

  • CapSkip 1.4.0 atau lebih baru yang berjalan di komputer Windows. Rilis itu menambahkan CaptchaFox, bersama Friendly Captcha dan Capy Puzzle.
  • PHP 8.4 atau lebih baru, untuk Dom\HTMLDocument dan metode querySelector miliknya. Di PHP 8.0 sampai 8.3, lakukan parsing halaman dengan DOMDocument dan DOMXPath sebagai gantinya, seperti yang ditunjukkan di panduan memecahkan Friendly Captcha di PHP.
  • Paket capskip/capskip versi 1.3.0 atau lebih baru, rilis pertama yang memiliki metode captchafox, serta Guzzle 7 atau 8. Setiap contoh di sini berjalan tanpa perubahan di Guzzle 7.15 dan 8.2.
  • URL halaman yang menampilkan widget. Sitekey, field form, dan build widget semuanya diambil dari halaman itu.
  • Sebuah alamat untuk pemecah CAPTCHA. 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 aplikasi PHP di mesin lain dapat memanggilnya melalui API. Keduanya diatur di pengaturan koneksi, dan Langkah 4 membahas kapan harus beralih.
# PHP 8.4 or later, then the SDK and Guzzle 7 or 8
composer require capskip/capskip guzzlehttp/guzzle

Jalankan skrip dari command line. Satu pemecahan bisa memakan waktu beberapa menit saat CapSkip sedang sibuk, lebih lama daripada umur permintaan web pada umumnya.

Langkah 1: ambil halaman dengan satu klien Guzzle dan baca sitekey

Klien Guzzle tidak menyimpan cookie kecuali Anda membuatnya dengan opsi cookies disetel ke true. Dengan opsi itu, klien memakai satu cookie jar yang sama untuk setiap permintaan yang dikirimnya, termasuk cookie yang disetel saat redirect. Jadi cookie sesi di balik form yang dilindungi CSRF tetap bertahan sampai Anda mengirim form. Buat klien sekali saja dan gunakan untuk kedua permintaan:

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

Dua opsi dan satu callback memegang peran utama di sini. Dengan http_errors disetel ke false, 403 kembali sebagai respons yang bisa Anda baca, bukan sebagai ClientException. Itu juga sebabnya kode memeriksa status sendiri: tanpa pemeriksaan itu, halaman blokir atau 404 akan di-parse seolah-olah itu form. on_stats berjalan sekali untuk setiap permintaan yang dikirim Guzzle, termasuk setiap hop redirect, sehingga setelah pemanggilan selesai, callback itu sudah menyimpan alamat akhir tempat halaman berasal. Langkah 2 menjelaskan mengapa alamat itu sangat penting untuk CaptchaFox. Dom\HTMLDocument adalah parser HTML5 yang ditambahkan di PHP 8.4. Tidak seperti loadHTML milik DOMDocument, parser ini membaca halaman tanpa meta charset sebagai UTF-8, sehingga field tersembunyi yang berisi teks beraksen kembali ke server dalam keadaan utuh.

Sitekey bersifat publik, sama untuk setiap pengunjung, dan secara konvensi diawali sk_. Sebagian besar halaman menaruhnya di container widget, yaitu sebuah div dengan class captchafox dan atribut data-sitekey. Halaman yang membangun widget dari skripnya sendiri justru memberikan key itu ke pemanggilan captchafox.render. Beberapa platform menyematkan paket dari s.uicdn.com/mampkg/, bukan widget standar dari cdn.captchafox.com. Situs tersebut mengharapkan token yang diawali MAM_, dan halamannya bisa menyimpan key di alamat milik skrip itu sendiri.

// 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 mencocokkan nama class secara utuh, sehingga pembungkus dengan class seperti captchafox-wrapper tidak ikut terambil secara tidak sengaja. Pemeriksaan MAM mencocokkan path paket captchafox, bukan host-nya, karena CDN yang sama juga menyajikan paket lain yang tidak berhubungan, salah satunya widget chat. Pada elemen Dom\HTMLDocument, getAttribute mengembalikan null untuk atribut yang tidak ada, dan itulah yang membuat fallback bisa dirangkai. Key yang berada di dalam file skrip hasil bundle sama sekali tidak ada di HTML. Sebagai gantinya, buka DevTools: key-nya adalah segmen path setelah /captcha/ dalam permintaan ke api.captchafox.com, atau ke mam-api.captchafox.com di halaman MAM. Tanpa container, kode ini mengambil form pertama di halaman, jadi jika halaman punya kotak pencarian atau form kedua, pilih form pendaftaran berdasarkan id-nya.

Langkah 2: pemanggilan captchafox

Untuk memecahkan CaptchaFox di PHP, Anda cukup memakai satu metode. Metode ini menerima sitekey, URL halaman, dan array opsi, dan untuk widget standar, hanya dua yang pertama yang dibutuhkan:

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

Array hasil menyimpan token di bawah key token dan sekali lagi di bawah code, id yang diberikan CapSkip untuk job tersebut di bawah captchaId, serta userAgent jika pemecahan melaporkannya. User agent itu milik browser tempat CapSkip memecahkan CAPTCHA, bukan milik apa pun yang Anda kirim, dan Langkah 3 bergantung padanya. Metode ini membuang opsi apa pun yang bernilai null sebelum mengirim apa pun, itulah sebabnya baris api_server bisa dijalankan di setiap pemanggilan.

Kirim halaman tempat widget benar-benar berjalan. CaptchaFox mendaftarkan setiap key terhadap daftar domain yang diizinkan dan memeriksa host sebelum menerbitkan apa pun, sehingga URL halaman di domain lain ditolak di setiap percobaan. Itulah sebabnya Langkah 1 melacak alamat setelah redirect. Tautan yang mengalihkan ke host www atau ke halaman pendaftaran versi lokal bukanlah tempat widget berada.

Opsi yang diterima metode ini

api_server memilih build widget, dan apiServer berfungsi sebagai alias-nya. Array proxy dengan key type dan uri mengarahkan pemecahan melalui proxy Anda. useragent diterima tetapi sengaja tidak diterapkan, karena CapSkip memecahkan di browser sungguhan dengan identitasnya sendiri. Opsi timeout dalam detik menggantikan recaptchaTimeout milik klien untuk satu pemanggilan itu. Batas atas interval polling juga bisa disetel per panggilan, tetapi hanya sebagai polling_interval dalam snake case. pollingInterval dalam camelCase adalah milik konstruktor, dan sebagai opsi per panggilan, opsi itu melempar ValidationException. Hal yang sama terjadi pada sitekey atau URL yang kosong, opsi yang tidak dikenal metode ini, dan tipe proxy selain HTTP, HTTPS, SOCKS5 atau SOCKS5H, semuanya sebelum permintaan apa pun meninggalkan mesin Anda.

Langkah 3: kirim token dengan user agent yang menerbitkannya

Saat Anda memecahkan CaptchaFox di PHP, inilah langkah yang menentukan apakah situs menerima token. Guzzle mengirim GuzzleHttp/8 sebagai user agent di setiap permintaan yang tidak Anda beri user agent, atau GuzzleHttp/7 di versi mayor yang lebih lama. Itu bukan browser yang menghasilkan token, dan situs yang mengharapkan browser penerbitnya akan menolak kiriman itu. Salin nilai yang dikembalikan ke permintaan yang membawa token:

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;

Loop ini mengumpulkan apa yang akan dikirim browser untuk form ini: token CSRF tersembunyi, field teks, dan kotak yang dicentang, tanpa field yang dinonaktifkan maupun tombol. Loop ini melewati menu select dan text area, jadi tambahkan keduanya berdasarkan nama jika form memilikinya, dan tambahkan nama tombol submit jika server memeriksanya. form_params mengirim field dalam bentuk URL-encoded, seperti cara browser mengirim form ini. UriResolver disertakan dalam paket PSR-7 milik Guzzle dan me-resolve action form terhadap halaman seperti yang dilakukan browser, termasuk action kosong, action yang hanya berisi query string, dan dot segment.

Guzzle juga tidak mengirim Referer sendiri. Beberapa framework, salah satunya Django, menolak form post HTTPS yang tidak membawa header Referer maupun Origin, meskipun cookie dan token-nya sudah benar. Guzzle 8 juga langsung menolak nilai header null dengan InvalidArgumentException, itulah sebabnya user agent hanya ditambahkan jika pemecahan melaporkannya.

Dua aturan lagi berasal dari cara CaptchaFox memeriksa token di sisi situs. Setiap token hanya bisa diverifikasi sekali dan hanya dalam waktu singkat, jadi pecahkan saat Anda sudah siap, lalu langsung kirim. Selain itu, perlakukan token sebagai string opaque: jangan pernah memotong atau meng-encode ulang token. Sebagian situs mengirimnya di body JSON dari skrip mereka sendiri, jadi kirim form sekali secara manual dengan DevTools terbuka, lalu tiru apa yang dikirim halaman itu. Panduan memulai di dokumentasi CaptchaFox menunjukkan markup widget dan pemeriksaan di sisi server.

Langkah 4: percobaan ulang, timeout, dan tempat pemecah CAPTCHA berjalan

Anda tidak memilih challenge mana yang ditampilkan CaptchaFox. Sebagian besar pemecahan tidak menampilkan apa pun, dan jika puzzle geser muncul, CapSkip biasanya menyelesaikannya. Challenge pilih gambar dan audio jarang muncul dan tidak dipecahkan: keduanya kembali dalam hitungan detik sebagai ApiException yang pesannya berisi ERROR_CAPTCHA_UNSOLVABLE. Percobaan baru biasanya memunculkan challenge lain, jadi percobaan ulang itu sepadan:

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

Pesan exception berisi balasan mentah dari CapSkip, jadi pemeriksaan ini membaca kode error langsung dari pesan itu. Key yang ditolak situs, misalnya di domain yang salah, juga terbaca sebagai tidak bisa dipecahkan dan ikut dicoba ulang. Namun key itu gagal dalam hitungan detik di setiap percobaan, dan itulah tandanya. Cara lainnya adalah pengaturan Retries di baris CaptchaFox pada pengaturan CapSkip. Nilainya 0 sampai 3, dan 0 secara default. Dengan pengaturan ini, CapSkip mencoba ulang sendiri challenge yang tidak didukung, percobaan yang kehabisan waktu, dan error browser, tetapi tidak pernah mencoba ulang key yang ditolak.

captchafox melakukan polling dengan recaptchaTimeout milik klien, 300 detik secara default, karena ini adalah sesi browser sungguhan. CapSkip juga menjalankan timer-nya sendiri: sebuah tugas bisa menunggu hingga 250 detik (Wait Timeout) untuk salah satu dari 10 thread CaptchaFox (Max. Threads), dan satu percobaan mendapat 150 detik (Row Timeout). Jika ada thread yang kosong, percobaan yang gagal sampai ke Anda sebagai ApiException jauh sebelum 300 detik milik SDK habis. Setelah antrean yang panjang, SDK bisa mencapai batasnya lebih dulu dan melempar TimeoutException selagi CapSkip masih bekerja. Jadi berikan timeout yang lebih panjang saat Anda mengantrekan banyak pemecahan atau menaikkan Retries, karena setiap percobaan ulang bisa menambah satu Row Timeout lagi.

Untuk banyak pendaftaran, jaga agar paling banyak sepuluh pemecahan berjalan bersamaan agar sesuai dengan Max. Threads, dan beri setiap pendaftaran klien Guzzle-nya sendiri agar cookie jar tidak pernah tercampur. captchafox memblokir sampai token tiba, jadi satu proses PHP menjalankan beberapa pemecahan secara berdampingan melalui metode send dan getResult milik SDK. Langkah 4 di panduan Friendly Captcha memuat loop tersebut. Untuk CaptchaFox, kirim job dengan parameter method disetel ke captchafox, dan simpan field userAgent dari array yang dikembalikan getResult, bukan hanya field request-nya, karena Langkah 3 mengirim form dengan keduanya.

Contoh kode memakai 127.0.0.1 karena itu benar selama PHP dan CapSkip berada di PC Windows yang sama. Pindahkan skrip ke VPS, shared host, atau aplikasi Laravel di Linux, maka loopback menunjuk ke mesin itu sendiri, sehingga pemanggilan pertama melempar NetworkException dengan Failed to connect. Alihkan CapSkip ke mode Server dan ia akan mendengarkan di alamat jaringan atau IP publik Anda, sehingga semua lingkungan itu dapat menjangkaunya melalui API yang sama. Gunakan IP publik statis jika rutenya melewati internet. CapSkip membuka port-nya untuk semua alamat saat mode Server dimulai, jadi untuk membatasinya hanya pada mesin yang Anda harapkan, tambahkan aturan blokir Windows Firewall untuk alamat lainnya. Pemecah CAPTCHA tetap berupa mesin milik Anda sendiri, dan pemecahannya tetap tanpa kuota. Klien tidak membaca variabel lingkungan dengan sendirinya, jadi baca CAPSKIP_HOST dengan getenv lalu teruskan ke klien, seperti yang dilakukan contoh lengkap.

Contoh lengkap yang berfungsi

Berikut seluruh prosesnya dalam satu file. signUp mengambil halaman dengan kliennya sendiri, membaca key, memecahkan dengan percobaan ulang, lalu mengirim form beserta cookie-nya, Referer, dan user agent yang menerbitkan token.

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

Jika skrip selalu gagal dalam hitungan detik setiap kali dijalankan, periksa URL halaman sebelum hal lain, karena key yang dipakai di luar domain terdaftarnya gagal dengan cara yang sama setiap kali. Jika pemeriksaan sitekey melempar error, halaman membangun widget dari skrip hasil bundle, dan key-nya ada di tab Network seperti dijelaskan di Langkah 1. Endpoint mentah di balik metode ini didokumentasikan di bagian CaptchaFox pada referensi API.

Kesalahan umum dan artinya

Apa yang Anda lihatPenyebabPerbaiki
Situs menolak token yang dikembalikan CapSkip sebagai berhasil dipecahkanForm dikirim sebagai GuzzleHttp/8 atau GuzzleHttp/7, bukan sebagai browser yang menerbitkan tokenKirim userAgent dari hasil sebagai header User-Agent pada permintaan itu
Ditolak padahal user agent sudah cocokHalaman memuat paket MAM tetapi pemecahan memakai widget standar, atau sebaliknyaBaca tag script dan setel api_server agar sesuai dengannya
Respons 403 saat form dikirim, padahal token dan user agent validKlien dibuat tanpa cookies disetel ke true, atau klien kedua yang mengirim form, sehingga field CSRF tidak lagi cocok; atau kiriman tidak memiliki RefererKirim form dengan klien dari Langkah 1 dan kirim URL halaman sebagai Referer
GuzzleHttp\Exception\ClientException saat form dikirimhttp_errors aktif secara default, jadi setiap respons 4xx melempar exceptionBuat klien dengan http_errors disetel ke false dan baca kode statusnya
InvalidArgumentException yang menyebut headers.User-AgentGuzzle 8 menolak nilai header null, dan pemecahan tidak melaporkan user agentPertahankan pemeriksaan isset; jika situs menolak nama bawaan Guzzle, kirim sendiri user agent browser desktop versi terbaru
ApiException dalam hitungan detik, setiap kali, untuk satu keyURL halaman berada di luar domain tempat key didaftarkan, sering kali karena yang dikirim adalah alamat sebelum redirect, atau key itu sendiri salahKirim alamat yang dicatat on_stats, dan periksa ulang key-nya
ApiException sesekali yang berisi ERROR_CAPTCHA_UNSOLVABLECaptchaFox menampilkan challenge pilih gambar atau audio, atau puzzle geser yang tidak bisa dibaca CapSkipCoba ulang di kode, atau naikkan Retries di pengaturan CaptchaFox pada CapSkip
RuntimeException yang menyatakan GET halaman mengembalikan 403Situs menolak pengambilan halaman, sering kali karena user agent bawaan GuzzleSetel user agent browser desktop versi terbaru pada klien untuk pengambilan halaman; kiriman form tetap membawa user agent yang menerbitkan token
ValidationException sebelum apa pun terkirimSitekey atau URL kosong, opsi yang tidak diterima metode ini, seperti pollingInterval per panggilan, atau tipe proxy yang tidak diterima CapSkipPastikan sitekey ditemukan, dan perbaiki atau buang opsi yang disebut dalam pesan
TimeoutException setelah lima menitWaktu menunggu thread CaptchaFox yang kosong ditambah pemecahannya sendiri melewati 300 detik milik SDK, atau CapSkip berhenti di tengah pemecahanJaga agar paling banyak sepuluh pemecahan berjalan bersamaan, atau berikan timeout yang lebih panjang untuk pemanggilan itu
Class "Dom\HTMLDocument" not foundVersi PHP lebih lama dari 8.4Tingkatkan versi PHP, atau lakukan parsing dengan DOMDocument dan DOMXPath
Fatal error: Cannot use GuzzleHttp\Exception\NetworkException as NetworkExceptionGuzzle 8 menambahkan class NetworkException miliknya sendiri, dan keduanya diimpor dengan satu nama pendek yang samaImpor salah satunya dengan alias, misalnya GuzzleNetworkException
NetworkException: Failed to connect to 127.0.0.1 port 8080CapSkip tidak berjalan, host atau port-nya salah, atau PHP berjalan di mesin lainJalankan CapSkip, lalu periksa apakah ia harus berada di mode Local atau mode Server

FAQ

Apakah saya butuh browser untuk memecahkan CaptchaFox di PHP?

Tidak. Skrip Anda hanya membutuhkan HTML halaman, yang diambil oleh Guzzle. Pekerjaan browser terjadi di dalam CapSkip, yang menjalankan widget sungguhan di browser miliknya sendiri lalu mengembalikan token beserta user agent browser tersebut. Kode PHP Anda tidak pernah menjalankan JavaScript, jadi kode itu hanya perlu membawa sesi dan header dengan benar.

Bisakah saya memakai cURL mentah sebagai pengganti Guzzle?

Bisa. Gunakan satu handle cURL dengan CURLOPT_COOKIEFILE disetel ke string kosong, yang mengaktifkan mesin cookie in-memory milik cURL, lalu kirim form di handle yang sama. cURL mentah sama sekali tidak mengirim header User-Agent kecuali Anda menyetelnya, jadi setel CURLOPT_USERAGENT ke userAgent dari hasil sebelum mengirim form, dan CURLOPT_REFERER ke halaman itu. Panduan Friendly Captcha untuk PHP membangun seluruh alur itu dengan cURL dan DOMXPath.

Bisakah saya mengirim token dari Symfony Panther sebagai gantinya?

Bisa, asalkan browser yang mengirimnya berjalan dengan user agent yang dikembalikan CapSkip. Pecahkan dulu, lalu jalankan Chrome milik Panther dengan argumen user agent disetel ke nilai itu, muat form, tulis token ke field cf-captcha-response, lalu kirim. Jika skrip Anda tidak membutuhkan browser untuk hal lain, cara dengan Guzzle lebih ringan. Panduan Symfony Panther menunjukkan cara menulis token ke halaman dari PHP.

Bisakah aplikasi PHP di VPS memakai pemecah CAPTCHA di PC Windows saya?

Bisa. Alihkan CapSkip ke mode Server di pengaturan koneksi agar ia mendengarkan di alamat jaringan, bukan di loopback. Lalu setel CAPSKIP_HOST di tempat aplikasi berjalan dan teruskan ke klien, seperti pada contoh lengkap. VPS, shared host, dan container semuanya terhubung melalui API HTTP yang sama. Gunakan IP publik statis jika rutenya melewati internet, dan tambahkan aturan blokir Windows Firewall untuk alamat yang tidak boleh menjangkau port tersebut. Pemecah CAPTCHA tetap berada di perangkat milik Anda, jadi menjalankan lebih banyak pemecahan tidak pernah mengubah biaya yang Anda bayar.

Versi singkatnya

Untuk memecahkan CaptchaFox di PHP, ambil halaman dengan satu klien Guzzle yang dibuat dengan cookies aktif dan http_errors nonaktif, lalu catat alamat akhirnya dengan on_stats. Baca sitekey dari container captchafox, pemanggilan render, atau skrip MAM, lalu catat build mana yang dimuat halaman. Panggil captchafox dengan sitekey dan alamat itu, dan tambahkan api_server hanya untuk halaman MAM. Kirim token di cf-captcha-response dengan form_params, halaman itu sebagai Referer, dan userAgent dari hasil sebagai header User-Agent, sekali saja dan secepatnya. Coba ulang challenge yang tidak bisa dipecahkan, jangan jalankan lebih banyak pemecahan sekaligus daripada Max. Threads, yaitu 10 secara default, dan beralihlah ke mode Server begitu skrip tidak lagi berada di mesin pemecah CAPTCHA.

Challenge yang ditampilkan ulang adalah satu-satunya bentuk percobaan ulang yang biasanya dibutuhkan tipe ini. Dengan pemecah captcha lokal di mesin Windows Anda sendiri, setiap challenge baru hanya memakan beberapa detik waktu mesin itu, bukan satu pemecahan berbayar lagi.