So lösen Sie Capy Puzzle in PHP mit Symfony BrowserKit

solve capy puzzle in php - How to Solve Capy Puzzle in PHP With Symfony BrowserKit

Um Capy Puzzle in PHP zu lösen, öffnen Sie die Seite im HttpBrowser von Symfony und lesen den PUZZLE_-Schlüssel aus dem Widget-Skript aus. Übergeben Sie ihn zusammen mit der Seiten-URL an die Methode capy des Pakets capskip/capskip. Senden Sie dann die drei zurückgegebenen Werte in den Feldern capy_captchakey, capy_challengekey und capy_answer ab, aus derselben Browser-Session. HttpBrowser, die Klasse, die Goutte abgelöst hat, nimmt Ihnen den Großteil der Arbeit ab: Sie behält die Cookies, folgt Weiterleitungen und sendet die Seite als Referer. Der Haken ist ihr Formularobjekt. Die capy_-Felder erscheinen erst, wenn das JavaScript des Widgets gelaufen ist, daher weigert sich DomCrawler, sie zu setzen, und wirft Unreachable field. CapSkip hat Capy Puzzle in Version 1.4.0 hinzugefügt. Dieser Leitfaden behandelt den Schlüssel, den Aufruf, die zweisekündige Haltezeit, das Absenden an dieser Weigerung vorbei, Wiederholungen und einen Solver auf einem anderen Rechner.

Was Sie brauchen

  • CapSkip 1.4.0 oder neuer auf einem Windows-Rechner. Dieses Release hat Capy Puzzle hinzugefügt, zusammen mit CaptchaFox und Friendly Captcha.
  • PHP 8.4.1 oder neuer für die Komponenten von Symfony 8. Unter PHP 8.2 oder 8.3 wählt Composer stattdessen die LTS-Releases 7.4, und der Code unten läuft auf beiden unverändert. Das Paket capskip braucht außerdem die Erweiterungen curl und json.
  • Version 1.3.0 oder neuer von capskip/capskip, das erste Release mit einer Methode capy, dazu vier Symfony-Komponenten: BrowserKit, HttpClient, Mime und CssSelector.
  • Die URL der Seite, die das Puzzle anzeigt. Der Schlüssel, der Capy-Host und das Formular 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 ein Skript auf einem anderen Rechner es über die API aufrufen kann. Eingestellt werden beide Modi in den Verbindungseinstellungen.
# symfony/mime is needed for any POST, css-selector for filter()
composer require capskip/capskip symfony/browser-kit symfony/http-client symfony/mime symfony/css-selector

Composer installiert DomCrawler zusammen mit BrowserKit, aber BrowserKit setzt die anderen drei nicht voraus, also geben Sie sie selbst an. Ohne HttpClient gibt es keinen HttpBrowser, den Sie erzeugen könnten, und ohne CssSelector werfen die filter()-Aufrufe unten eine LogicException. Mime ist die Komponente, die erst spät scheitert. Der Abruf der Seite funktioniert ohne sie, und erst das Absenden des Formulars scheitert, mit einer LogicException, die Sie auffordert, symfony/mime zu installieren.

Schritt 1: Die Seite in HttpBrowser öffnen und den Schlüssel finden

HttpBrowser verpackt den HTTP-Client von Symfony in einen kleinen Browser: ein Cookie-Jar, ein Verlauf und Weiterleitungen, denen er von selbst folgt. Das deckt das Session-Cookie und den CSRF-Token ab, die ein Capy-Login-Formular meist mitbringt. Bei einem Fehlerstatus wirft er allerdings keine Exception. Ein 403 oder 404 kommt wie jede andere Seite zurück, prüfen Sie also den Status, bevor Sie nach einem Schlüssel suchen, wie es der Codeblock tut. Die Dokumentation der BrowserKit-Komponente beschreibt den Rest der API.

<?php
// composer require capskip/capskip symfony/browser-kit symfony/http-client symfony/mime symfony/css-selector
require __DIR__ . '/vendor/autoload.php';

use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\HttpClient;

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

// HttpBrowser keeps the cookies, follows redirects and sends a Referer.
$browser = new HttpBrowser(HttpClient::create());
$page = $browser->request('GET', PAGE_URL);
$status = $browser->getInternalResponse()->getStatusCode();
if ($status !== 200) {
    throw new RuntimeException("The page answered {$status}, not the form.");
}

