So lösen Sie ALTCHA in Playwright und befüllen das versteckte Feld

solve altcha in playwright - How to Solve ALTCHA in Playwright and Fill the Hidden Field

Um ALTCHA in Playwright zu lösen, überlassen Sie die Arbeit niemals dem Widget. ALTCHA ist Proof of Work und keine Erkennungsaufgabe: Die Seite stellt ein Hashing-Problem, und wer es korrekt beantwortet, wird durchgelassen, wobei die CPU-Zeit die Maut ist. Es gibt nichts zu sehen und nichts zu klicken, der Browser ist also für den Rest des Ablaufs da, nicht für das Captcha. Fangen Sie die Challenge ab, die die Seite ohnehin angefordert hat, hashen Sie sie in Millisekunden auf Ihrer eigenen Maschine und schreiben Sie die Antwort dann in das Feld, das das Formular absendet. Dieser Leitfaden zeigt genau das in Playwright für Python.

Was Sie brauchen

  • CapSkip 1.2.6 oder neuer, laufend auf einem Windows-Rechner. Die ALTCHA-Unterstützung kam mit diesem Release, ein älterer Build hat also keine Methode, die Sie aufrufen könnten.
  • Python 3.10 oder neuer, mit installierten Playwright- und CapSkip-Paketen und mindestens einem heruntergeladenen Browser.
  • Die URL der Seite, auf der das Widget sitzt. Einen Sitekey brauchen Sie nicht, denn ALTCHA hat keinen.
  • Eine Adresse für den Solver. Der Lokal-Modus antwortet auf 127.0.0.1 und damit nur auf diesem Gerät, und der Server-Modus lauscht auf Ihrer Netzwerkadresse oder öffentlichen IP, sodass ein Container, ein CI-Runner oder ein anderer Rechner ihn über dieselbe API erreicht. Schritt 4 klärt, welcher davon gilt, und beide finden Sie unter Verbindungseinstellungen.
# pip install playwright capskip
pip install playwright capskip
playwright install chromium

Schritt 1: Die Challenge abfangen, während die Seite offen ist

Alles Weitere hängt an diesem einen Wert. Die Challenge ist ein kleines JSON-Dokument mit einem Algorithmus, einem Challenge-Hash, einem Salt, einer Signatur und einer Maximalzahl, und die Seite signiert es. Aus einem Playwright-Lauf heraus kommen Sie auf zwei Wegen daran, und welcher funktioniert, hängt davon ab, wie die Seite gebaut ist.

Den Endpunkt am Widget auslesen

Das Widget-Element nennt den Endpunkt, den es abfragen wird. Raten Sie beim Attribut nicht, denn es hat sich zwischen den Widget-Generationen geändert.

Widget-GenerationAttribut, das die Challenge benennt
v1 und v2challengeurl für einen Endpunkt, mit einem separaten Attribut challengejson, wenn die Challenge inline vorliegt
v3 und neuerchallenge, und dieses eine Attribut nimmt entweder eine URL oder die Challenge-Daten
# pip install playwright capskip
from playwright.sync_api import sync_playwright

PAGE_URL = "https://example.com/signup"

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto(PAGE_URL)

    # v1 and v2 use challengeurl; v3 and later use challenge.
    widget = page.locator("altcha-widget")
    endpoint = widget.get_attribute("challengeurl")
    if not endpoint:
        endpoint = widget.get_attribute("challenge")

Die drei Interaktionsstile im type-Attribut des Widgets, native, checkbox und switch, sind rein visuell. Sie senden dieselbe Nutzlast, und der Unterschied erreicht den Solver nie, Sie müssen also nicht herausfinden, welchen davon Sie gerade vor sich haben. Das separate display-Attribut ist auf dieselbe Weise visuell. ALTCHA dokumentiert sie alle in seinem eigenen Widget-Leitfaden.

Oder die Antwort abfangen, die die Seite ohnehin geholt hat

