PHP में Guzzle और cookie jar से CaptchaFox कैसे हल करें

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

PHP में CaptchaFox हल करने के लिए, पेज को ऐसे Guzzle क्लाइंट से fetch करें जो cookies रखता हो, और widget से sitekey पढ़ें। उसे पेज URL के साथ capskip/capskip पैकेज के captchafox method को पास करें। फिर token को cf-captcha-response फ़ील्ड में पोस्ट करें, Referer के रूप में पेज के साथ, और उस user agent के साथ जो CapSkip ने token के साथ लौटाया। इनमें से हर स्टेप पर Guzzle की डिफ़ॉल्ट सेटिंग्स आपके ख़िलाफ़ काम करती हैं। जब तक आप न कहें, वह कोई cookie नहीं रखता, कोई Referer नहीं भेजता, और हर सर्वर को बताता है कि उसका user agent GuzzleHttp/8 है। 403 मिलने पर वह response लौटाने के बजाय exception फेंकता है। CapSkip ने CaptchaFox को 1.4.0 में जोड़ा। यह गाइड Guzzle 7 और 8 पर fetch, कॉल, पोस्ट और दोबारा कोशिशें कवर करती है।

आपको क्या चाहिए

  • किसी Windows मशीन पर चल रहा CapSkip 1.4.0 या उसके बाद का संस्करण। CaptchaFox उसी रिलीज़ में जोड़ा गया था, Friendly Captcha और Capy Puzzle के साथ।
  • PHP 8.4 या उसके बाद का संस्करण, Dom\HTMLDocument और उसके querySelector method के लिए। PHP 8.0 से 8.3 पर इसके बजाय पेज को DOMDocument और DOMXPath से पार्स करें, जैसा यहाँ दिखाया गया है: PHP में Friendly Captcha हल करने की गाइड.
  • capskip/capskip का 1.3.0 या उसके बाद का वर्ज़न, जो captchafox method वाली पहली रिलीज़ है, और Guzzle 7 या 8। यहाँ का हर सैंपल Guzzle 7.15 और 8.2 पर बिना बदलाव के चलता है।
  • उस पेज का URL जो widget दिखाता है। sitekey, फॉर्म फ़ील्ड और widget का build, सब उसी पेज से मिलते हैं।
  • सॉल्वर के लिए एक पता। Local मोड में CapSkip केवल उसी डिवाइस के लिए 127.0.0.1 पर जवाब देता है। Server मोड में वह आपके नेटवर्क पते या पब्लिक IP पर सुनता है, ताकि किसी दूसरी मशीन पर चल रहा PHP ऐप API के ज़रिए उसे कॉल कर सके। दोनों मोड कनेक्शन सेटिंग्स में मिलते हैं, और स्विच कब करना है, यह स्टेप 4 में बताया गया है।
# PHP 8.4 or later, then the SDK and Guzzle 7 or 8
composer require capskip/capskip guzzlehttp/guzzle

script को command line से चलाएँ। CapSkip व्यस्त हो तो एक solve में कई मिनट लग सकते हैं, और इतनी देर तक कोई वेब रिक्वेस्ट आमतौर पर ज़िंदा नहीं रहती।

स्टेप 1: एक ही Guzzle क्लाइंट से पेज fetch करें और sitekey पढ़ें

Guzzle क्लाइंट तब तक कोई cookie नहीं रखता, जब तक आप उसे cookies ऑप्शन को true पर सेट करके न बनाएँ। इसके साथ, क्लाइंट अपनी भेजी हर रिक्वेस्ट के लिए एक ही cookie jar रखता है, redirect पर सेट हुई cookie समेत। इसलिए CSRF से सुरक्षित फॉर्म के पीछे वाली session cookie पोस्ट करने तक बची रहती है। क्लाइंट एक बार बनाएँ और दोनों रिक्वेस्ट के लिए उसी का इस्तेमाल करें:

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

