So lösen Sie CaptchaFox in PHP mit Guzzle und einem Cookie-Jar

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

Um CaptchaFox in PHP zu lösen, rufen Sie die Seite mit einem Guzzle-Client ab, der Cookies behält, und lesen den sitekey aus dem Widget aus. Übergeben Sie ihn zusammen mit der Seiten-URL an die Methode captchafox des Pakets capskip/capskip. Senden Sie den Token dann im Feld cf-captcha-response ab, mit der Seite als Referer und mit dem User-Agent, den CapSkip zusammen mit dem Token zurückgegeben hat. Die Standardeinstellungen von Guzzle arbeiten bei jedem dieser Schritte gegen Sie. Guzzle behält keine Cookies, sofern Sie es nicht ausdrücklich verlangen, sendet keinen Referer und teilt jedem Server mit, sein User-Agent sei GuzzleHttp/8. Bei einem 403 wirft es eine Exception, statt die Antwort zurückzugeben. CapSkip hat CaptchaFox in Version 1.4.0 hinzugefügt. Dieser Leitfaden behandelt den Abruf, den Aufruf, das Absenden und die Wiederholungsversuche, unter Guzzle 7 und 8.

Was Sie brauchen

  • CapSkip 1.4.0 oder neuer auf einem Windows-Rechner. Dieses Release hat CaptchaFox hinzugefügt, zusammen mit Friendly Captcha und Capy Puzzle.
  • PHP 8.4 oder neuer, für Dom\HTMLDocument und dessen Methode querySelector. Unter PHP 8.0 bis 8.3 parsen Sie die Seite stattdessen mit DOMDocument und DOMXPath. Wie das geht, zeigt der Leitfaden zum Lösen von Friendly Captcha in PHP.
  • Version 1.3.0 oder neuer von capskip/capskip, das erste Release mit einer Methode captchafox, und Guzzle 7 oder 8. Jedes Beispiel hier läuft unverändert unter Guzzle 7.15 und 8.2.
  • Die URL der Seite, die das Widget anzeigt. Der sitekey, die Formularfelder und der Build des Widgets stammen alle von dieser Seite.
  • Eine Adresse für den Solver. Im Local-Modus antwortet CapSkip auf 127.0.0.1 nur für dieses Gerät. Im Server-Modus lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, damit eine PHP-App auf einem anderen Rechner es über die API aufrufen kann. Eingestellt werden beide Modi in den Verbindungseinstellungen, und Schritt 4 erklärt, wann Sie wechseln sollten.
# PHP 8.4 or later, then the SDK and Guzzle 7 or 8
composer require capskip/capskip guzzlehttp/guzzle

Führen Sie das Skript über die Kommandozeile aus. Ein Lösevorgang kann Minuten dauern, wenn CapSkip ausgelastet ist, und so lange lebt eine Webanfrage normalerweise nicht.

Schritt 1: Die Seite mit einem einzigen Guzzle-Client abrufen und den sitekey auslesen

Ein Guzzle-Client behält keine Cookies, sofern Sie ihn nicht mit der Option cookies auf true erzeugen. Damit hält der Client ein einziges Cookie-Jar für alle Anfragen, die er sendet, einschließlich eines Cookies, das bei einer Weiterleitung gesetzt wird. So übersteht das Session-Cookie hinter einem CSRF-geschützten Formular die Zeit bis zum Absenden. Erzeugen Sie den Client einmal und verwenden Sie ihn für beide Anfragen:

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

Zwei Optionen und ein Callback tragen hier die Hauptlast. Mit http_errors auf false kommt ein 403 als Antwort zurück, die Sie lesen können, statt als ClientException. Deshalb prüft der Code den Status auch selbst: Eine Sperrseite oder ein 404 würde sonst so geparst, als wäre es das Formular. on_stats läuft einmal für jede Anfrage, die Guzzle sendet, einschließlich der einzelnen Weiterleitungsschritte. Nach dem Aufruf hält es also die Adresse fest, von der die Seite letztlich kam. Schritt 2 erklärt, warum diese Adresse für CaptchaFox so wichtig ist. Dom\HTMLDocument ist der HTML5-Parser, der in PHP 8.4 hinzugekommen ist. Anders als loadHTML von DOMDocument liest er eine Seite ohne meta charset als UTF-8, sodass ein verstecktes Feld mit Umlauten oder Akzenten unverändert zum Server zurückgeht.