// The widget script's k= parameter is the key, and everything before
// /puzzle/get_js/ is the Capy host.
$script = $page->filter('script[src*="/puzzle/get_js/"]');
$src = $script->count() ? $script->attr('src') : '';
if (str_starts_with($src, '//')) {
    $src = 'https:' . $src;
}
parse_str((string) parse_url($src, PHP_URL_QUERY), $query);
$key = $query['k'] ?? $page->filter('input[name="capy_captchakey"]')->attr('value', '');
$apiServer = strstr($src, '/puzzle/get_js/', true) ?: null;

// The form around the widget, or else the page's first POST form.
$widget = $page->filter('script[src*="/puzzle/get_js/"], input[name="capy_captchakey"]');
$formNode = ($widget->count() ? $widget->closest('form') : null)
    ?? $page->filter('form')->reduce(fn ($f) => strtolower((string) $f->attr('method')) === 'post')->first();
echo $key, ' ', $apiServer ?? 'default host', ' ', $formNode->count(), PHP_EOL;

Der Schlüssel ist öffentlich und für jeden Besucher identisch, und ein Puzzle-Schlüssel beginnt mit PUZZLE_. Die meisten Seiten tragen ihn im Parameter k der URL des Widget-Skripts, und diese URL ist Teil des HTML, das HttpBrowser herunterlädt. Manche rendern außerdem ein verstecktes Eingabefeld capy_captchakey im Formular, und der Codeblock liest dieses Feld, wenn es kein Skript gibt. Alles in der Skript-URL vor /puzzle/get_js/ ist die Capy API, hinter der der Schlüssel liegt, und CapSkip nennt sie api_server. Standardwert ist https://jp.api.capy.me, wo der Live-Dienst läuft. Beispiele, die für andere Solver geschrieben wurden, zeigen oft api.capy.me ohne das regionale Präfix, und dieser Host wird nicht mehr aufgelöst. Lassen Sie ihn also weg, falls Sie ein solches Beispiel kopiert haben. Beginnt ein src mit zwei Schrägstrichen, wird https: davorgesetzt, denn die Capy API wird nur über HTTPS ausgeliefert.

Die letzten Zeilen wählen das Formular aus. Der Aufruf closest() geht vom Widget aus nach oben bis zu dem Formular, das es enthält. Sitzt das Skript im head oder hinter dem Formular, liegt es in keinem Formular, und der Codeblock greift auf das erste POST-Formular der Seite zurück. Hat die Seite zwei POST-Formulare, wählen Sie Ihres stattdessen mit filter() über seine id aus.

Schritt 2: Der Aufruf von capy()

Um Capy Puzzle in PHP zu lösen, brauchen Sie eine einzige Methode. Sie nimmt den Schlüssel, die Seiten-URL und ein optionales Options-Array entgegen, und für eine Seite auf dem Standard-Host reichen die ersten beiden.

// composer require capskip/capskip
use CapSkip\CapSkip;

$solver = new CapSkip([
    'host' => '127.0.0.1',
    'port' => 8080,
    // Capy answers after a two second hold; poll often enough to catch it.
    'pollingInterval' => 0.5,
]);

// The page's real URL, after any redirect. A null api_server (no widget
// script in the HTML) is dropped, so CapSkip uses its default host.
$pageUrl = $browser->getHistory()->current()->getUri();
$result = $solver->capy($key, $pageUrl, ['api_server' => $apiServer]);

echo $result['captchakey'], PHP_EOL;     // goes in capy_captchakey
echo $result['challengekey'], PHP_EOL;   // goes in capy_challengekey
echo $result['answer'], PHP_EOL;         // goes in capy_answer

Das Ergebnis ist ein Array. Lesen Sie die drei benannten Einträge aus. Der Eintrag code enthält die gesamte Antwort des Servers, ein Array mit allen vier Feldern, das sich gut zum Loggen eignet. respKey ist ein leerer String, der nur zur Kompatibilität mit anderen Diensten existiert. Die Antwort ist ein langer String, der ungefähr so beginnt: 0xax8ex0xax84x. Das ist der Ziehpfad, den das Widget beim Bewegen des Teils aufgezeichnet hätte, keine Koordinate.

Übergeben Sie die URL, auf der der Browser tatsächlich gelandet ist, wie es der Codeblock tut, und nicht die Adresse, die Sie eingegeben haben. Eine Website kann diese Adresse weiterleiten, um einen abschließenden Schrägstrich oder ein Sprachpräfix anzuhängen. CapSkip sendet die Seiten-URL als Referer, wenn es das Puzzle abruft, daher sollte sie die Seite sein, von der das Formular wirklich stammt.