यहाँ असली काम दो ऑप्शन और एक callback करते हैं। http_errors को false पर रखने से 403 एक ClientException के रूप में नहीं, बल्कि ऐसे response के रूप में लौटता है जिसे आप पढ़ सकते हैं। इसी वजह से कोड status खुद जाँचता है: वरना कोई block पेज या 404 ऐसे पार्स हो जाता, मानो वही फॉर्म हो। on_stats, Guzzle की भेजी हर रिक्वेस्ट के लिए एक बार चलता है, redirect के हर hop समेत, इसलिए कॉल के बाद उसमें वह पता होता है जिससे पेज आख़िरकार आया। CaptchaFox के लिए यह पता इतना मायने क्यों रखता है, यह स्टेप 2 में बताया गया है। Dom\HTMLDocument, PHP 8.4 में जोड़ा गया HTML5 parser है। DOMDocument के loadHTML के उलट, यह बिना meta charset वाले पेज को UTF-8 के रूप में पढ़ता है, इसलिए accented टेक्स्ट वाला छिपा हुआ फ़ील्ड सर्वर तक सही-सलामत वापस पहुँचता है।

sitekey सार्वजनिक होती है, हर विज़िटर के लिए एक जैसी होती है, और आमतौर पर sk_ से शुरू होती है। ज़्यादातर पेज उसे widget के container पर रखते हैं: captchafox class वाला एक div, जिस पर data-sitekey attribute होता है। जो पेज widget को अपनी script से बनाता है, वह key इसके बजाय एक captchafox.render कॉल को पास करता है। कुछ प्लेटफ़ॉर्म cdn.captchafox.com वाले standard widget के बजाय s.uicdn.com/mampkg/ से एक पैकेज एम्बेड करते हैं। वे साइटें MAM_ से शुरू होने वाले token की उम्मीद करती हैं, और उनके पेजों पर key खुद उस script के पते में हो सकती है।

// 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 पूरे class नामों से मैच करता है, इसलिए captchafox-wrapper जैसी class वाला कोई wrapper गलती से नहीं चुना जाता। MAM वाली जाँच host के बजाय captchafox पैकेज path से मिलान करती है, क्योंकि वही CDN दूसरे, असंबंधित पैकेज भी सर्व करता है, जिनमें एक chat widget भी है। Dom\HTMLDocument के elements पर, न मिलने वाले attribute के लिए getAttribute null लौटाता है, और इसी से fallbacks एक के बाद एक जुड़ पाते हैं। किसी bundled script फ़ाइल के अंदर की key HTML में होती ही नहीं। उसके लिए DevTools खोलें: api.captchafox.com को, या MAM पेजों पर mam-api.captchafox.com को गई रिक्वेस्ट में /captcha/ के बाद वाला path segment ही key है। container न हो, तो कोड पेज का पहला फॉर्म ले लेता है, इसलिए जब पेज पर कोई search box या दूसरा फॉर्म हो, तो साइन-अप फॉर्म को उसकी id से चुनें।

स्टेप 2: captchafox कॉल

PHP में CaptchaFox हल करने के लिए आपको एक ही method चाहिए। यह sitekey, पेज URL और एक options array लेता है, और standard widget के लिए उसे बस पहले दो की ज़रूरत होती है:

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 में token दो बार होता है, token के तहत और फिर code के तहत। captchaId के तहत इस job के लिए CapSkip की id होती है, और अगर solve ने कोई user agent रिपोर्ट किया हो, तो userAgent भी। वह user agent उस ब्राउज़र का है जिसमें CapSkip ने हल किया, न कि किसी ऐसी चीज़ का जो आपने भेजी हो, और स्टेप 3 उसी पर निर्भर है। कुछ भी भेजने से पहले यह method हर वह ऑप्शन हटा देता है जिसकी वैल्यू null हो, इसीलिए api_server वाली लाइन हर कॉल पर चल सकती है।

