cURL और DOMXPath के साथ PHP में Friendly Captcha कैसे हल करें

solve friendly captcha in php - How to Solve Friendly Captcha in PHP With cURL and DOMXPath

PHP में Friendly Captcha हल करने के लिए, पेज को एक ऐसे cURL handle से fetch करें जिसका cookie engine चालू हो। DOMXPath से frc-captcha widget और उसके script टैग पढ़ें, और script से तय करें कि v1 है या v2। फिर sitekey, पेज URL, वर्ज़न और script का पता capskip/capskip पैकेज के friendlyCaptcha method को पास करें। बदले में मिला token उसी handle से पोस्ट करें, फॉर्म के hidden फ़ील्ड के साथ: v2 पर frc-captcha-response में, v1 पर frc-captcha-solution में। PHP की दो आदतें यह काम बिगाड़ देती हैं। हर रिक्वेस्ट के लिए नया curl_init, session cookie को फेंक देता है, इसलिए CSRF से सुरक्षित फॉर्म एक बिल्कुल सही token को भी ठुकरा देता है। और कॉल solve खत्म होने तक block रहती है, इसलिए एक के बाद एक पचास फॉर्म में काफ़ी समय लगता है, जब तक आप उनके solve एक साथ सबमिट न करें। CapSkip ने Friendly Captcha को 1.4.0 में जोड़ा, और यह गाइड पूरा काम आपकी तरफ़ बिना किसी ब्राउज़र के करती है।

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

  • किसी Windows मशीन पर चल रहा CapSkip 1.4.0 या उसके बाद का संस्करण। Friendly Captcha उसी रिलीज़ में जोड़ा गया था, CaptchaFox और Capy Puzzle के साथ।
  • curl और dom extensions के साथ PHP 8.0 या उसके बाद का वर्ज़न, और capskip/capskip का 1.3.0 या उसके बाद का वर्ज़न, जो friendlyCaptcha वाली पहली रिलीज़ है। अगर Composer ext-curl न होने की वजह से पैकेज लेने से मना करे, तो अपनी php.ini में extension=curl को uncomment करें।
  • उस पेज का URL जो widget दिखाता है। sitekey, script का पता और फ़ील्ड का नाम, तीनों उसी पेज के HTML से निकलते हैं।
  • सॉल्वर के लिए एक पता। Local मोड केवल उसी डिवाइस के लिए 127.0.0.1 पर जवाब देता है; Server मोड आपके नेटवर्क पते या पब्लिक IP पर सुनता है ताकि किसी दूसरी मशीन पर चल रहा PHP ऐप API के ज़रिए उसे कॉल कर सके। दोनों कनेक्शन सेटिंग्स में मिलते हैं, और स्विच कब करना है, यह स्टेप 4 में बताया गया है।
# composer require capskip/capskip
composer require capskip/capskip

scripts को कमांड लाइन से चलाएँ। किसी बुरे दिन Friendly Captcha का एक solve कई मिनट ले सकता है, और कोई वेब रिक्वेस्ट शायद ही इतनी देर टिकती है; ऐसा क्यों है, यह स्टेप 4 बताता है।

स्टेप 1: एक ही cURL handle से पेज fetch करें

PHP का cURL एक्सटेंशन तब तक कोई cookie नहीं रखता जब तक आप उससे न कहें। हर handle बिना किसी cookie store के शुरू होता है, और हर रिक्वेस्ट के लिए नया handle बिल्कुल खाली शुरू होता है, इसलिए फॉर्म पोस्ट करने तक वह session cookie गायब हो चुकी होती है जो पेज ने सेट की थी। जो साइट अपने CSRF token को उसी session से जोड़ती है, वह फिर पोस्ट को ठुकरा देती है, अक्सर 403 के साथ, चाहे कैप्चा token कितना भी सही हो।

इसका इलाज एक ही option है। CURLOPT_COOKIEFILE को खाली स्ट्रिंग पर सेट करने से उस handle के लिए cURL का in-memory cookie engine चालू हो जाता है, कोई फ़ाइल पढ़े या लिखे बिना, जैसा कि curl_setopt रेफ़रेंस में बताया गया है। handle को रखें और पोस्ट के लिए उसी को फिर से इस्तेमाल करें, तो साइट की सेट की हुई हर cookie साथ आती है, redirect पर सेट हुई cookie भी।

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

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

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

// One handle for the whole run. An empty COOKIEFILE switches on the
// in-memory cookie engine, so the session survives until the post.
$http = curl_init();
curl_setopt_array($http, [
    CURLOPT_URL => PAGE_URL,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_COOKIEFILE => '',
    // PHP's cURL sends no User-Agent at all unless you set one.
    CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36',
]);
$html = curl_exec($http);
if ($html === false) {
    throw new RuntimeException('fetch failed: ' . curl_error($http));
}
// The address the page finally came from, after any redirects.
$pageUrl = curl_getinfo($http, CURLINFO_EFFECTIVE_URL);