Optionen, die die Methode annimmt

Neben api_server, das auch auf apiServer hört, nimmt die Methode ein proxy-Array mit den Schlüsseln type und uri an. Außerdem nimmt sie useragent an, das CapSkip bei der einzigen Anfrage mitsendet, die es zum Abrufen des Puzzles stellt. Jede Option mit dem Wert null wird verworfen, deshalb kann der Aufruf oben den ausgelesenen Host jedes Mal übergeben. Es gibt auch eine Option version, aber nur puzzle wird akzeptiert. Die andere Familie von Capy, avatar, ist eine andere Challenge hinter einem anderen Endpunkt, daher lehnt das SDK sie ab, statt eine Antwort zu liefern, die die Website zurückweisen würde.

Zwei Optionen pro Aufruf sollten Sie kennen. Ein timeout in Sekunden ersetzt für diesen einen Lösevorgang das defaultTimeout des Clients. Auch die Obergrenze für das Abfrageintervall lässt sich pro Aufruf setzen, allerdings nur als polling_interval. Die camelCase-Schreibweise pollingInterval gehört zum Konstruktor und löst, an capy() übergeben, eine ValidationException aus, die die Option beim Namen nennt. Ein leerer Schlüssel oder eine leere URL, eine unbekannte Option und ein anderer Proxy-Typ als HTTP, HTTPS, SOCKS5 oder SOCKS5H lösen ebenfalls eine ValidationException aus, bevor überhaupt eine Anfrage Ihren Rechner verlässt.

Warum ein Capy-Lösevorgang etwa zwei Sekunden dauert

Die Lücke ist schnell gefunden. CapSkip ruft das Puzzle-Bild ab, ortet die Lücke mit etwas Pixel-Mathematik und baut den Ziehpfad, ganz ohne Browser. Dann wartet es, und zwar absichtlich.

Capy misst die Zeit zwischen der Ausgabe eines Puzzles und dem Eintreffen der Antwort und lehnt eine Antwort ab, die schneller zurückkommt, als ein Mensch das Teil hätte ziehen können. Die Ablehnung sieht aus wie eine falsche Antwort, daher wirkt eine korrekte, aber zu früh gelieferte Lösung genau wie ein kaputter Solver. CapSkip hält deshalb jedes Capy-Ergebnis zurück, bis seit dem Abruf des Puzzles zwei Sekunden vergangen sind. Die Haltezeit ist ein Sleep, sie kostet also Latenz und keine CPU.

Mit den Standardeinstellungen dauert ein Aufruf eher vier Sekunden, und das liegt daran, wie das SDK pollt. Es fragt das Ergebnis direkt nach dem Einreichen ab, dann erneut nach einer Viertelsekunde, und verdoppelt den Abstand jedes Mal bis zu seinem pollingInterval von 5 Sekunden. Die Abfragen fallen auf 0, 0.25, 0.75, 1.75 und 3.75 Sekunden, eine Antwort, die nach zwei Sekunden bereitsteht, wartet also auf die fünfte. In unseren Tests kehrte ein Client mit Standardeinstellungen nach 3.8 Sekunden zurück und einer, der wie oben mit pollingInterval auf 0.5 erstellt wurde, nach 2.3. Bauen Sie vor dem Absenden keine eigene Verzögerung ein, und versuchen Sie nicht, die Haltezeit einzusparen. Genau sie sorgt dafür, dass die Antwort durchgeht.

Schritt 3: Die drei capy_-Felder hinzufügen und absenden

An dieser Stelle scheitert BrowserKit-Code meistens. Naheliegend ist es, das Formularobjekt zu befüllen und abzusenden, mit setValues(), mit dem Argument values von submit() oder indem Sie das Feld wie ein Array-Element zuweisen. Alle drei Wege werfen eine InvalidArgumentException mit dem Text Unreachable field, gefolgt vom Feldnamen. DomCrawler baut das Formular aus dem HTML, das es geparst hat. Das Capy-Widget schreibt seine drei Eingabefelder per JavaScript in das Formular, wenn es in einem Browser läuft. HttpBrowser führt kein JavaScript aus, und soweit DomCrawler weiß, existieren diese Felder deshalb nicht.

// The form's own fields, the hidden CSRF token included.
$form = $formNode->form();

// $form->setValues() would throw Unreachable field: the widget never
// ran, so the form has no capy_ fields. Send them through request().
$fields = array_replace($form->getPhpValues(), [
    // Use the page's own field names, password included.
    'username' => 'YOUR_USERNAME',
    'capy_captchakey' => $result['captchakey'],
    'capy_challengekey' => $result['challengekey'],
    'capy_answer' => $result['answer'],
]);
$browser->request($form->getMethod(), $form->getUri(), $fields);