वही पेज भेजें जिस पर widget असल में चलता है। CaptchaFox हर key को अनुमत डोमेन की एक सूची के साथ रजिस्टर करता है और कुछ भी जारी करने से पहले host जाँचता है, इसलिए किसी दूसरे डोमेन का पेज URL हर कोशिश पर मना कर दिया जाता है। इसीलिए स्टेप 1 redirects के बाद वाले पते पर नज़र रखता है। जो लिंक किसी www host या स्थानीय भाषा वाले साइन-अप पेज पर redirect होता है, वह खुद वह जगह नहीं है जहाँ widget रहता है।

method जिन ऑप्शन को स्वीकार करता है

api_server, widget build चुनता है, और apiServer उसके alias के रूप में काम करता है। type और uri keys वाला एक proxy array, solve को आपकी प्रॉक्सी से होकर भेजता है। useragent स्वीकार तो किया जाता है पर जानबूझकर लागू नहीं किया जाता, क्योंकि CapSkip एक असली ब्राउज़र में उसकी अपनी पहचान के साथ हल करता है। सेकंड में दिया गया timeout ऑप्शन उस एक कॉल के लिए क्लाइंट के recaptchaTimeout की जगह ले लेता है। पोलिंग की ऊपरी सीमा भी हर कॉल पर सेट की जा सकती है, लेकिन सिर्फ़ snake case वाले polling_interval के रूप में। camelCase वाला pollingInterval constructor का ऑप्शन है, और हर कॉल के ऑप्शन के रूप में वह ValidationException फेंकता है। खाली sitekey या URL, कोई ऐसा ऑप्शन जिसे method नहीं जानता, और HTTP, HTTPS, SOCKS5 या SOCKS5H के अलावा कोई proxy type भी यही करते हैं, और यह सब कोई भी रिक्वेस्ट आपकी मशीन से निकलने से पहले ही होता है।

स्टेप 3: token को उसी user agent के साथ पोस्ट करें जिसने उसे बनाया

PHP में CaptchaFox हल करते समय यही वह स्टेप है जो तय करता है कि साइट token लेगी या नहीं। जिस भी रिक्वेस्ट को आप user agent नहीं देते, उस पर Guzzle अपना user agent GuzzleHttp/8 भेजता है, या पुराने मेजर वर्ज़न पर GuzzleHttp/7। यह वह ब्राउज़र नहीं है जिसने token बनाया, और जो साइट token बनाने वाले ब्राउज़र की उम्मीद करती है, वह पोस्ट ठुकरा देती है। लौटी हुई वैल्यू को उस रिक्वेस्ट पर कॉपी करें जो 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 वही इकट्ठा करता है जो कोई ब्राउज़र इस फॉर्म के लिए भेजता: छिपा हुआ CSRF token, टेक्स्ट फ़ील्ड और टिक किए गए box, disabled फ़ील्ड या बटनों के बिना। यह select menus और text areas छोड़ देता है, इसलिए अगर फॉर्म में वे हों, तो उन्हें नाम से जोड़ें, और अगर सर्वर submit बटन का नाम जाँचता है, तो वह भी जोड़ें। form_params फ़ील्ड को URL-encoded भेजता है, ठीक वैसे जैसे ब्राउज़र यह फॉर्म सबमिट करता है। UriResolver, Guzzle के PSR-7 पैकेज के साथ आता है और फॉर्म के action को पेज के हिसाब से वैसे ही resolve करता है जैसे ब्राउज़र करता है, खाली action, सिर्फ़ query string और dot segments समेत।

Guzzle अपनी तरफ़ से कोई Referer भी नहीं भेजता। कुछ frameworks, जिनमें Django भी है, ऐसी HTTPS फॉर्म पोस्ट ठुकरा देते हैं जिसमें न Referer header हो, न Origin header, cookie और token चाहे कितने भी सही हों। Guzzle 8, null header वैल्यू को भी सीधे InvalidArgumentException के साथ मना कर देता है, इसीलिए user agent तभी जोड़ा जाता है जब solve ने कोई user agent रिपोर्ट किया हो।