Der sitekey ist öffentlich, für jeden Besucher identisch und beginnt üblicherweise mit sk_. Die meisten Seiten tragen ihn am Container des Widgets, einem div mit der Klasse captchafox und einem Attribut data-sitekey. Eine Seite, die das Widget aus ihrem eigenen Skript aufbaut, übergibt den Schlüssel stattdessen an einen Aufruf von captchafox.render. Manche Plattformen binden statt des Standard-Widgets von cdn.captchafox.com ein Paket von s.uicdn.com/mampkg/ ein. Diese Websites erwarten einen Token, der mit MAM_ beginnt, und ihre Seiten können den Schlüssel in der Adresse dieses Skripts selbst tragen.

// 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 vergleicht ganze Klassennamen, ein Wrapper mit einer Klasse wie captchafox-wrapper wird also nicht versehentlich erfasst. Die MAM-Prüfung gleicht den Paketpfad von captchafox ab und nicht den Host, denn dasselbe CDN liefert auch andere, nicht verwandte Pakete aus, darunter ein Chat-Widget. Auf Elementen von Dom\HTMLDocument gibt getAttribute für ein fehlendes Attribut null zurück, und genau das erlaubt es, die Fallbacks zu verketten. Ein Schlüssel in einer gebündelten Skriptdatei steht gar nicht im HTML. Öffnen Sie stattdessen die DevTools: Der Schlüssel ist das Pfadsegment nach /captcha/ in einer Anfrage an api.captchafox.com, auf MAM-Seiten an mam-api.captchafox.com. Ohne Container nimmt der Code das erste Formular der Seite. Hat die Seite ein Suchfeld oder ein zweites Formular, wählen Sie das Registrierungsformular deshalb über seine id aus.

Schritt 2: Der Aufruf von captchafox

Um CaptchaFox in PHP zu lösen, brauchen Sie eine einzige Methode. Sie nimmt den sitekey, die Seiten-URL und ein Options-Array entgegen, und für das Standard-Widget braucht sie nur die ersten beiden:

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

Das Ergebnis-Array enthält den Token unter token und noch einmal unter code, die ID, unter der CapSkip den Auftrag führt, unter captchaId und userAgent, wenn das Lösen einen User-Agent gemeldet hat. Dieser User-Agent gehört zu dem Browser, in dem CapSkip gelöst hat, nicht zu irgendetwas, das Sie gesendet haben, und Schritt 3 hängt davon ab. Die Methode verwirft jede Option mit dem Wert null, bevor sie etwas sendet. Deshalb kann die Zeile mit api_server bei jedem Aufruf laufen.

Senden Sie die Seite, auf der das Widget wirklich läuft. CaptchaFox registriert jeden Schlüssel für eine Liste erlaubter Domains und prüft den Host, bevor es irgendetwas ausstellt. Eine Seiten-URL auf einer anderen Domain wird deshalb bei jedem Versuch abgelehnt. Aus diesem Grund verfolgt Schritt 1 die Adresse nach Weiterleitungen. Ein Link, der auf einen www-Host oder auf eine lokalisierte Registrierungsseite weiterleitet, ist nicht der Ort, an dem das Widget sitzt.

Optionen, die die Methode annimmt

api_server wählt den Build des Widgets, und apiServer funktioniert als Alias dafür. Ein proxy-Array mit den Schlüsseln type und uri leitet den Lösevorgang über Ihren Proxy. useragent wird akzeptiert und bewusst nicht angewendet, denn CapSkip löst in einem echten Browser mit dessen eigener Identität. Eine Option timeout in Sekunden ersetzt für diesen einen Aufruf das recaptchaTimeout des Clients. Auch die Obergrenze für das Abfrageintervall lässt sich pro Aufruf setzen, allerdings nur als polling_interval in Snake-Case-Schreibweise. Die camelCase-Schreibweise pollingInterval gehört zum Konstruktor und wirft als Option pro Aufruf eine ValidationException. Dasselbe gilt für einen leeren sitekey oder eine leere URL, für eine Option, die die Methode nicht kennt, und für einen anderen Proxy-Typ als HTTP, HTTPS, SOCKS5 oder SOCKS5H, jeweils bevor überhaupt eine Anfrage Ihren Rechner verlässt.