Das Attribut zu lesen, reicht nicht immer. Ein v3-Widget kann die Challenge-Daten in diesem Attribut halten statt einer URL, parsen Sie den Wert also als JSON, wenn er mit einer geschweiften Klammer beginnt, und ein Widget, das vollständig aus JavaScript konfiguriert wurde, lässt im Markup gar nichts zu lesen übrig. Die Netzwerkantwort abzufangen deckt diesen zweiten Fall ab, und es reicht Ihnen das Dokument statt eines Zeigers darauf. Richten Sie das Warten ein, bevor irgendetwas den Abruf auslöst, sonst passiert die Anfrage, während nichts zuhört.

# Filter out the widget's own script: its URL also contains
# altcha, and it loads before the challenge is ever requested.
is_challenge = lambda r: ("altcha" in r.url
    and "json" in r.headers.get("content-type", ""))

# This fires during navigation only when the widget carries
# auto="onload". Otherwise wrap the click that triggers it.
with page.expect_response(is_challenge) as caught:
    page.goto(PAGE_URL)

challenge = caught.value.json()
print(challenge["algorithm"], challenge["maxnumber"])

Prüfen Sie das auto-Attribut des Widgets, bevor Sie dieser Form vertrauen. Es entscheidet, wann die Verifizierung startet, und nur der Wert onload sendet die Anfrage während der Navigation. Bleibt es weg oder steht es auf onfocus oder onsubmit, wird nichts geholt, bis jemand das Formular berührt, legen Sie das Warten also um einen Klick auf das Widget statt um den goto.

Gleichen Sie auf einen Teilstring ab statt auf die ganze URL, aber niemals auf das Wort altcha allein. Der Pfad unterscheidet sich je nach Seite und trägt oft einen Cache-Busting-Querystring, ein exakter Vergleich ist also ein Grund, warum das nie auslöst, und ein zu lockerer Abgleich ist der andere: Das Widget-Script wird meist von einem Pfad mit altcha darin ausgeliefert, es lädt zuerst, und das Warten löst dann auf JavaScript aus, das kein JSON-Parser akzeptiert. Playwright behandelt das Muster in seinem Netzwerk-Leitfaden.

Schritt 2: Die Challenge an die eine ALTCHA-Methode übergeben

Eine einzige Methode, vollständig beschrieben durch die ALTCHA-Solver-Seite, und diese Methode nimmt die Challenge in beiden Varianten entgegen. Übergeben Sie den Endpunkt, und der Solver holt die Challenge selbst. Übergeben Sie das Dokument, und es geht überhaupt keine Anfrage hinaus.

from capskip import CapSkip

solver = CapSkip(host="127.0.0.1", port=8080)

# The document from step 1, so nothing is fetched twice.
result = solver.altcha(url=PAGE_URL, challenge_json=challenge)

# Or hand over the endpoint and let CapSkip fetch it.
# result = solver.altcha(url=PAGE_URL, challenge_url=endpoint)

print(result["token"])    # base64 payload for the form field
print(result["number"])   # the counter that satisfied it

Bevorzugen Sie das Inline-Dokument, wenn der Browser es bereits gesehen hat. Das ist die eine Stelle, an der das Steuern eines Browsers den Rat verändert. Eine Challenge, die an Ihre Browser-Session ausgegeben wurde, ist diejenige, an der die Seite Sie messen wird, dem Solver genau dieses Dokument zu übergeben hält also beide im Gleichschritt. Eine zweite Challenge von einem Endpunkt zu holen ist nicht falsch, aber es bedeutet, dass die Seite nun eine Challenge hält, während Sie eine andere beantworten, und auf einer Seite, die die Challenge an eine Session bindet, passt die Antwort nicht. Die Option nimmt ein Dictionary entgegen, das für Sie serialisiert wird, oder einen JSON-String, falls Sie bereits einen haben.

Zwei Schlüssel in diesem Ergebnis gibt es nur für ALTCHA. Der Schlüssel token enthält die base64-Nutzlast, die das Formular erwartet, und der Schlüssel number enthält den Zähler, der die Challenge gelöst hat. Der Schlüssel code trägt denselben String wie token, beide funktionieren also, aber token ist nach dem Feld benannt, in das er gehört. Die GeeTest-Schlüssel und der Turnstile-User-Agent fehlen hier.

Welche Algorithmen der Solver abdeckt