दो और नियम इस बात से निकलते हैं कि CaptchaFox साइट की तरफ़ token की जाँच कैसे करता है। हर token एक ही बार और थोड़ी ही देर के लिए verify होता है, इसलिए तभी हल करें जब आप तैयार हों, और तुरंत पोस्ट करें। और token को opaque मानें: उसे कभी न काटें, न दोबारा encode करें। कुछ साइटें उसे अपनी script से JSON बॉडी में भेजती हैं, इसलिए DevTools खोलकर फॉर्म को एक बार हाथ से सबमिट करें और पेज जो भेजता है, ठीक वैसा ही करें। CaptchaFox के getting started दस्तावेज़ widget का markup और सर्वर की तरफ़ होने वाली जाँच दिखाते हैं।

स्टेप 4: दोबारा कोशिश, timeout और सॉल्वर कहाँ चलता है

CaptchaFox कौन सा challenge दिखाएगा, यह आप नहीं चुनते। ज़्यादातर solve में कुछ भी नहीं दिखता, और जब slide puzzle आता है, तो CapSkip आमतौर पर उसे पूरा कर देता है। Image-select और ऑडियो challenge दुर्लभ हैं और हल नहीं किए जाते: वे कुछ ही सेकंड में एक ApiException के रूप में लौटते हैं, जिसके मैसेज में ERROR_CAPTCHA_UNSOLVABLE होता है। नई कोशिश में आमतौर पर कुछ और आता है, इसलिए दोबारा कोशिश करना फ़ायदेमंद है:

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

exception के मैसेज में CapSkip का raw जवाब होता है, इसलिए यह जाँच error code को सीधे उसी से पढ़ लेती है। जिस key को साइट मना कर देती है, जैसे गलत डोमेन पर, वह भी unsolvable के रूप में ही दिखती है और उसे भी दोबारा आज़माया जाता है। फिर भी वह हर कोशिश में कुछ ही सेकंड में फेल होती है, और यही उसकी पहचान है। दूसरा तरीका CapSkip की सेटिंग्स में CaptchaFox वाली पंक्ति की Retries सेटिंग है। यह 0 से 3 तक की वैल्यू लेती है और डिफ़ॉल्ट रूप से 0 होती है। तब CapSkip बिना सपोर्ट वाले challenges, समय खत्म हो चुकी कोशिशों और ब्राउज़र errors पर अपने आप दोबारा कोशिश करता है, लेकिन मना की गई key पर कभी नहीं।

captchafox क्लाइंट के recaptchaTimeout पर पोलिंग करता है, जो डिफ़ॉल्ट रूप से 300 सेकंड है, क्योंकि यह एक असली ब्राउज़र session है। CapSkip की अपनी समय सीमाएँ भी होती हैं: कोई टास्क 10 CaptchaFox threads (Max. Threads) में से किसी एक के लिए 250 सेकंड (Wait Timeout) तक इंतज़ार कर सकता है, और एक कोशिश को 150 सेकंड (Row Timeout) मिलते हैं। कोई thread खाली हो, तो फेल हुई कोशिश SDK के 300 सेकंड पूरे होने से काफ़ी पहले ही ApiException के रूप में आप तक पहुँच जाती है। लंबी queue के बाद SDK पहले अपनी सीमा तक पहुँच सकता है और TimeoutException फेंक सकता है, जबकि CapSkip अभी भी काम कर रहा होता है। इसलिए जब आप कई solve queue में डालें या Retries बढ़ाएँ, तो लंबा timeout पास करें, क्योंकि हर दोबारा कोशिश एक और Row Timeout जोड़ सकती है।

कई साइन-अप के लिए, Max. Threads से मेल खाने के लिए एक समय में ज़्यादा से ज़्यादा दस solve चालू रखें, और हर रन को उसका अपना Guzzle क्लाइंट दें, ताकि cookie jars कभी आपस में न मिलें। captchafox तब तक block करता है जब तक token न आ जाए, इसलिए एक PHP process में कई solve साथ-साथ SDK के send और getResult methods के ज़रिए चलते हैं। वह loop Friendly Captcha गाइड के स्टेप 4 में है। CaptchaFox के लिए टास्क भेजते समय method को captchafox पर सेट करें, और getResult जो array लौटाता है, उससे सिर्फ़ उसका request फ़ील्ड नहीं, userAgent फ़ील्ड भी रखें, क्योंकि स्टेप 3 दोनों के साथ पोस्ट करता है।