Schritt 3: Den Token mit dem User-Agent absenden, der ihn ausgestellt hat

Wenn Sie CaptchaFox in PHP lösen, entscheidet dieser Schritt darüber, ob die Website den Token annimmt. Guzzle sendet bei jeder Anfrage, der Sie keinen User-Agent mitgeben, GuzzleHttp/8 als User-Agent, in der älteren Hauptversion GuzzleHttp/7. Das ist nicht der Browser, der den Token erzeugt hat, und eine Website, die den ausstellenden Browser erwartet, lehnt das Absenden ab. Übernehmen Sie den zurückgegebenen Wert in die Anfrage, die den Token trägt:

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;

Die Schleife sammelt, was ein Browser für dieses Formular senden würde: den versteckten CSRF-Token, Textfelder und angehakte Checkboxen, ohne deaktivierte Felder oder Buttons. Auswahlmenüs und Textbereiche überspringt sie. Fügen Sie diese also über ihren Namen hinzu, wenn das Formular welche hat, und ergänzen Sie den Namen des Absende-Buttons, wenn der Server darauf prüft. form_params sendet die Felder URL-kodiert, so wie ein Browser dieses Formular absendet. UriResolver gehört zum PSR-7-Paket von Guzzle und löst die action des Formulars so gegen die Seite auf, wie es ein Browser tut, einschließlich einer leeren action, eines reinen Query-Strings und von Punktsegmenten.

Einen eigenen Referer sendet Guzzle ebenfalls nicht. Manche Frameworks, darunter Django, lehnen einen HTTPS-Formular-POST ab, der weder einen Referer- noch einen Origin-Header trägt, ganz gleich, wie korrekt Cookie und Token sind. Guzzle 8 lehnt außerdem einen Header-Wert null rundweg mit einer InvalidArgumentException ab. Deshalb wird der User-Agent nur hinzugefügt, wenn das Lösen einen gemeldet hat.

Zwei weitere Regeln ergeben sich daraus, wie CaptchaFox den Token aufseiten der Website prüft. Jeder Token lässt sich nur einmal und nur für kurze Zeit verifizieren. Lösen Sie also erst, wenn Sie bereit sind, und senden Sie dann sofort ab. Und behandeln Sie den Token als opak: Kürzen Sie ihn nie und kodieren Sie ihn nie um. Manche Websites senden ihn aus ihrem eigenen Skript in einem JSON-Body. Senden Sie das Formular also einmal von Hand mit geöffneten DevTools ab und bilden Sie nach, was die Seite sendet. CaptchaFox zeigt in seiner Dokumentation zum Einstieg das Markup des Widgets und die serverseitige Prüfung.

Schritt 4: Wiederholungen, Timeouts und wo der Solver läuft

Welche Challenge CaptchaFox ausspielt, können Sie nicht wählen. Bei den meisten Lösevorgängen wird gar nichts angezeigt, und wenn ein Slider-Puzzle erscheint, erledigt CapSkip es meistens. Bildauswahl- und Audio-Challenges sind selten und werden nicht gelöst: Sie kommen innerhalb von Sekunden als ApiException zurück, deren Meldung ERROR_CAPTCHA_UNSOLVABLE enthält. Ein neuer Versuch bringt meist eine andere Challenge, eine Wiederholung lohnt sich also:

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

Die Meldung der Exception enthält die rohe Antwort von CapSkip, die Prüfung liest den Fehlercode also direkt daraus. Ein Schlüssel, den die Website ablehnt, zum Beispiel auf der falschen Domain, erscheint ebenfalls als unlösbar und wird erneut versucht. Er scheitert allerdings bei jedem Versuch innerhalb von Sekunden, und genau daran erkennen Sie ihn. Der andere Weg ist die Einstellung Retries in der Zeile CaptchaFox der CapSkip-Einstellungen. Sie nimmt Werte von 0 bis 3 an und steht standardmäßig auf 0. CapSkip wiederholt dann nicht unterstützte Challenges, Versuche mit Zeitüberschreitung und Browserfehler von selbst, aber nie einen abgelehnten Schlüssel.