// After a redirect, this is where the login landed.
echo $browser->getInternalResponse()->getStatusCode(), ' ',
    $browser->getHistory()->current()->getUri(), PHP_EOL;

Die Abhilfe: Nutzen Sie das Formular für das, was es kennt, und senden Sie die Anfrage selbst. Die Methode getPhpValues() gibt die Felder des Formulars so zurück, wie ein Browser sie absenden würde, ohne den Button, aber mit dem versteckten CSRF-Token, und zwar in der verschachtelten Form, die PHP für Namen mit eckigen Klammern verwendet. Dann legt array_replace() Ihre Login-Felder und die drei Werte darüber. Die Funktion überschreibt außerdem ein Eingabefeld capy_captchakey, das die Seite selbst gerendert hat, sodass der Schlüssel nie doppelt hinausgeht. Zum Schluss sendet request() alles mit der eigenen Methode und Adresse des Formulars.

Das Formularobjekt zu umgehen, kostet Sie auf der Browserseite nichts. Die Methode request() ist das, was submit() intern aufruft. Sie sendet weiterhin die Cookies aus dem Cookie-Jar und die aktuelle Seite als Referer, also den Header, den strenge Frameworks wie Django bei einem HTTPS-Formular-POST prüfen. Sie folgt weiterhin einer Weiterleitung und speichert das Cookie, das eine 302-Antwort setzt, bevor sie ihr folgt. Ein Login, der bei Erfolg die Session erneuert, landet deshalb mit der neuen Session im Cookie-Jar auf der Kontoseite. Eines lässt das aus einem Formular-Knoten gebaute Formular weg: den Absende-Button. Prüft der Server den Namen des Buttons, erzeugen Sie das Formular stattdessen aus dem Button, mit selectButton() und dessen Beschriftung.

Senden Sie die Antwort genau so ab, wie sie zurückkam. Das Backend der Website prüft sie gegen das ausgegebene Puzzle, daher macht jedes Kürzen oder Neuzusammensetzen sie ungültig. Senden Sie dann zügig ab. CapSkip erzeugt für jeden Lösevorgang einen frischen Challenge-Key, und Capy bindet das Puzzle, das es ausgibt, an diesen Key. Der Key ist also nur einmal verwendbar und kurzlebig, und eine Lösung deckt ein Absenden ab. Manche Seiten senden über ihr eigenes Skript mit einem JSON-Body statt als Formular-POST. Senden Sie einmal von Hand mit geöffneten DevTools ab und setzen Sie die drei Werte dort ein, wo die Seite sie einsetzt.

Schritt 4: Wiederholungen, wenn ein Lösevorgang scheitert

Wenn Sie Capy Puzzle in PHP in nennenswertem Umfang lösen, werden einige Lösungsversuche scheitern. Ein gescheiterter Lösevorgang kommt als ApiException an, deren Meldung die rohe Antwort von CapSkip ist, standardmäßig mit ERROR_CAPTCHA_UNSOLVABLE darin. Über res.php, den Endpunkt, den das SDK abfragt, sieht jeder gescheiterte Lösevorgang gleich aus, und zwei Ursachen sind hier wichtig. Eine verfehlte Lücke kommt gelegentlich vor, und der nächste Versuch bekommt ein ganz neues Puzzle auf einem anderen Foto, er gelingt daher meist. Ein Schlüssel, den die Capy API ablehnt, scheitert bei jedem Versuch, und der Zähler Bad Keys auf der Capy-Karte im Monitor-Tab von CapSkip steigt. Wiederholen Sie im ersten Fall und beheben Sie den zweiten. Ein api_server, der nicht antwortet, scheitert ebenfalls jedes Mal, ohne Bad Keys zu verändern.

function solveCapy(\CapSkip\CapSkip $solver, string $key, string $url, array $options = [], int $attempts = 3): array
{
    for ($attempt = 1; ; $attempt++) {
        try {
            return $solver->capy($key, $url, $options);
        } catch (\CapSkip\Exceptions\ApiException $e) {
            // A missed hole reads as unsolvable, and the next try draws a
            // new puzzle. Anything else, such as a wrong API key, is final.
            if (!str_contains($e->getMessage(), 'UNSOLVABLE') || $attempt >= $attempts) {
                throw $e;
            }
        }
    }
}