Das Legacy-Schema ist mit SHA-1, SHA-256, SHA-384 und SHA-512 abgedeckt, Proof of Work v2 mit PBKDF2 und iterativem SHA. PBKDF2 ist die Voreinstellung, die ALTCHA selbst empfiehlt, und damit die große Mehrheit der Seiten im Live-Betrieb.

Argon2id und scrypt sind die Ausnahmen, und sie werden abgelehnt statt versucht: Eine Challenge, die eines von beiden verlangt, kommt nach etwa einer Drittelsekunde als ERROR_CAPTCHA_UNSOLVABLE zurück und wird nie wiederholt, denn eine speicherharte Funktion lässt sich durch einen erneuten Versuch nicht beheben. Bei diesem Typ weist dieses Ergebnis auf den Algorithmus hin und nicht auf ein unlesbares Bild, und der Fehlercode hat einen eigenen Leitfaden.

Schritt 3: Den Token in das versteckte Feld des Widgets schreiben

Das Widget sendet seine Nutzlast in einem versteckten Input, dessen Name aus seinem eigenen name-Attribut stammt, das standardmäßig auf altcha steht. Lesen Sie dieses Attribut aus, statt es anzunehmen, genauso wie Sie das challenge-Attribut auslesen. In einem Browser-Lauf füllen Sie das Feld selbst, denn nichts hat das Widget verifiziert, und es wird nichts eingetragen haben.

# Walk up from the submit button so the field lands in the
# form that actually posts, not in the first form on the page.
SET_ALTCHA_FIELD = """({name, token}) => {
    const button = document.querySelector('button[type=submit]');
    const form = button ? button.form : document.querySelector('form');
    let field = form.querySelector('[name=' + name + ']');
    if (!field) {
        field = document.createElement('input');
        field.type = 'hidden';
        field.name = name;
        form.appendChild(field);
    }
    field.value = token;
}"""

field_name = widget.get_attribute("name") or "altcha"
page.evaluate(SET_ALTCHA_FIELD, {"name": field_name, "token": result["token"]})
page.click("button[type=submit]")

Wählen Sie das richtige Formular. Eine Registrierungsseite trägt oft mehrere, und wenn Sie das Feld an das erste Formular der Seite anhängen, während der Submit-Button zu einem anderen gehört, sieht der Server nie einen Wert. Genau deshalb läuft das Snippet vom Submit-Button aus nach oben.

Reichen Sie den String unverändert durch. Der Token ist base64 eines JSON-Dokuments, dessen Felder von der HMAC-Signatur des Servers abgedeckt sind, alles, was nach Aufräumen aussieht, macht ihn also kaputt: Whitespace entfernen, dekodieren und neu kodieren oder das JSON mit den Schlüsseln in anderer Reihenfolge neu aufbauen. Manche Integrationen lesen die Nutzlast aus einem JSON-Body-Feld statt aus einem Formularfeld, und das Widget lässt sich auch so konfigurieren, dass es sie in einem Cookie liefert, prüfen Sie also, was der Submit der Seite selbst sendet, und bilden Sie das nach.

Eine Sache, die ein Browser hinzufügt und ein reiner HTTP-Client nicht: Die Seite kann ihr eigenes Script über das Formular laufen lassen. Bleibt der Submit-Button deaktiviert, wartet die Seite darauf zu hören, dass das Widget erfolgreich war, statt das Feld zu lesen. Darauf gibt es zwei ehrliche Antworten, und welche Sie nehmen, hängt davon ab, wie viel von der Seite Sie behalten wollen. Sie können herausfinden, worauf die Seite hört, und das bedienen, oder Sie überspringen den Button und senden die Felder des Formulars direkt mit den Cookies des Browsers, was meist kürzer und immer stabiler ist.

Schritt 4: Wo der Solver läuft, sobald Playwright in die CI umzieht

Die Beispiele oben nutzen 127.0.0.1, weil das richtig ist, solange sich Ihr Skript und CapSkip einen Rechner teilen. Der Solver wird von Ihrem Python-Code aufgerufen, nicht vom Browser und nicht von der Seite, über die Adresse entscheidet also, wo der Testprozess läuft. Playwright lässt das leicht vergessen, weil der Browser ohnehin oft woanders sitzt.