captchafox pollt mit dem recaptchaTimeout des Clients, standardmäßig 300 Sekunden, denn es handelt sich um eine echte Browser-Sitzung. CapSkip hat zusätzlich eigene Zeitlimits: Eine Aufgabe kann bis zu 250 Sekunden (Wait Timeout) auf einen der 10 CaptchaFox-Threads (Max. Threads) warten, und ein einzelner Versuch bekommt 150 Sekunden (Row Timeout). Ist ein Thread frei, erreicht Sie ein gescheiterter Versuch als ApiException, lange bevor die 300 Sekunden des SDK abgelaufen sind. Nach langer Wartezeit in der Warteschlange kann das SDK sein Limit zuerst erreichen und eine TimeoutException werfen, während CapSkip noch arbeitet. Übergeben Sie also ein längeres timeout, wenn Sie viele Lösevorgänge in die Warteschlange stellen oder Retries erhöhen, denn jede Wiederholung kann ein weiteres Row Timeout hinzufügen.

Halten Sie bei vielen Registrierungen höchstens zehn Lösevorgänge gleichzeitig in Arbeit, passend zu Max. Threads, und geben Sie jedem Durchlauf einen eigenen Guzzle-Client, damit sich die Cookie-Jars nie vermischen. captchafox blockiert, bis der Token eintrifft. Ein einzelner PHP-Prozess führt Lösevorgänge deshalb über die SDK-Methoden send und getResult parallel aus. Schritt 4 des Leitfadens zu Friendly Captcha enthält diese Schleife. Für CaptchaFox reichen Sie den Auftrag mit der Methode captchafox ein und behalten aus dem Array, das getResult zurückgibt, das Feld userAgent, nicht nur das Feld request, denn Schritt 3 sendet mit beidem ab.

Die Beispiele verwenden 127.0.0.1, denn das ist richtig, solange PHP und CapSkip auf demselben Windows-PC laufen. Verschieben Sie das Skript auf einen VPS, einen Shared Host oder in eine Laravel-App unter Linux, zeigt Loopback auf genau diese Maschine, und der erste Aufruf wirft eine NetworkException mit Failed to connect. Schalten Sie CapSkip in den Server-Modus, dann lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, sodass jede dieser Umgebungen es über dieselbe API erreichen kann. Verwenden Sie eine statische öffentliche IP, wenn der Weg über das Internet führt. CapSkip öffnet seinen Port für jede Adresse, sobald der Server-Modus startet. Damit nur die Rechner zugreifen, die Sie erwarten, fügen Sie für alle anderen eine Blockierungsregel in der Windows Firewall hinzu. Der Solver bleibt Ihr eigener Rechner, und abgerechnet wird pro Lösung weiterhin nichts. Der Client liest keine Umgebungsvariablen von selbst. Lesen Sie CAPSKIP_HOST deshalb mit getenv aus und übergeben Sie den Wert, wie es das vollständige Beispiel tut.

Vollständiges lauffähiges Beispiel

Hier ist der gesamte Ablauf in einer Datei. signUp ruft die Seite mit einem eigenen Client ab, liest den Schlüssel aus, löst mit Wiederholungen und sendet das Formular mit seinen Cookies, einem Referer und dem User-Agent ab, der den Token ausgestellt hat.

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