सैंपल 127.0.0.1 इस्तेमाल करते हैं, क्योंकि जब तक PHP और CapSkip एक ही Windows PC पर हों, यही सही है। script को किसी VPS, shared host या Linux पर चल रहे Laravel ऐप में ले जाएँ, तो loopback उसी मशीन की ओर इशारा करता है, इसलिए पहली ही कॉल Failed to connect के साथ NetworkException फेंकती है। CapSkip को Server मोड पर कर दें, तो वह आपके नेटवर्क पते या पब्लिक IP पर सुनता है, ताकि इनमें से कोई भी उसी API के ज़रिए उस तक पहुँच सके। जब रास्ता इंटरनेट से होकर जाए तो स्टैटिक पब्लिक IP इस्तेमाल करें। Server मोड शुरू होते ही CapSkip अपना पोर्ट हर पते के लिए खोल देता है, इसलिए उसे सिर्फ़ अपेक्षित मशीनों तक सीमित रखने के लिए बाकी पतों के लिए Windows Firewall में एक block नियम जोड़ें। सॉल्वर अब भी आपकी अपनी मशीन है, और solve अब भी बिना मीटर वाले हैं। क्लाइंट एनवायरनमेंट वेरिएबल खुद नहीं पढ़ता, इसलिए CAPSKIP_HOST को getenv से पढ़ें और उसे पास करें, जैसा पूरा उदाहरण करता है।

पूरा चलने वाला उदाहरण

यह रहा पूरा रन, एक ही फ़ाइल में। signUp अपने अलग क्लाइंट से पेज fetch करता है, key पढ़ता है, दोबारा कोशिशों के साथ हल करता है, और फॉर्म को उसकी cookies, एक Referer और token बनाने वाले ब्राउज़र के user agent के साथ पोस्ट करता है।

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

अगर हर रन कुछ ही सेकंड में फेल हो जाए, तो सबसे पहले पेज URL जाँचें, क्योंकि अपने रजिस्टर्ड डोमेन के बाहर इस्तेमाल हुई key हर बार एक ही तरह फेल होती है। अगर sitekey वाली जाँच error फेंकती है, तो पेज widget को किसी bundled script से बनाता है, और key Network टैब में होती है, जैसा स्टेप 1 में बताया गया है। इस method के पीछे का raw endpoint यहाँ दर्ज है: API रेफ़रेंस का CaptchaFox सेक्शन.

आम errors और उनका मतलब