Verschieben Sie diesen Prozess in das offizielle Playwright-Docker-Image oder auf einen CI-Runner, und Loopback zeigt nun auf den Container, wo nichts lauscht, die erste Lösung wirft also eine NetworkException. Stellen Sie CapSkip auf den Server-Modus um, dann lauscht es stattdessen auf Ihrer Netzwerkadresse oder öffentlichen IP, und der Container verbindet sich über dieselbe HTTP-API. Eine statische öffentliche IP ist zu empfehlen, wenn die Route über das Internet läuft, zusammen mit einer Firewall-Regel, die nur die erwarteten Adressen zulässt. Der Server-Modus ändert, wo der Solver lauscht, und sonst nichts: Es bleibt Ihre Hardware, und es bleibt ohne Nutzungsgebühren.

Wo der Python-Prozess läuftWelcher Verbindungsmodus
Auf dem CapSkip-Rechner, mit Steuerung eines lokalen BrowsersLocal-Modus. 127.0.0.1 ist hier wirklich richtig
Auf einem anderen Rechner im selben NetzwerkServer-Modus, auf der privaten Adresse dieses Rechners
In einem Playwright-Container, auf einem CI-Runner oder einem VPSServer-Modus mit einer statischen öffentlichen IP und einer Firewallregel
Lokal, aber mit Verbindung zu einem entfernten BrowserLokal-Modus. Der Browser spricht nie mit dem Solver

Lesen Sie Host und Port aus der Umgebung, damit ein Skript an beiden Orten funktioniert. Der Client übernimmt CAPSKIP_HOST, CAPSKIP_PORT und CAPSKIP_API_KEY auch von selbst, wenn Sie sie lieber nicht übergeben möchten.

Ein ALTCHA-spezifischer Hinweis zu Proxys. Ein Proxy wird hier unterstützt, aber er wird nur für das Abholen der Challenge verwendet. Es gibt keine Browser-Session, die darüber geleitet werden müsste, er hat also keine Wirkung auf den Proof of Work selbst, und er tut überhaupt nichts, wenn Sie das Challenge-Dokument inline übergeben.

Vollständiges lauffähiges Beispiel

import os
from capskip import CapSkip, ApiException, NetworkException, TimeoutException
from playwright.sync_api import sync_playwright

PAGE_URL = "https://example.com/signup"

SET_ALTCHA_FIELD = """({name, token}) => {
    const button = document.querySelector('button[type=submit]');
    const form = button ? button.form : document.querySelector('form');
    let field = form.querySelector('[name=' + name + ']');
    if (!field) {
        field = document.createElement('input');
        field.type = 'hidden';
        field.name = name;
        form.appendChild(field);
    }
    field.value = token;
}"""

solver = CapSkip(
    host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.environ.get("CAPSKIP_PORT", 8080)),
)

def is_challenge(r):
    return "altcha" in r.url and "json" in r.headers.get("content-type", "")

with sync_playwright() as p:
    page = p.chromium.launch(headless=True).new_page()

    # Catch, solve and submit with nothing slow in between.
    with page.expect_response(is_challenge) as caught:
        page.goto(PAGE_URL)

    try:
        result = solver.altcha(url=PAGE_URL, challenge_json=caught.value.json())
    except ApiException:
        raise SystemExit("refused: Argon2id, scrypt, or an expired challenge")
    except NetworkException:
        raise SystemExit("solver unreachable: check host and connection mode")
    except TimeoutException:
        raise SystemExit("no answer inside defaultTimeout")

    field_name = page.locator("altcha-widget").get_attribute("name") or "altcha"
    page.fill("input[name=email]", "someone@example.com")
    page.evaluate(SET_ALTCHA_FIELD, {"name": field_name, "token": result["token"]})
    page.click("button[type=submit]")

Alle vier Exceptions leiten sich von CapSkipError ab, wer stattdessen diese eine fängt, behandelt also jeden Fehler, den das SDK werfen kann, in einem einzigen Block. Fangen Sie die spezifischen ab, wenn die Reaktion sich unterscheidet, wie oben, und CapSkipError, wenn nicht.