Der Codeblock nennt die CapSkip-Klassen mit vollem Namen, sodass sich die Funktion in jede Datei einfügen lässt, egal was diese Datei importiert. Ein abgelehnter Schlüssel erscheint ebenfalls als unlösbar und wird hier deshalb auch wiederholt, und er scheitert bei jedem Versuch auf dieselbe Weise. Daran unterscheiden Sie die beiden Fälle. Der andere Weg ist die Einstellung Retries im Capy-Bereich der CapSkip-Einstellungen, die Werte von 0 bis 3 annimmt und standardmäßig auf 0 steht. CapSkip wiederholt eine verfehlte Lücke dann von selbst, einen abgelehnten Schlüssel aber nie, weil er genauso abgelehnt würde.

Die Methode capy pollt mit dem defaultTimeout des Clients, 120 Sekunden, denn ein Capy-Lösevorgang besteht aus einem Abruf und etwas Arithmetik und nicht aus einer Browser-Sitzung. CapSkip misst zusätzlich selbst die Zeit. In seinen Capy-Einstellungen kann eine Aufgabe bis zu 250 Sekunden (Wait Timeout) auf einen der 10 Threads (Max. Threads) warten, und ein Versuch bekommt 60 Sekunden (Row Timeout). Ein normaler Lösevorgang passt um ein Vielfaches in all das hinein. Das SDK pollt außerdem durch Verbindungsfehler hindurch weiter. Stoppt CapSkip mitten im Lösevorgang, endet der Aufruf daher mit TimeoutException und nicht mit NetworkException.

Vollständiges lauffähiges Beispiel

Hier ist der gesamte Ablauf in einer Datei. Die Funktion logIn() öffnet die Seite in einem eigenen HttpBrowser, liest Schlüssel und Host aus, löst mit Wiederholungen und sendet das Formular mit den drei Werten ab. Sie gibt den Browser zurück, sodass die Anfragen, die Sie nach dem Login stellen, die neue Session mitführen.

<?php
// composer require capskip/capskip symfony/browser-kit symfony/http-client symfony/mime symfony/css-selector
// login.php: run with php login.php
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 Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\HttpClient;

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

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

function solveCapy(CapSkip $solver, string $key, string $url, array $options = [], int $attempts = 3): array
{
    for ($attempt = 1; ; $attempt++) {
        try {
            return $solver->capy($key, $url, $options);
        } catch (ApiException $e) {
            // A missed hole reads as unsolvable; the next try draws a new puzzle.
            if (!str_contains($e->getMessage(), 'UNSOLVABLE') || $attempt >= $attempts) {
                throw $e;
            }
            echo "attempt {$attempt}: {$e->getMessage()}", PHP_EOL;
        }
    }
}

function logIn(CapSkip $solver, string $pageUrl, array $credentials): HttpBrowser
{
    // One browser per login: it owns the cookie jar and the history.
    $browser = new HttpBrowser(HttpClient::create());
    $page = $browser->request('GET', $pageUrl);
    $status = $browser->getInternalResponse()->getStatusCode();
    if ($status !== 200) {
        throw new RuntimeException("The page answered {$status}, not the form.");
    }
    $pageUrl = $browser->getHistory()->current()->getUri();

    $script = $page->filter('script[src*="/puzzle/get_js/"]');
    $src = $script->count() ? $script->attr('src') : '';
    if (str_starts_with($src, '//')) {
        $src = 'https:' . $src;
    }
    parse_str((string) parse_url($src, PHP_URL_QUERY), $query);
    $key = $query['k'] ?? $page->filter('input[name="capy_captchakey"]')->attr('value', '');
    if (!str_starts_with($key, 'PUZZLE_')) {
        throw new RuntimeException('No PUZZLE_ key in the HTML; find it in DevTools.');
    }
    $apiServer = strstr($src, '/puzzle/get_js/', true) ?: null;

    $widget = $page->filter('script[src*="/puzzle/get_js/"], input[name="capy_captchakey"]');
    $formNode = $widget->closest('form')
        ?? $page->filter('form')->reduce(fn ($f) => strtolower((string) $f->attr('method')) === 'post')->first();
    if (!$formNode->count()) {
        throw new RuntimeException('No POST form on the page; select it by id.');
    }
    $form = $formNode->form();

    $result = solveCapy($solver, $key, $pageUrl, ['api_server' => $apiServer]);

    // All three together, straight away: the challenge key is single-use.
    $browser->request($form->getMethod(), $form->getUri(), array_replace(
        $form->getPhpValues(),
        $credentials,
        [
            'capy_captchakey' => $result['captchakey'],
            'capy_challengekey' => $result['challengekey'],
            'capy_answer' => $result['answer'],
        ],
    ));

    return $browser;
}