आप जो देखते हैंकारणफिक्स
साइट उस token को ठुकरा देती है जिसे CapSkip ने हल हुआ बताकर लौटाया थापोस्ट GuzzleHttp/8 या GuzzleHttp/7 के रूप में गई, उस ब्राउज़र के रूप में नहीं जिसने token बनाया थाउस रिक्वेस्ट पर नतीजे के userAgent को User-Agent header के रूप में भेजें
user agent मेल खाने के बावजूद ठुकराया गयापेज MAM पैकेज लोड करता है और solve में standard widget इस्तेमाल हुआ, या इसका उल्टाscript टैग पढ़ें और api_server को उसके मुताबिक़ सेट करें
सही token और user agent के बावजूद पोस्ट पर 403क्लाइंट cookies को true किए बिना बनाया गया, या पोस्ट किसी दूसरे क्लाइंट ने की, इसलिए CSRF फ़ील्ड अब मेल नहीं खाता; या पोस्ट में Referer नहीं थास्टेप 1 वाले क्लाइंट से पोस्ट करें और पेज URL को Referer के रूप में भेजें
पोस्ट पर GuzzleHttp\Exception\ClientExceptionhttp_errors डिफ़ॉल्ट रूप से चालू रहता है, इसलिए कोई भी 4xx response exception फेंकता हैhttp_errors को false पर सेट करके क्लाइंट बनाएँ और status code पढ़ें
InvalidArgumentException, जिसमें headers.User-Agent का नाम होGuzzle 8 null header वैल्यू को मना कर देता है, और solve ने कोई user agent रिपोर्ट नहीं कियाisset वाली जाँच रहने दें; अगर साइट Guzzle के अपने नाम को मना करती है, तो किसी मौजूदा desktop ब्राउज़र का user agent खुद भेजें
एक ही key के लिए हर बार कुछ ही सेकंड में ApiExceptionपेज URL उन डोमेन से बाहर है जिनके लिए key रजिस्टर है, अक्सर इसलिए कि redirect से पहले वाला पता भेजा गया, या key ही गलत हैon_stats ने जो पता दर्ज किया, वही भेजें, और key दोबारा जाँचें
कभी-कभार आने वाला ApiException, जिसमें ERROR_CAPTCHA_UNSOLVABLE होता हैCaptchaFox ने image-select या ऑडियो challenge दिखाया, या ऐसा slide जिसे CapSkip पढ़ नहीं सकाकोड में दोबारा कोशिश करें, या CapSkip की CaptchaFox सेटिंग्स में Retries बढ़ाएँ
RuntimeException, जो बताता है कि पेज के GET ने 403 लौटायासाइट ने पेज fetch को मना कर दिया, अक्सर Guzzle के अपने user agent की वजह सेपेज fetch के लिए क्लाइंट पर किसी मौजूदा desktop ब्राउज़र का user agent सेट करें; पोस्ट फिर भी token बनाने वाले ब्राउज़र का user agent ही ले जाती है
कुछ भी भेजे जाने से पहले ही ValidationExceptionखाली sitekey या URL, कोई ऐसा ऑप्शन जो यह method नहीं लेता, जैसे हर कॉल पर दिया गया pollingInterval, या कोई ऐसा proxy type जिसे CapSkip स्वीकार नहीं करताजाँचें कि sitekey मिली थी, और संदेश में बताए गए ऑप्शन को ठीक करें या हटा दें
पाँच मिनट बाद TimeoutExceptionकिसी खाली CaptchaFox thread का इंतज़ार और खुद solve, दोनों मिलकर SDK के 300 सेकंड से आगे निकल गए, या CapSkip solve के बीच में रुक गयाएक समय में ज़्यादा से ज़्यादा दस solve चालू रखें, या उस कॉल के लिए लंबा timeout पास करें
Class "Dom\HTMLDocument" not foundPHP का वर्ज़न 8.4 से पुराना हैPHP अपग्रेड करें, या DOMDocument और DOMXPath से पार्स करें
Fatal error: Cannot use GuzzleHttp\Exception\NetworkException as NetworkExceptionGuzzle 8 ने अपनी एक NetworkException class जोड़ी, और दोनों को एक ही छोटे नाम से import किया गयाइनमें से एक को alias के साथ import करें, जैसे GuzzleNetworkException
NetworkException: Failed to connect to 127.0.0.1 port 8080CapSkip चल नहीं रहा, host या port गलत है, या PHP किसी दूसरी मशीन पर चलता हैCapSkip शुरू करें, फिर देखें कि उसे Local मोड में होना चाहिए या Server मोड में

FAQ

क्या PHP में CaptchaFox हल करने के लिए मुझे ब्राउज़र चाहिए?

नहीं। आपकी script को सिर्फ़ पेज का HTML चाहिए, जिसे Guzzle fetch करता है। ब्राउज़र वाला काम CapSkip के अंदर होता है, जो असली widget को अपने ब्राउज़र में चलाता है और token को उसी ब्राउज़र के user agent के साथ लौटाता है। आपका PHP कोड कभी JavaScript नहीं चलाता, इसलिए उसे बस session और headers को सही ढंग से आगे ले जाना होता है।