Die anderen Typen funktionieren vom selben Client aus genauso. reCAPTCHA und Turnstile nehmen einen Sitekey und eine Seiten-URL, GeeTest nimmt einen gt-Wert, eine Challenge und die Seiten-URL, und das Bild-Lösen nimmt einen Dateipfad, eine URL oder base64. Alle Methoden, die das Paket bereitstellt, finden Sie auf der Python-Captcha-Solver-Seite, und das größere Browser-Bild finden Sie auf der Playwright-Captcha-Solver-Seite.

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
Beide Widget-Attribute kommen als None zurückDas Widget wurde vollständig aus JavaScript konfiguriert, keiner der beiden Namen existiert also im MarkupFangen Sie stattdessen die Antwort ab, was nicht vom Markup abhängt
get_attribute hängt 30 Sekunden und wirft dann eine ExceptionDas Widget-Element ist nie erschienen, der Locator hat also sein Standard-Timeout abgewartetPrüfen Sie den Selektor gegen die gerenderte Seite und weichen Sie dann auf das Abfangen der Antwort aus
Das Warten auf die Antwort läuft in ein TimeoutDas Widget hat kein auto-Attribut mit dem Wert onload, es wurde also nie etwas geholt, oder das Warten wurde erst nach der Navigation scharfgeschaltetUmschließen Sie das, was den Abruf auslöst, und öffnen Sie den Kontextmanager davor
Ein JSON-Parse-Fehler bei der abgefangenen AntwortDas Warten hat auf dem eigenen Script des Widgets ausgelöst, dessen URL ebenfalls altcha enthältNehmen Sie den JSON-Content-Type in den Filter auf
Eine ApiException bei einer Challenge, die Sie eben erst abgefangen habenDie Inline-Challenge war bereits abgelaufen, der Solver hat sie also abgelehnt statt sie zu hashenFangen Sie sie neu ab und lösen Sie sie in einem Zug, oder übergeben Sie den Endpunkt, damit der Solver sie erneut holen kann
Ein nackter Verifizierungsfehler, obwohl Ihr Log zeigt, dass der Token gelöst wurdeDie Challenge ist zwischen dem Lösen und dem Absenden abgelaufenAbfangen, lösen und absenden, ohne etwas Langsames dazwischen
ERROR_CAPTCHA_UNSOLVABLE in einer ApiException, nach etwa einer DrittelsekundeDie Challenge verwendet Argon2id oder scryptNichts zu wiederholen. Diese beiden werden bewusst abgelehnt
Eine NetworkException beim ersten LösenCapSkip läuft nicht, oder das Skript liegt in einem Container und zeigt auf LoopbackStarten Sie CapSkip und entscheiden Sie sich dann zwischen Lokal-Modus und Server-Modus
Das Formular wird gesendet, aber der Server meldet einen fehlenden altcha-WertDas versteckte Feld wurde an ein anderes Formular der Seite angehängtFragen Sie das Formular ab, zu dem der Submit-Button gehört
Eine TimeoutException, die 120 Sekunden nenntDer Solver hat innerhalb des Standard-Polling-Timeouts nicht geantwortetPrüfen Sie, ob der Solver läuft und nicht ausgelastet ist. Die Obergrenze anzuheben verzögert nur dieselbe Antwort
Der Submit-Button wird nie aktivDie Seite gibt ihn erst frei, wenn ihr eigenes Script den Erfolg des Widgets siehtSenden Sie die Formularfelder direkt oder bedienen Sie das, worauf die Seite hört
Eine ValidationException beim AufrufEs wurde keine der beiden Challenge-Optionen angegeben, oder es wurde eine Option übergeben, die ALTCHA nicht nimmtÜbergeben Sie den Endpunkt oder das Dokument und lassen Sie alles andere weg

FAQ

Brauche ich überhaupt einen Browser, um ALTCHA zu lösen?