try {
    // Use the page's own field names.
    $browser = logIn($solver, PAGE_URL, ['username' => 'YOUR_USERNAME', 'password' => 'YOUR_PASSWORD']);
    // A redirect to the account page means it worked; $browser keeps that session.
    $status = $browser->getInternalResponse()->getStatusCode();
    echo $status, ' ', $browser->getHistory()->current()->getUri(), PHP_EOL;
    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 defaultTimeout' . PHP_EOL);
    exit(1);
} catch (CapSkipError $e) {
    fwrite(STDERR, 'solve failed: ' . get_class($e) . ": {$e->getMessage()}" . PHP_EOL);
    exit(1);
} catch (RuntimeException $e) {
    // The page itself, or the network on the way to it.
    fwrite(STDERR, get_class($e) . ": {$e->getMessage()}" . PHP_EOL);
    exit(1);
}

Die catch-Blöcke folgen den zwei Exception-Familien. Die Exceptions von CapSkip erweitern RuntimeException nicht, die Transportfehler des HTTP-Clients dagegen schon. Eine ausgefallene Website landet also im letzten Block. Ein Solver, der nicht läuft, landet im dritten, und einer, der mitten im Lösevorgang stoppt, im zweiten, als TimeoutException. Eine fehlende Mime-Komponente wirft die LogicException von BrowserKit, die hier absichtlich nichts abfängt: Das ist ein Einrichtungsfehler, den Sie einmal beheben, und nichts, was zur Laufzeit behandelt werden muss. Antwortet das Absenden mit 4xx oder 5xx, beendet sich das Skript mit 1. Ein 200 allein beweist allerdings nicht, dass der Login funktioniert hat, denn viele Websites beantworten einen fehlgeschlagenen Login erneut mit dem Formular. Prüfen Sie die URL, auf der er gelandet ist, oder die Seite selbst. Den rohen Endpunkt hinter der Methode finden Sie im Capy-Abschnitt der API-Referenz.

Viele Logins gleichzeitig ausführen

PHP arbeitet eine Anfrage nach der anderen ab, und jeder Aufruf von capy() blockiert für seine etwa zwei Sekunden. Der einfache Weg zu mehr gleichzeitigen Logins sind mehr Prozesse: Führen Sie das Skript einmal pro Konto aus, jedes mit seinem eigenen HttpBrowser. Teilen Sie nie einen Browser zwischen Logins. Er hält ein einziges Cookie-Jar und einen einzigen Verlauf, zwei Sessions würden also ihre Cookies, CSRF-Tokens und Referer vermischen. Lassen Sie nicht mehr als 10 gleichzeitig laufen, denn CapSkip löst standardmäßig 10 Capy-Puzzles gleichzeitig und lässt den Rest auf einen freien Thread warten.

Um alles in einem Prozess zu halten, reichen Sie mehrere Jobs mit der Methode send() des SDK ein und holen sie mit getResult() ab. Auf diesem Muster baut der Leitfaden zu Friendly Captcha in PHP seinen Pool auf. Für Capy nimmt send() die Methode capy, den captchakey und die pageurl entgegen, und getResult() mit aktiviertem JSON gibt die drei Werte als Array zurück. Senden Sie jede Antwort ab, sobald ihr Ergebnis eintrifft, und nicht erst nach dem ganzen Batch, denn die frühesten Challenge-Keys altern, während die letzten Lösevorgänge noch laufen. Bei hohem Volumen fügen Sie außerdem Proxys hinzu. Jeder Lösevorgang holt ein neues Puzzle von der Capy API, und ein stetiger Strom solcher Abrufe von einer einzigen Adresse ist genau das Muster, für das es Rate Limiting gibt. Konfigurieren Sie einen Proxy-Pool im Capy-Bereich von CapSkip, oder übergeben Sie bei jedem Aufruf ein proxy-Array; der Proxy betrifft nur den Abruf des Puzzles, die einzige Anfrage, die ein Capy-Lösevorgang stellt.

Den Löser auf einer anderen Maschine betreiben