क्या मैं Guzzle के बजाय सीधे cURL इस्तेमाल कर सकता हूँ?

हाँ। एक ही cURL handle इस्तेमाल करें, जिसमें CURLOPT_COOKIEFILE खाली स्ट्रिंग पर सेट हो, जिससे उसका in-memory cookie engine चालू हो जाता है, और उसी handle पर पोस्ट करें। जब तक आप सेट न करें, सीधा cURL कोई User-Agent header भेजता ही नहीं, इसलिए पोस्ट से पहले CURLOPT_USERAGENT को नतीजे के userAgent पर सेट करें, और CURLOPT_REFERER को पेज पर। PHP के लिए Friendly Captcha गाइड इसी पूरे flow को cURL और DOMXPath से बनाती है।

क्या मैं इसके बजाय Symfony Panther से token सबमिट कर सकता हूँ?

हाँ, बशर्ते उसे सबमिट करने वाला ब्राउज़र उसी user agent के साथ चले जो CapSkip ने लौटाया। पहले हल करें, फिर Panther का Chrome शुरू करें, उसके user agent argument को उसी वैल्यू पर सेट करके, फिर फॉर्म लोड करें, token को cf-captcha-response फ़ील्ड में लिखें और सबमिट करें। अगर आपकी script को किसी और काम के लिए ब्राउज़र की ज़रूरत नहीं है, तो Guzzle वाला तरीका ज़्यादा हल्का है। Symfony Panther गाइड दिखाती है कि PHP से किसी पेज में token कैसे लिखा जाता है।

क्या VPS पर चल रहा PHP ऐप मेरे Windows PC पर चल रहे सॉल्वर का इस्तेमाल कर सकता है?

हाँ। कनेक्शन सेटिंग्स में CapSkip को Server मोड पर कर दें ताकि वह loopback के बजाय किसी नेटवर्क पते पर सुने। फिर जहाँ ऐप चलता है वहाँ CAPSKIP_HOST सेट करें और उसे क्लाइंट को पास करें, जैसा पूरा उदाहरण करता है। VPS, shared host और कंटेनर, सब उसी HTTP API के ज़रिए जुड़ते हैं। जब रास्ता इंटरनेट से होकर जाए तो स्टैटिक पब्लिक IP इस्तेमाल करें, और जिन पतों को पोर्ट तक नहीं पहुँचना चाहिए उनके लिए Windows Firewall में एक block नियम जोड़ें। सॉल्वर आपके अपने हार्डवेयर पर ही रहता है, इसलिए ज़्यादा solve चलाने से आपका खर्च कभी नहीं बदलता।

संक्षेप में

PHP में CaptchaFox हल करने के लिए, cookies चालू और http_errors बंद करके बनाए गए एक ही Guzzle क्लाइंट से पेज fetch करें, और on_stats से आख़िरी पता दर्ज करें। sitekey को captchafox container, render कॉल या MAM script से पढ़ें, और ध्यान दें कि पेज कौन सा build लोड करता है। captchafox को sitekey और उसी पते के साथ कॉल करें, और api_server सिर्फ़ MAM पेजों के लिए जोड़ें। token को form_params के साथ cf-captcha-response में पोस्ट करें, Referer के रूप में पेज और User-Agent header के रूप में नतीजे के userAgent के साथ, एक बार और तुरंत। unsolvable challenge आने पर दोबारा कोशिश करें, एक समय में Max. Threads (डिफ़ॉल्ट रूप से 10) से ज़्यादा solve न चलाएँ, और जैसे ही script सॉल्वर की मशीन से बाहर जाए, Server मोड पर चले जाएँ।

इस टाइप में दोबारा कोशिश का मतलब आमतौर पर बस नया challenge निकलवाना होता है। आपकी अपनी Windows मशीन पर चलने वाले लोकल captcha सॉल्वर के साथ हर नया challenge उस मशीन के बस कुछ सेकंड लेता है, किसी और बिल किए गए solve का ख़र्च नहीं।