Nein. ALTCHA ist ein Hashing-Problem, es wird also mit CPU gelöst, und an der Antwort ist kein Browser beteiligt. Wenn Sie Playwright nur wegen des Captchas geöffnet haben, schließen Sie es: Holen Sie die Challenge mit einem HTTP-Client und senden Sie den Token zurück. Genau diesen Weg beschreibt der Leitfaden für reines Python im Detail. Playwright verdient seinen Platz, wenn der Rest des Ablaufs eine echte Seite braucht, etwa ein Login, das Cookies setzt, ein mehrstufiges Formular oder eine Seite, die ihr Markup per Script rendert.

Erreicht Playwright den Solver aus Docker oder GitHub Actions?

Ja, über den Server-Modus. Stellen Sie CapSkip unter den Verbindungseinstellungen von Loopback auf Ihre Netzwerkadresse oder öffentliche IP um und richten Sie dann die Host-Umgebungsvariable darauf aus. Container, Runner und Solver sprechen dann dieselbe HTTP-API wie auf einem einzigen Rechner. Verwenden Sie eine statische öffentliche IP, wenn die Route über das Internet läuft, und schränken Sie sie mit einer Firewall-Regel ein. Der Solver bleibt in jedem Fall auf Hardware, die Ihnen gehört, an der Lizenz und an der Anzahl der Lösungen ändert sich also nichts.

Wie lange bleibt ein ALTCHA-Token gültig?

Nicht lange, und die Seite entscheidet. Manche Fenster schließen sich innerhalb von zwei Minuten. Läuft eines ab, lehnt die Seite die Antwort mit einem nackten Verifizierungsfehler ab, der genau wie eine falsche Antwort aussieht, und nichts in der Antwort verrät Ihnen, welcher der beiden Fälle eingetreten ist. Sammeln Sie Challenges also nicht auf Vorrat, parken Sie keinen Token in einer Variablen, während der Browser drei weitere Seiten durchläuft, und halten Sie nie einen fest, während eine Person ein Formular ausfüllt. Eine frische Lösung kostet Millisekunden und ist damit billiger, als herauszufinden, warum eine veraltete gescheitert ist.

Schneiden die eigenen Timeouts von Playwright die Lösung ab?

Nein, denn die Lösung ist kein Playwright-Aufruf. Die standardmäßigen Aktions- und Navigations-Timeouts von 30 Sekunden decken Klicks, Wartevorgänge und Seitenladungen ab, und Ihr Solver-Aufruf ist gewöhnliches Python, das zwischen zwei davon sitzt. Die Obergrenze, die greift, ist das eigene Standard-Polling-Timeout des Clients von 120 Sekunden, das ALTCHA statt des längeren reCAPTCHA-Timeouts verwendet, weil es sich um CPU-Arbeit und nicht um eine Browser-Session handelt. Achten Sie stattdessen auf ein Limit, das um den ganzen Test gelegt ist, etwa ein Plugin-Timeout pro Test oder ein Limit für den CI-Job.

Die Kurzfassung

Fangen Sie die Challenge ab, die das Widget angefordert hat, entweder über das Attribut oder über die Antwort, übergeben Sie dieses Dokument zusammen mit der Seiten-URL an die eine ALTCHA-Methode und schreiben Sie den Token in ein verstecktes Feld, benannt nach dem eigenen name-Attribut des Widgets, in dem Formular, das tatsächlich absendet. Fassen Sie den Token unterwegs nicht an. Halten Sie Abfangen, Lösen und Absenden dicht beieinander, denn das Fenster kann sich innerhalb von zwei Minuten schließen, und eine abgelaufene Challenge ist von einer falschen Antwort nicht zu unterscheiden. Wechseln Sie in den Server-Modus, sobald das Skript sich keinen Rechner mehr mit dem Solver teilt.

Noch eine letzte Sache, die verändert, wie Sie den erneuten Versuch gestalten. Weil auf diesem Weg ein lokaler Captcha-Löser den Proof of Work auf einer Maschine berechnet, die Ihnen ohnehin schon gehört, kostet eine verworfene Challenge nur ein paar Millisekunden Ihrer eigenen CPU und sonst nichts. Sie können es sich also leisten, die Seite neu zu laden und eine frische Challenge zu holen, statt einen veralteten Token durch einen langen Lauf zu schleppen.