// Without a meta charset, loadHTML reads UTF-8 as Latin-1.
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML('<?xml encoding="UTF-8">' . $html);
$xp = new DOMXPath($doc);

इसमें तीन लाइनें बाद की debugging से बचाती हैं। PHP का cURL तब तक कोई User-Agent header नहीं भेजता जब तक आप उसे सेट न करें, और कुछ साइटें बिना User-Agent वाली रिक्वेस्ट लौटा देती हैं। CURLINFO_EFFECTIVE_URL वह पता देता है जहाँ से पेज आख़िरकार आया, और जब पहला URL redirect करता है तो सॉल्वर और Referer, दोनों के लिए यही सही URL है। और encoding prefix, loadHTML को ऐसे पेज को Latin-1 के रूप में पढ़ने से रोकता है जो कोई charset घोषित नहीं करता, वरना accent वाली कोई hidden वैल्यू वापसी में कचरा बन जाती।

स्टेप 2: widget पढ़ें, v1 या v2 चुनें, और friendlyCaptcha कॉल करें

दोनों वर्ज़न एक ही element रेंडर करते हैं: frc-captcha class वाला एक div, जिस पर data-sitekey attribute होता है, इसलिए सिर्फ़ widget से प्रोटोकॉल के बारे में कुछ पता नहीं चलता। यह script टैग बताता है। v2 साइट @friendlycaptcha/sdk पैकेज लोड करती है, जिसकी फ़ाइल site.min.js है, और v1 साइट friendly-challenge लोड करती है, जिसकी फ़ाइल widget.module.min.js है। DOMXPath widget, उसके आसपास का फॉर्म और हर script ढूँढ लेता है:

$widget = $xp->query('//*[contains(concat(" ", normalize-space(@class), " "), " frc-captcha ")]')->item(0);
if (!$widget instanceof DOMElement || $widget->getAttribute('data-sitekey') === '') {
    throw new RuntimeException('No frc-captcha widget in the HTML; it may be built by JavaScript.');
}
$form = $xp->query('ancestor::form[1]', $widget)->item(0);

// Every script except nomodule fallbacks, as full addresses.
$scripts = [];
foreach ($xp->query('//script[@src][not(@nomodule)]') as $tag) {
    $scripts[] = absoluteUrl($tag->getAttribute('src'), $pageUrl);
}

खुद होस्ट किए गए widgets के लिए पूरे पते ज़रूरी हैं। v2 पर solve चलाने के लिए CapSkip ठीक वही script अपने ब्राउज़र में लोड करता है, और /vendor/v2/site.min.js जैसा सिर्फ़ पाथ कभी लोड नहीं होता। PHP का parse_url किसी URL को हिस्सों में तोड़ देता है, लेकिन relative URL को किसी पेज के आधार पर पूरे पते में नहीं बदल सकता, इसलिए यह रहा एक छोटा resolver जो absolute, protocol-relative, root-relative और dot-segment पाथ संभालता है, साथ ही # या ?step=2 जैसे उसी पेज वाले form actions भी:

// Resolve a script src against the page URL, as a browser would.
function absoluteUrl(string $src, string $base): string
{
    if (preg_match('~^[a-z][a-z0-9+.-]*:~i', $src)) {
        return $src;                                   // already absolute
    }
    if (str_starts_with($src, '#')) {
        return strtok($base, '#');                     // same page
    }
    if (str_starts_with($src, '?')) {
        return strtok($base, '?#') . $src;             // same path, new query
    }
    $b = parse_url($base);
    if (str_starts_with($src, '//')) {
        return $b['scheme'] . ':' . $src;              // protocol-relative
    }
    $origin = $b['scheme'] . '://' . $b['host'] . (isset($b['port']) ? ':' . $b['port'] : '');
    $path = str_starts_with($src, '/') ? $src : preg_replace('~[^/]*$~', '', $b['path'] ?? '/') . $src;
    $out = [];
    foreach (explode('/', $path) as $seg) {
        if ($seg === '..') {
            if (count($out) > 1) {
                array_pop($out);
            }
        } elseif ($seg !== '.') {
            $out[] = $seg;
        }
    }
    return $origin . implode('/', $out);
}

CapSkip वर्ज़न एक तय क्रम में चुनता है और पहले जवाब पर रुक जाता है: पहले आपका पास किया वर्ज़न, फिर module_script के रूप में पास किया गया script का पता, फिर डिफ़ॉल्ट v1। फँसाने वाली चीज़ यही डिफ़ॉल्ट है। /assets/app.4f2a.js जैसी bundled script से CapSkip को कुछ पता नहीं चलता, इसलिए वह v1 हल कर देता है और v2 साइट हर token ठुकरा देती है। इसलिए फ़ैसला अपने कोड में करें, जहाँ आप अनुमान लगाने से मना कर सकते हैं:

// Returns [version, script src or null], or throws if the page
// does not say which protocol it uses.
function friendlyVersion(array $scripts, DOMElement $widget): array
{
    // Package names are unambiguous, so check them first.
    foreach ($scripts as $src) {
        if (str_contains($src, '@friendlycaptcha/sdk')) return ['v2', $src];
        if (str_contains($src, 'friendly-challenge')) return ['v1', $src];
    }
    // Self-hosted builds usually keep the file name. Themes ship their
    // own site.min.js too, so only trust a path that says friendly.
    foreach ($scripts as $src) {
        if (!str_contains(strtolower($src), 'friendly')) continue;
        $file = basename((string) parse_url($src, PHP_URL_PATH));
        if (in_array($file, ['site.min.js', 'site.compat.min.js'], true)) return ['v2', $src];
        if (in_array($file, ['widget.module.min.js', 'widget.min.js', 'widget.polyfilled.min.js'], true)) return ['v1', $src];
    }
    // Last resort: v2 and v1 name their widget options differently.
    if ($widget->hasAttribute('data-api-endpoint') || $widget->hasAttribute('data-form-field-name')) return ['v2', null];
    if ($widget->hasAttribute('data-puzzle-endpoint') || $widget->hasAttribute('data-solution-field-name')) return ['v1', null];
    throw new RuntimeException('v1 or v2? Read the page and set it by hand.');
}

फिर कॉल करें। friendlyCaptcha तीन चीज़ें लेता है: sitekey, पेज URL और एक options array, और कुछ भी भेजने से पहले वह हर उस option को हटा देता है जिसकी वैल्यू null है:

use CapSkip\CapSkip;

$solver = new CapSkip(['host' => '127.0.0.1', 'port' => 8080]);
[$version, $script] = friendlyVersion($scripts, $widget);
// EU sites: data-api-endpoint="eu" (v2) or data-puzzle-endpoint (v1)
$endpoint = $widget->getAttribute('data-api-endpoint') ?: $widget->getAttribute('data-puzzle-endpoint');

$result = $solver->friendlyCaptcha($widget->getAttribute('data-sitekey'), $pageUrl, [
    'version' => $version,
    'module_script' => $script,
    'api_server' => $endpoint ?: null,
]);

echo substr($result['token'], 0, 40), PHP_EOL;   // v2 tokens start with AQQA.

endpoint पर लगे ?: null पर ध्यान दें। DOMElement का getAttribute उस attribute के लिए खाली स्ट्रिंग लौटाता है जो पेज पर नहीं है, null कभी नहीं, और SDK सिर्फ़ null हटाता है, इसलिए इसके बिना एक खाली api_server रिक्वेस्ट के साथ चला जाता है। यह option Friendly Captcha के EU endpoint पर चलने वाली साइटों के लिए है। global endpoint भी EU sitekey के लिए token बना देता है, इसलिए वहाँ हल करने पर नाकामी सिर्फ़ साइट की अपनी जाँच में सामने आती है, बिल्कुल गलत वर्ज़न वाली चुपचाप नाकामी की तरह। नतीजे की token entry वह स्ट्रिंग है जिसे सबमिट करना है, उसकी code entry में वही स्ट्रिंग होती है, और captchaId इस job के लिए CapSkip की id है। v1, v2, 1 या 2 के अलावा कोई और वर्ज़न, खाली sitekey, या ऐसा option जिसे यह method नहीं जानता, आपकी मशीन से कोई भी रिक्वेस्ट निकलने से पहले ही ValidationException throw कर देता है। PHP 8.4 में Dom\HTMLDocument भी आता है, जिसमें querySelector और एक असली HTML5 parser है। लेकिन उसके elements, DOMElement नहीं हैं, और उसका getAttribute किसी गायब attribute के लिए null लौटाता है, इसलिए इस गाइड का कोड उस पर चलाने से पहले उसमें बदलाव करने होंगे।

स्टेप 3: token को फॉर्म के अपने फ़ील्ड के साथ पोस्ट करें

token वाला फ़ील्ड आपके fetch किए HTML में नहीं होता, क्योंकि widget script उसे ब्राउज़र में बनाती है। इसलिए उसे आप खुद जोड़ते हैं, उसी नाम से जो वर्ज़न और widget इस्तेमाल करते हैं। फॉर्म में बाकी जो कुछ भी जाता है, hidden CSRF token समेत, वह फॉर्म से ही आता है, और पोस्ट स्टेप 1 वाले handle से ही जाती है:

// The fields a browser would send: hidden ones, text, checked boxes.
$fields = [];
foreach ($xp->query('.//input[@name]', $form) as $input) {
    $type = strtolower($input->getAttribute('type'));
    if ($input->hasAttribute('disabled') || 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')] = $checkable && !$input->hasAttribute('value') ? 'on' : $input->getAttribute('value');
}
$fields['email'] = 'YOUR_EMAIL';

// A renamed field wins; otherwise the default for the version.
$field = $widget->getAttribute('data-form-field-name')
    ?: $widget->getAttribute('data-solution-field-name')
    ?: ($version === 'v2' ? 'frc-captcha-response' : 'frc-captcha-solution');
$fields[$field] = $result['token'];

curl_setopt_array($http, [
    CURLOPT_URL => absoluteUrl($form->getAttribute('action') ?: $pageUrl, $pageUrl),
    CURLOPT_POST => true,
    // A string, not the array: an array makes cURL send multipart.
    CURLOPT_POSTFIELDS => http_build_query($fields),
    // Browsers send the page as Referer; some servers refuse a post without it.
    CURLOPT_REFERER => $pageUrl,
]);
$response = curl_exec($http);
echo curl_getinfo($http, CURLINFO_RESPONSE_CODE), PHP_EOL;

यह loop वह सब इकट्ठा करता है जो कोई ब्राउज़र इस फॉर्म के लिए भेजता, और इसी तरह CSRF token आपको उसका नाम लिए बिना ही सर्वर तक वापस पहुँचता है। यह submit बटन, disabled फ़ील्ड, select मेन्यू और text areas को छोड़ देता है, इसलिए अगर साइट इनकी जाँच करती है तो बटन, मेन्यू और text areas को नाम से जोड़ें। CURLOPT_POSTFIELDS को array नहीं, बल्कि http_build_query से बनी स्ट्रिंग दें: array मिलने पर cURL multipart/form-data भेजता है, जबकि ब्राउज़र यह फॉर्म URL-encoded भेजता है। पेज URL को Referer के रूप में भी भेजें। कुछ frameworks, जिनमें Django भी है, ऐसी HTTPS फॉर्म पोस्ट को मना कर देते हैं जिसमें न Referer हो न Origin, चाहे cookie और token कितने भी सही हों। v2 token करीब छह किलोबाइट का होता है, इसलिए उसकी जगह बॉडी में है, query string में कभी नहीं, जबकि v1 token डॉट से अलग किए गए चार हिस्सों वाला और कुछ सौ कैरेक्टर का होता है। इसे बिना छेड़े जस का तस आगे भेजें।

दो मामलों में अपने ब्राउज़र के DevTools में देखना पड़ता है। v1 पर, अगर data-solution-field-name को एक अकेले हाइफ़न पर सेट किया गया है, तो इसका मतलब है कि widget कोई फ़ील्ड लिखता ही नहीं, और साइट की अपनी script token को किसी और तरीके से भेजती है। कुछ साइटें फॉर्म सबमिट करने के बजाय JavaScript से JSON भी पोस्ट करती हैं। दोनों ही मामलों में, एक बार हाथ से सबमिट करें और पेज असल में जो रिक्वेस्ट भेजता है, उसे कॉपी करें।

हर token एक ही सबमिट के लिए है। Friendly Captcha का वेरिफ़िकेशन ऐसे response को ठुकरा देता है जो पहले इस्तेमाल हो चुका हो या expire हो चुका हो, इसलिए हर फॉर्म के लिए दोबारा हल करें।

स्टेप 4: एक PHP process से कई फॉर्म, और सॉल्वर कहाँ चलता है

friendlyCaptcha तब तक block रहता है जब तक token न आ जाए। यहाँ PHP में कोई event loop नहीं है, और पैकेज का AsyncCapSkip उसी class का बस दूसरा नाम है, जिसे इसलिए रखा गया है ताकि चारों SDK एक जैसे दिखें। एक फॉर्म के लिए यह ठीक है। पचास फॉर्म के लिए, एक के बाद एक कॉल करने पर हर solve के दौरान CapSkip के दस Friendly Captcha threads में से नौ खाली बैठे रहते हैं।

SDK का मैनुअल वर्कफ़्लो एक ही process से यह समस्या हल कर देता है। send() एक job सबमिट करता है और तुरंत उसकी id लौटा देता है, और getResult() किसी एक id को सिर्फ़ एक बार poll करता है, और जब तक जवाब तैयार न हो, NetworkException throw करता है। एक समय में ज़्यादा से ज़्यादा दस jobs चालू रखें, क्योंकि Friendly Captcha के लिए CapSkip की डिफ़ॉल्ट Max. Threads सेटिंग इतनी ही है, और इससे ऊपर queue में गया task खाली thread के लिए 250 सेकंड तक इंतज़ार करता है, उसके बाद CapSkip उसे फेल कर देता है:

use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\CapSkipError;
use CapSkip\Exceptions\NetworkException;
use CapSkip\Exceptions\TimeoutException;

// Keep up to $limit solves in flight and call $done($i, $outcome) as
// each ends: $outcome is the token, or the exception that ended it.
function solveAll(\CapSkip\CapSkip $solver, array $jobs, callable $done, int $limit = 10, int $timeout = 300): void
{
    $running = [];                                      // index => [id, started]
    while ($jobs || $running) {
        while ($jobs && count($running) < $limit) {
            $i = array_key_first($jobs);
            $job = $jobs[$i];
            unset($jobs[$i]);
            try {
                $running[$i] = [$solver->send([
                    'method' => 'friendly_captcha',
                    'sitekey' => $job['sitekey'],
                    'url' => $job['url'],
                    'version' => $job['version'],
                    'module_script' => $job['script'] ?? null,
                    'api_server' => $job['endpoint'] ?? null,
                ]), time()];
            } catch (CapSkipError $e) {
                $done($i, $e);
            }
        }
        sleep(1);
        foreach ($running as $i => [$id, $started]) {
            try {
                $token = $solver->getResult($id, 1)['request'];
            } catch (NetworkException $e) {
                // Not ready yet. Give up only once the deadline passes.
                if (time() - $started < $timeout) continue;
                // An empty message means not ready; anything else is a connection error.
                $token = $e->getMessage() !== '' ? $e : new TimeoutException("no answer in {$timeout} s");
            } catch (ApiException $e) {
                $token = $e;                            // e.g. ERROR_CAPTCHA_UNSOLVABLE
            }
            unset($running[$i]);
            $done($i, $token);
        }
    }
}

// $jobs: one array per page from Step 2 (sitekey, url, version, script,
// endpoint), plus that page's cURL handle and fields for the post.
solveAll($solver, $jobs, function (int $i, $outcome) use ($jobs) {
    if ($outcome instanceof Throwable) {
        echo "form $i failed: ", $outcome->getMessage(), PHP_EOL;
        return;
    }
    // Post straight away, as in Step 3, on $jobs[$i]'s own handle.
});

हर पेज को उसका अपना cURL handle दें और उसे job में रखें, क्योंकि हर फॉर्म का CSRF token उसी session का होता है जिसने उसे fetch किया था। callback से पोस्ट करने का मतलब है कि हर token बनते ही चला जाता है, सबसे धीमे solve का इंतज़ार नहीं करता। NetworkException का मतलब है कि जवाब तैयार नहीं है, और इसका मतलब यह भी है कि CapSkip तक पहुँचा नहीं जा सका। इसलिए अगर सॉल्वर रन के बीच में रुक जाए, तो जो jobs अभी भेजी नहीं गईं वे send() पर Failed to connect के साथ फेल होती हैं, और जो पहले ही भेजी जा चुकी हैं वे डेडलाइन निकलने के बाद ही फेल होती हैं। हर जवाब एक ही बार पढ़ा जा सकता है; उसके बाद CapSkip खाली body लौटाता है, जिसे SDK भी जवाब तैयार न होने के रूप में ही रिपोर्ट करता है, इसलिए किसी id को पढ़ते ही loop से हटा दें।

solve का समय हर बार अलग होता है। Friendly Captcha हर रिक्वेस्ट के लिए काम की मात्रा तय करता है और जिन पतों को वह बहुत देख चुका है उनके लिए इसे बढ़ा देता है, और CapSkip हर v2 widget को असली ब्राउज़र में हल करता है। इसीलिए friendlyCaptcha 120 सेकंड वाले defaultTimeout तक नहीं, बल्कि recaptchaTimeout तक पोलिंग करता है, जो डिफ़ॉल्ट रूप से 300 सेकंड है, और इसीलिए ऊपर वाला loop भी यही आँकड़ा इस्तेमाल करता है। CapSkip की अपनी घड़ी भी चलती है: उसकी Friendly Captcha सेटिंग्स में किसी solve को 120 सेकंड का Row Timeout मिलता है, उसके बाद उसे ERROR_CAPTCHA_UNSOLVABLE के साथ फेल कर दिया जाता है। जब दूसरी jobs पहले से CapSkip के दसों threads घेरे हों, तो कॉल को किसी खाली thread का इंतज़ार भी करना पड़ता है, इसलिए SDK को उस इंतज़ार और solve, दोनों के कुल समय से ज़्यादा टिकना होता है: CapSkip की डिफ़ॉल्ट सेटिंग्स पर 370 सेकंड तक। इसीलिए ऊपर वाला loop एक समय में दस से ज़्यादा jobs नहीं चलाता, और इसीलिए जो script सॉल्वर को दूसरे काम के साथ साझा करती है, उसे किसी एक friendlyCaptcha कॉल के लिए SDK का इंतज़ार बढ़ाने के लिए options में timeout पास करना चाहिए। poll interval भी हर कॉल के लिए सेट किया जा सकता है, लेकिन सिर्फ़ snake case में polling_interval के रूप में; camelCase वाला pollingInterval constructor का option है, और हर कॉल वाले option के रूप में यह ValidationException throw करता है। अगर लंबे रन के दौरान solve धीमे होते जाएँ, तो आमतौर पर वजह एक ही पते पर बढ़ती कठिनाई होती है। type और uri keys वाला एक प्रॉक्सी array पास करें, या CapSkip में प्रॉक्सी पूल सेट करें।

यही इंतज़ार इस बात की भी वजह है कि scripts कमांड लाइन से चलती हैं। PHP का max_execution_time वेब रिक्वेस्ट के लिए डिफ़ॉल्ट रूप से 30 सेकंड है और CLI के लिए बंद रहता है। Windows पर यह इंतज़ार में बीता समय भी गिनता है, इसलिए पेज रिक्वेस्ट के अंदर चल रहा solve एक fatal error पर खत्म होता है, जिसे कोई catch block नहीं देख पाता। Linux पर यह सिर्फ़ CPU समय गिनता है, और वहाँ उसकी जगह वेब सर्वर का अपना timeout रिक्वेस्ट को काट देता है। दोनों ही सूरतों में, solve किसी CLI script या queue worker में करें।

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

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

यह रहा पूरा रन एक ही फ़ाइल में: सिर्फ़ एक पेज URL से शुरू करके PHP में Friendly Captcha हल करने का सबसे छोटा पूरा तरीका।

<?php
// composer require capskip/capskip
// solve-friendly.php: run with php solve-friendly.php
declare(strict_types=1);

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

use CapSkip\CapSkip;
use CapSkip\Exceptions\CapSkipError;
use CapSkip\Exceptions\ValidationException;

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

function absoluteUrl(string $src, string $base): string
{
    if (preg_match('~^[a-z][a-z0-9+.-]*:~i', $src)) {
        return $src;
    }
    if (str_starts_with($src, '#')) {
        return strtok($base, '#');
    }
    if (str_starts_with($src, '?')) {
        return strtok($base, '?#') . $src;
    }
    $b = parse_url($base);
    if (str_starts_with($src, '//')) {
        return $b['scheme'] . ':' . $src;
    }
    $origin = $b['scheme'] . '://' . $b['host'] . (isset($b['port']) ? ':' . $b['port'] : '');
    $path = str_starts_with($src, '/') ? $src : preg_replace('~[^/]*$~', '', $b['path'] ?? '/') . $src;
    $out = [];
    foreach (explode('/', $path) as $seg) {
        if ($seg === '..') {
            if (count($out) > 1) {
                array_pop($out);
            }
        } elseif ($seg !== '.') {
            $out[] = $seg;
        }
    }
    return $origin . implode('/', $out);
}

function friendlyVersion(array $scripts, DOMElement $widget): array
{
    foreach ($scripts as $src) {
        if (str_contains($src, '@friendlycaptcha/sdk')) return ['v2', $src];
        if (str_contains($src, 'friendly-challenge')) return ['v1', $src];
    }
    foreach ($scripts as $src) {
        if (!str_contains(strtolower($src), 'friendly')) continue;
        $file = basename((string) parse_url($src, PHP_URL_PATH));
        if (in_array($file, ['site.min.js', 'site.compat.min.js'], true)) return ['v2', $src];
        if (in_array($file, ['widget.module.min.js', 'widget.min.js', 'widget.polyfilled.min.js'], true)) return ['v1', $src];
    }
    if ($widget->hasAttribute('data-api-endpoint') || $widget->hasAttribute('data-form-field-name')) return ['v2', null];
    if ($widget->hasAttribute('data-puzzle-endpoint') || $widget->hasAttribute('data-solution-field-name')) return ['v1', null];
    throw new RuntimeException('v1 or v2? Read the page and set it by hand.');
}

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

$http = curl_init();
curl_setopt_array($http, [
    CURLOPT_URL => PAGE_URL,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_COOKIEFILE => '',
    CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36',
]);
$html = curl_exec($http);
if ($html === false) {
    exit('fetch failed: ' . curl_error($http) . PHP_EOL);
}
$pageUrl = curl_getinfo($http, CURLINFO_EFFECTIVE_URL);

libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML('<?xml encoding="UTF-8">' . $html);
$xp = new DOMXPath($doc);

$widget = $xp->query('//*[contains(concat(" ", normalize-space(@class), " "), " frc-captcha ")]')->item(0);
if (!$widget instanceof DOMElement || $widget->getAttribute('data-sitekey') === '') {
    exit('No frc-captcha widget in the HTML; it may be built by JavaScript.' . PHP_EOL);
}
$form = $xp->query('ancestor::form[1]', $widget)->item(0);
if (!$form instanceof DOMElement) {
    exit('The widget is not inside a form; copy the request the page makes instead.' . PHP_EOL);
}
$scripts = [];
foreach ($xp->query('//script[@src][not(@nomodule)]') as $tag) {
    $scripts[] = absoluteUrl($tag->getAttribute('src'), $pageUrl);
}

try {
    [$version, $script] = friendlyVersion($scripts, $widget);
} catch (RuntimeException $e) {
    exit($e->getMessage() . PHP_EOL);
}
$endpoint = $widget->getAttribute('data-api-endpoint') ?: $widget->getAttribute('data-puzzle-endpoint');
try {
    $result = $solver->friendlyCaptcha($widget->getAttribute('data-sitekey'), $pageUrl, [
        'version' => $version,
        'module_script' => $script,
        'api_server' => $endpoint ?: null,
    ]);
} catch (ValidationException $e) {
    exit('not sent: ' . $e->getMessage() . PHP_EOL);
} catch (CapSkipError $e) {
    exit('solve failed: ' . get_class($e) . ': ' . $e->getMessage() . PHP_EOL);
}

$fields = [];
foreach ($xp->query('.//input[@name]', $form) as $input) {
    $type = strtolower($input->getAttribute('type'));
    if ($input->hasAttribute('disabled') || 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')] = $checkable && !$input->hasAttribute('value') ? 'on' : $input->getAttribute('value');
}
$fields['email'] = 'YOUR_EMAIL';
$field = $widget->getAttribute('data-form-field-name')
    ?: $widget->getAttribute('data-solution-field-name')
    ?: ($version === 'v2' ? 'frc-captcha-response' : 'frc-captcha-solution');
$fields[$field] = $result['token'];

// Post wherever the form's action points, on the same handle.
curl_setopt_array($http, [
    CURLOPT_URL => absoluteUrl($form->getAttribute('action') ?: $pageUrl, $pageUrl),
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query($fields),
    CURLOPT_REFERER => $pageUrl,
]);
curl_exec($http);
echo curl_getinfo($http, CURLINFO_RESPONSE_CODE), ' ', $version, ' ', $field, PHP_EOL;

वर्ज़न एक बार तय होता है और दो बार इस्तेमाल होता है, solve के लिए और फ़ील्ड के नाम के लिए, इसलिए दोनों में कभी मतभेद नहीं हो सकता। जब DOMXPath को कोई widget नहीं मिलता, तो आमतौर पर पेज उसे JavaScript से बनाता है, और आपको sitekey रेंडर हुए पेज से या उसे बनाने वाली script से लेना होगा। raw endpoint जो भी पैरामीटर स्वीकार करता है, वे सब यहाँ दर्ज हैं: Friendly Captcha API रेफ़रेंस.

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

आप जो देखते हैंकारणफिक्स
CapSkip से मिले token के साथ भी 403 या CSRF errorपोस्ट नए handle से गई, पेज की cookies के बिना, उसके hidden फ़ील्ड के बिना, या Referer के बिनाखाली COOKIEFILE वाले स्टेप 1 के handle से पोस्ट करें, फ़ील्ड फॉर्म से बनाएँ, और CURLOPT_REFERER सेट करें
साइट token ठुकरा देती है, और कोई दूसरा error नहीं आतागलत वर्ज़न हल हुआ, अक्सर डिफ़ॉल्ट वाला v1स्टेप 2 की तरह वर्ज़न कोड में तय करें और उसे पास करें
ऐसी साइट पर token ठुकराया जाता है जिसके widget पर data-api-endpoint (v2) या data-puzzle-endpoint (v1) हैtoken global endpoint से आया, जबकि साइट EU endpoint इस्तेमाल करती हैattribute की वैल्यू को api_server के रूप में पास करें
widget को खुद होस्ट करने वाली साइट पर v2 solve फेल हो जाता हैmodule_script एक relative पाथ था, इसलिए widget कभी लोड ही नहीं हुआabsoluteUrl से src को पेज URL के आधार पर पूरे पते में बदलें
गायब फ़ील्ड वाला error, और रिक्वेस्ट multipart/form-data के रूप में गईCURLOPT_POSTFIELDS को array दिया गया थाhttp_build_query($fields) पास करें, जो एक URL-encoded स्ट्रिंग है
accent वाली hidden वैल्यू बिगड़कर पहुँचती हैंloadHTML ने बिना meta charset वाले पेज को Latin-1 के रूप में पढ़ास्टेप 1 वाला encoding prefix बनाए रखें
pollingInterval का नाम लेता हुआ ValidationExceptionहर कॉल वाले ऑप्शन की स्पेलिंग polling_interval हैहर कॉल में snake case इस्तेमाल करें, या pollingInterval को constructor पर सेट करें
हर बार कुछ ही सेकंड में ERROR_CAPTCHA_UNSOLVABLE के साथ ApiExceptionFriendly Captcha sitekey या पेज के origin को मना कर देता है, या खाते में v2 या EU endpoint चालू नहीं हैsitekey, पेज URL और api_server जाँचें; दोबारा कोशिश करने से फ़ायदा नहीं होगा
दो मिनट या उससे ज़्यादा समय बाद ERROR_CAPTCHA_UNSOLVABLE के साथ ApiExceptionsolve CapSkip के 120 सेकंड वाले Row Timeout से आगे चला गया, या खाली thread के इंतज़ार में 250 सेकंड वाले Wait Timeout से ज़्यादा रुकाएक समय में ज़्यादा से ज़्यादा दस चालू रखें, प्रॉक्सी जोड़ें, या Friendly Captcha सेटिंग्स में Row Timeout बढ़ाएँ
300 सेकंड बाद TimeoutExceptiontask खाली thread के इंतज़ार के बाद हल हुआ, और कुल मिलाकर SDK की पोलिंग से ज़्यादा समय लगाएक समय में ज़्यादा से ज़्यादा दस चालू रखें, या लंबा timeout पास करें
Fatal error: Maximum execution time of 30 seconds exceeded, और कोई catch block नहीं चलाsolve एक वेब रिक्वेस्ट के अंदर चला, जहाँ Windows पर PHP की 30 सेकंड की सीमा इंतज़ार का समय भी गिनती हैकमांड लाइन से या किसी queue worker में हल करें
NetworkException: Failed to connect to 127.0.0.1 port 8080CapSkip चल नहीं रहा, host या port गलत है, या PHP किसी दूसरी मशीन पर चलता हैCapSkip शुरू करें, फिर देखें कि Local मोड चाहिए या Server मोड

FAQ

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

नहीं। आपकी script को सिर्फ़ HTML चाहिए, जिसे cURL ले आता है। ब्राउज़र का काम CapSkip के अंदर होता है, जो v2 widgets को अपने ब्राउज़र में और v1 puzzles को सीधे हल करता है। अगर आपका PHP किसी और वजह से पहले से ब्राउज़र चला रहा है, तो वही कॉल काम करती है: बस sitekey और script का पता fetch किए गए HTML के बजाय लाइव पेज से पढ़ें।

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

हाँ। cookies option को true पर सेट करके एक क्लाइंट बनाएँ, तो वह अपनी हर रिक्वेस्ट के लिए एक cookie jar रखता है, redirects समेत, जो खाली COOKIEFILE वाला काम कर देता है। फ़ील्ड form_params से भेजें, जो उन्हें URL-encoded पोस्ट करता है, और पेज URL को Referer header के रूप में जोड़ें। Guzzle अपने User-Agent में खुद को GuzzleHttp बताता है, इसलिए अगर साइट नखरे करती है तो ब्राउज़र वाला User-Agent सेट करें। CapSkip वाली कॉल ज़रा भी नहीं बदलतीं, क्योंकि SDK सॉल्वर से अपने आप बात करता है।

क्या मैं इसे Laravel queue job में चला सकता हूँ?

हाँ, और इसके लिए controller से बेहतर जगह queue worker है, क्योंकि worker कमांड लाइन से चलता है। fetch, solve और पोस्ट, तीनों को एक ही job में रखें, ताकि token कभी अपने session से ज़्यादा समय तक न टिके। फिर job के timeout में धीमे solve जितना समय होना चाहिए, और Windows पर worker उस timeout को लागू ही नहीं कर सकता। Laravel queue गाइड में job timeout, retry_after और CapSkip के साथ Windows वाला मामला कवर किया गया है।

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

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

संक्षेप में

PHP में Friendly Captcha हल करने के लिए, खाली CURLOPT_COOKIEFILE वाले एक ही cURL handle से पेज fetch करें, फिर DOMXPath से frc-captcha element और उसके script टैग पढ़ें, और हर src को पूरे पते में बदलें। v1 या v2 पैकेज या फ़ाइल के नाम से तय करें, या widget के अपने attributes से, और अगर दोनों में से कोई न बताए तो exception throw करें। friendlyCaptcha को sitekey, पेज URL, उस वर्ज़न और script के पते के साथ कॉल करें, और EU साइटों के लिए api_server भी दें। token को एक बार, URL-encoded रूप में उसी handle से, फॉर्म के अपने फ़ील्ड के साथ, उस फ़ील्ड में पोस्ट करें जिसका नाम वर्ज़न तय करता है। कई फॉर्म के लिए, send से सबमिट करें और getResult से poll करें, एक बार में दस।

Friendly Captcha अपनी कठिनाई की कीमत पैसे में नहीं, CPU समय में वसूलता है, इसलिए किसी व्यस्त दिन की लागत बस आपकी अपनी मशीन पर लगा solve का समय है, इसके अलावा कुछ नहीं। आपके नियंत्रण वाले हार्डवेयर पर चल रहे एक असीमित कैप्चा सॉल्वर के साथ, आपकी सीमा सिर्फ़ वह मशीन और उसे दिए गए threads हैं।