Wenn jeder Durchlauf innerhalb von Sekunden scheitert, prüfen Sie zuerst die Seiten-URL, denn ein Schlüssel, der außerhalb seiner registrierten Domains verwendet wird, scheitert jedes Mal auf dieselbe Weise. Wirft die Prüfung des sitekey eine Exception, baut die Seite das Widget aus einem gebündelten Skript auf, und der Schlüssel steht im Network-Tab, wie in Schritt 1 beschrieben. Dokumentiert ist der rohe Endpunkt hinter dieser Methode im CaptchaFox-Abschnitt der API-Referenz.

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
Die Website lehnt einen Token ab, den CapSkip als gelöst zurückgegeben hatDas Absenden ging als GuzzleHttp/8 oder GuzzleHttp/7 hinaus, nicht als der Browser, der den Token ausgestellt hatSenden Sie den userAgent aus dem Ergebnis als User-Agent-Header mit dieser Anfrage
Abgelehnt, obwohl der User-Agent stimmtDie Seite lädt das MAM-Paket, und das Lösen hat das Standard-Widget verwendet, oder umgekehrtLesen Sie das Script-Tag und setzen Sie api_server passend dazu
Ein 403 beim Absenden, obwohl Token und User-Agent gültig sindDer Client wurde ohne cookies auf true erzeugt, oder ein zweiter Client hat abgesendet, sodass das CSRF-Feld nicht mehr passt; oder das Absenden erfolgte ohne RefererSenden Sie über den Client aus Schritt 1 ab und geben Sie die Seiten-URL als Referer mit
GuzzleHttp\Exception\ClientException beim Absendenhttp_errors ist standardmäßig aktiv, daher wirft jede 4xx-Antwort eine ExceptionErzeugen Sie den Client mit http_errors auf false und lesen Sie den Statuscode aus
InvalidArgumentException, die headers.User-Agent nenntGuzzle 8 lehnt einen Header-Wert null ab, und das Lösen hat keinen User-Agent gemeldetBehalten Sie die isset-Prüfung bei; lehnt die Website den eigenen Namen von Guzzle ab, senden Sie selbst den User-Agent eines aktuellen Desktop-Browsers
Eine ApiException innerhalb von Sekunden, jedes Mal, bei einem bestimmten SchlüsselDie Seiten-URL liegt außerhalb der Domains, für die der Schlüssel registriert ist, oft weil die Adresse vor einer Weiterleitung gesendet wurde, oder der Schlüssel selbst ist falschSenden Sie die Adresse, die on_stats festgehalten hat, und prüfen Sie den Schlüssel erneut
Gelegentlich eine ApiException mit ERROR_CAPTCHA_UNSOLVABLECaptchaFox hat eine Bildauswahl- oder Audio-Challenge ausgespielt oder einen Slider, den CapSkip nicht lesen konnteWiederholen Sie im Code, oder erhöhen Sie Retries in den CaptchaFox-Einstellungen von CapSkip
RuntimeException mit der Meldung, dass der GET der Seite 403 zurückgegeben hatDie Website hat den Abruf der Seite abgelehnt, oft wegen des eigenen User-Agents von GuzzleSetzen Sie für den Abruf der Seite den User-Agent eines aktuellen Desktop-Browsers auf dem Client; das Absenden trägt weiterhin den User-Agent, der den Token ausgestellt hat
ValidationException, bevor überhaupt etwas gesendet wirdEin leerer sitekey oder eine leere URL, eine Option, die die Methode nicht annimmt, etwa ein pollingInterval pro Aufruf, oder ein Proxy-Typ, den CapSkip nicht akzeptiertPrüfen Sie, ob der sitekey gefunden wurde, und korrigieren oder entfernen Sie die Option, die die Meldung nennt
TimeoutException nach fünf MinutenDie Wartezeit auf einen freien CaptchaFox-Thread hat zusammen mit dem Lösen selbst die 300 Sekunden des SDK überschritten, oder CapSkip hat mitten im Lösen gestopptHalten Sie höchstens zehn gleichzeitig in Arbeit, oder übergeben Sie für diesen Aufruf ein längeres timeout
Class "Dom\HTMLDocument" not foundPHP ist älter als 8.4Aktualisieren Sie PHP, oder parsen Sie mit DOMDocument und DOMXPath
Fatal error: Cannot use GuzzleHttp\Exception\NetworkException as NetworkExceptionGuzzle 8 hat eine eigene Klasse NetworkException hinzugefügt, und beide wurden unter demselben Kurznamen importiertImportieren Sie eine von beiden unter einem Alias, etwa GuzzleNetworkException
NetworkException: Failed to connect to 127.0.0.1 port 8080CapSkip läuft nicht, Host oder Port sind falsch, oder PHP läuft auf einem anderen RechnerStarten Sie CapSkip und prüfen Sie dann, ob es im Local-Modus oder im Server-Modus laufen soll