127.0.0.1 ist richtig, solange das Skript und CapSkip auf demselben Windows-PC laufen. Sobald der PHP-Code auf einen VPS, einen Container-Host, einen CI-Runner oder ein Shared Hosting umzieht, zeigt Loopback auf den falschen Rechner. Der erste Lösevorgang wirft dann eine NetworkException mit Failed to connect in der Meldung. Schalten Sie CapSkip in den Server-Modus, dann lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, sodass jede dieser Umgebungen es über dieselbe API aufrufen kann. Verwenden Sie eine statische öffentliche IP, wenn der Weg über das Internet führt, und aktivieren Sie die Validierung des API-Schlüssels. CapSkip öffnet seinen Port beim Start des Server-Modus für jede Adresse. Um ihn auf die Rechner zu beschränken, die Sie erwarten, fügen Sie in der Windows Firewall eine Blockierregel für alle anderen hinzu. Der Solver bleibt Ihr eigener Windows-Rechner, und abgerechnet wird pro Lösung weiterhin nichts.

Der Client liest Umgebungsvariablen nicht von selbst. Lesen Sie CAPSKIP_HOST, CAPSKIP_PORT und CAPSKIP_API_KEY in Ihrem Code aus und übergeben Sie die Werte an den Konstruktor, wie es das vollständige Beispiel tut. So läuft dasselbe Skript an Ihrem Schreibtisch und auf einem Server.

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
InvalidArgumentException: Unreachable field "capy_answer"setValues(), submit() mit values oder eine Array-Zuweisung an ein Feld, das das Widget im Browser erzeugtSenden Sie getPhpValues() plus die drei Werte über request(), wie in Schritt 3
LogicException mit der Aufforderung, symfony/mime zu installieren, nur beim Absenden des FormularsBrowserKit kann ohne die Mime-Komponente keinen Request-Body bauencomposer require symfony/mime
LogicException mit der Aufforderung, die Komponente CssSelector zu installierenfilter() wurde mit einem CSS-Selektor aufgerufen, und symfony/css-selector fehltcomposer require symfony/css-selector
Die Website meldet, die Session sei abgelaufen, obwohl capy() alle drei Werte geliefert hatDas Absenden ging von einem neuen Browser oder einem einfachen HTTP-Aufruf aus, ohne die Cookies der SeiteSenden Sie das Formular aus dem HttpBrowser ab, der die Seite abgerufen hat
Die Website lehnt die Antwort selbst abDie Antwort wurde verändert, oder nur ein oder zwei der Werte sind im Formular angekommenSenden Sie die Antwort unverändert zusammen mit den anderen beiden Werten in einer Anfrage
Ein Absenden, das einmal funktioniert hat, scheitert beim zweiten MalDer Challenge-Key ist nur einmal verwendbar und kurzlebigLösen Sie für jedes Absenden neu und senden Sie sofort ab
Eine ApiException bei jedem Versuch, bei einem bestimmten SchlüsselDie Capy API hat den Schlüssel abgelehnt, oder api_server zeigt auf den falschen HostKopieren Sie den Schlüssel erneut, einschließlich Präfix, und prüfen Sie den Host des Script-Tags
Gelegentlich eine ApiException mit ERROR_CAPTCHA_UNSOLVABLECapSkip konnte die Lücke in diesem Puzzle nicht findenVersuchen Sie es erneut; der nächste Versuch bekommt ein anderes Puzzle
ValidationException, bevor überhaupt etwas gesendet wirdEin leerer Schlüssel oder eine leere URL, eine Option, die die Methode nicht annimmt, etwa ein pollingInterval pro Aufruf, avatar als Version oder ein Proxy-Typ, den CapSkip nicht akzeptiertPrüfen Sie, ob der Schlüssel gefunden wurde, und korrigieren oder entfernen Sie die Option, die die Meldung nennt
Eine ApiException mit ERROR_KEY_DOES_NOT_EXIST oder ERROR_WRONG_USER_KEY beim ersten AufrufIn CapSkip ist die Validierung des API-Schlüssels aktiv, und das Skript hat den Standardschlüssel oder gar keinen gesendetSetzen Sie CAPSKIP_API_KEY auf einen in CapSkip konfigurierten Schlüssel
Jeder Aufruf dauert etwa vier SekundenDer Abstand zwischen den Abfragen verdoppelt sich immer weiter bis zur Standard-Obergrenze von 5 Sekunden, daher wartet eine Antwort, die nach zwei Sekunden bereitsteht, auf die Abfrage bei 3.75 SekundenSetzen Sie pollingInterval im Konstruktor auf 0.5
NetworkException: Failed to connect to 127.0.0.1 port 8080CapSkip läuft nicht, oder Host oder Port sind falsch; auf einem entfernten Rechner kann außerdem eine Firewall die Verbindung verwerfenStarten Sie CapSkip und prüfen Sie dann, ob es im Local-Modus oder im Server-Modus laufen soll
RuntimeException: The page answered 403, not the formDie Website hat den Abruf der Seite abgewiesen, noch vor jedem CaptchaÖffnen Sie die Seite in einem Browser von derselben Adresse aus; die Website blockiert möglicherweise den User-Agent Symfony BrowserKit