FAQ

Brauche ich einen Browser, um CaptchaFox in PHP zu lösen?

Nein. Ihr Skript braucht nur das HTML der Seite, und das ruft Guzzle ab. Die Browserarbeit findet in CapSkip statt: CapSkip steuert das echte Widget in seinem eigenen Browser und gibt den Token mit dem User-Agent dieses Browsers zurück. Ihr PHP-Code führt nie JavaScript aus und muss daher nur die Session und die Header korrekt mitführen.

Kann ich statt Guzzle direkt cURL verwenden?

Ja. Verwenden Sie ein einziges cURL-Handle mit CURLOPT_COOKIEFILE auf einem leeren String, was die Cookie-Engine im Arbeitsspeicher einschaltet, und senden Sie über dasselbe Handle ab. Reines cURL sendet überhaupt keinen User-Agent-Header, wenn Sie nicht selbst einen angeben. Setzen Sie CURLOPT_USERAGENT vor dem Absenden also auf den userAgent aus dem Ergebnis und CURLOPT_REFERER auf die Seite. Der Leitfaden zu Friendly Captcha für PHP baut diesen gesamten Ablauf mit cURL und DOMXPath auf.

Kann ich den Token stattdessen aus Symfony Panther absenden?

Ja, solange der Browser, der ihn absendet, unter dem User-Agent läuft, den CapSkip zurückgegeben hat. Lösen Sie zuerst und starten Sie dann das Chrome von Panther mit dem User-Agent-Argument auf diesem Wert, laden Sie das Formular, schreiben Sie den Token in das Feld cf-captcha-response und senden Sie ab. Wenn Ihr Skript sonst keinen Browser braucht, ist der Weg über Guzzle schlanker. Der Leitfaden zu Symfony Panther zeigt, wie Sie aus PHP heraus einen Token in eine Seite schreiben.

Kann eine PHP-App auf einem VPS den Solver auf meinem Windows-PC nutzen?

Ja. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, damit es auf einer Netzwerkadresse statt auf Loopback lauscht. Setzen Sie dann CAPSKIP_HOST dort, wo die App läuft, und übergeben Sie den Wert an den Client, wie es das vollständige Beispiel tut. Ein VPS, ein Shared Hosting und ein Container verbinden sich alle über dieselbe HTTP-API. Verwenden Sie eine statische öffentliche IP, wenn der Weg über das Internet führt, und fügen Sie in der Windows Firewall eine Blockierregel für Adressen hinzu, die den Port nicht erreichen sollen. Der Solver bleibt auf Hardware, die Ihnen gehört, mehr Lösevorgänge ändern also nie etwas an dem, was Sie zahlen.

Die Kurzfassung

Um CaptchaFox in PHP zu lösen, rufen Sie die Seite mit einem einzigen Guzzle-Client ab, den Sie mit aktivierten cookies und deaktiviertem http_errors erzeugt haben, und halten die endgültige Adresse mit on_stats fest. Lesen Sie den sitekey aus dem Container mit der Klasse captchafox, aus dem render-Aufruf oder aus dem MAM-Skript, und achten Sie darauf, welchen Build die Seite lädt. Rufen Sie captchafox mit dem sitekey und dieser Adresse auf und fügen Sie api_server nur für MAM-Seiten hinzu. Senden Sie den Token in cf-captcha-response mit form_params ab, mit der Seite als Referer und dem userAgent aus dem Ergebnis als User-Agent-Header, einmal und sofort. Versuchen Sie es bei unlösbaren Challenges erneut, halten Sie nicht mehr Lösevorgänge gleichzeitig in Arbeit als Max. Threads (standardmäßig 10), und wechseln Sie in den Server-Modus, sobald das Skript den Rechner des Solvers verlässt.

Eine neu ausgespielte Challenge ist meist die einzige Wiederholung, die dieser Typ braucht. Läuft ein lokaler Captcha-Löser auf Ihrem eigenen Windows-Rechner, kostet jede neue Challenge nur ein paar Sekunden Rechenzeit dieses Rechners statt einer weiteren abgerechneten Lösung.