FAQ

Kann ich dafür noch Goutte verwenden?

Das können Sie, aber es gibt keinen Grund dafür. Das letzte Release von Goutte, 4.0.3, erschien im April 2023, und Packagist markiert das Paket als aufgegeben zugunsten von symfony/browser-kit. In 4.0.3 ist seine Klasse Client eine als veraltet markierte Unterklasse von HttpBrowser, die nichts hinzufügt. Sie durch HttpBrowser zu ersetzen, ist eine einzeilige Änderung, und alles in diesem Leitfaden gilt unverändert. Goutte-Code, der das Formular mit setValues() befüllt, stößt aus demselben Grund auf denselben Fehler Unreachable field.

Führt der User-Agent Symfony BrowserKit dazu, dass die Antwort abgelehnt wird?

Von Capy nicht. Eine Capy-Antwort ist nicht an einen Browser gebunden, CapSkip gibt dafür keinen User-Agent zurück, und der optionale User-Agent, den Sie übergeben können, verändert nur die Anfrage, die das Puzzle abruft. HttpBrowser meldet sich allerdings als Symfony BrowserKit, und eine Website kann das aus eigenen Gründen abweisen. Wenn das passiert, setzen Sie vor der ersten Anfrage einen echten User-Agent, mit der Methode setServerParameter() des Browsers und dem Schlüssel HTTP_USER_AGENT.

Kann ich dasselbe stattdessen mit Guzzle machen?

Ja. Der Aufruf von capy und die drei Felder sind dieselben. Guzzle behält allerdings keine Cookies, sofern Sie ihm kein Cookie-Jar übergeben, sendet keinen Referer und wirft bei einem 403 eine Exception. Was HttpBrowser für Sie erledigt, richten Sie dort von Hand ein. Der Leitfaden zu CaptchaFox in PHP zeigt dieses Guzzle-Setup im Detail, Cookie-Jar und Referer eingeschlossen.

Kann PHP auf einem Shared Hosting den Solver erreichen?

Ja, wenn der Hoster ausgehende Verbindungen zu Ihrem Port erlaubt. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, damit es auf einer Netzwerkadresse statt auf Loopback lauscht, lesen Sie diese Adresse in Ihrem Skript aus CAPSKIP_HOST und übergeben Sie sie an den Konstruktor. Ein VPS, ein Container-Host, ein CI-Runner und ein Shared Hosting verbinden sich alle über dieselbe HTTP-API. Verwenden Sie eine statische öffentliche IP, wenn der Weg über das Internet führt, zusammen mit einer Blockierregel in der Windows Firewall für Adressen, die den Port nicht erreichen sollen. Der Solver bleibt auf Hardware, die Ihnen gehört, an der Art, wie Lösungen gezählt werden, ändert sich also nichts.

Die Kurzfassung

Um Capy Puzzle in PHP zu lösen, öffnen Sie die Seite in einem HttpBrowser und entnehmen den PUZZLE_-Schlüssel dem Parameter k des Widget-Skripts. Notieren Sie auch den Host des Skripts, falls er vom Standard abweicht. Rufen Sie capy() mit dem Schlüssel und der URL, auf der der Browser gelandet ist, über einen Client auf, der mit pollingInterval auf 0.5 erstellt wurde, und lassen Sie ihm seine etwa zwei Sekunden. Verzichten Sie dann auf die Setter des Formularobjekts: Senden Sie getPhpValues() plus captchakey, challengekey und answer über request(), unverändert, zusammen und sofort, aus demselben Browser. Wiederholen Sie den gelegentlichen Fehltreffer, lassen Sie nicht mehr als 10 Logins gleichzeitig laufen, und wechseln Sie in den Server-Modus, sobald das Skript den Rechner des Solvers verlässt.

Noch ein letzter Punkt zu Wiederholungen. Jeder Versuch bekommt ein anderes Puzzle, daher ist ein neuer Anlauf die richtige Abhilfe bei einem Fehltreffer, und da ein unbegrenzter Captcha-Löser auf Ihrem eigenen Rechner läuft, kostet ein zweiter Versuch ein paar Sekunden und keine weitere abgerechnete Lösung.