So lösen Sie Captchas in AWS Lambda, ohne in ein Timeout zu laufen

aws lambda captcha - How to Solve CAPTCHAs in AWS Lambda Without Timing Out

Ein Captcha-Lösen in AWS Lambda scheitert an zwei Stellen, und keine davon ist Ihr Code. API Gateway hört nach 29 Sekunden auf, auf die Funktion zu warten, ein reCAPTCHA, das 40 braucht, liefert dem Aufrufer also eine 504, während die Funktion noch arbeitet. Und 127.0.0.1 in der Lambda-Sandbox ist die Sandbox selbst, ein Client, der auf Loopback zeigt, findet dort also nichts, das lauscht. CapSkip läuft auf einem Rechner, der Ihnen gehört, und das ist in diesem Aufbau nie der, auf dem Ihre Funktion läuft. Bringen Sie zuerst die Adresse in Ordnung und holen Sie dann das Lösen vom Anfragepfad herunter.

Was Sie brauchen

  • CapSkip, laufend auf einer Windows-Maschine, die Sie kontrollieren. Es ist eine Desktop-Anwendung und läuft nicht in Lambda. Die Funktion ist hier der Client, mehr nicht.
  • Eine Lambda-Runtime mit Python 3.10 oder neuer, mit dem CapSkip-Paket im Deployment-Paket oder in einem Layer.
  • Der Server-Modus eingeschaltet. Der Local-Modus antwortet auf 127.0.0.1 und nur für dieses Gerät, was einer Funktion in AWS nichts nützt. Der Server-Modus lauscht auf Ihrer Netzwerkadresse oder öffentlichen IP, damit die Funktion ihn über dieselbe API erreichen kann, und beide finden Sie unter Verbindungseinstellungen. Eine statische öffentliche IP ist zu empfehlen, dazu eine Firewall-Regel für die eine Adresse, von der AWS ankommen wird.
  • Einen Weg, diese Adresse von der Funktion aus zu erreichen. Schritt 2 behandelt die zwei Varianten, denn eine Funktion, die an ein VPC gehängt ist, verhält sich anders als eine, die es nicht ist.

Warum das 29-Sekunden-Timeout von API Gateway den Entwurf bestimmt

Ein Captcha-Lösen in AWS Lambda muss in drei Grenzen passen. Schreiben Sie sie auf, bevor Sie Code schreiben, denn zusammen schließen sie den naheliegenden Entwurf aus.

LimitWertLässt es sich erhöhen?
Integrations-Timeout von API Gateway29 Sekunden als VorgabeBei regionalen und privaten REST-APIs auf Kontingentantrag. AWS warnt, dass die Erhöhung Sie Drosselungskontingent des Kontos kosten kann
Timeout der Lambda-Funktion3 Sekunden als Vorgabe, höchstens 900 Sekunden bei einer StandardfunktionJa, bis zu dieser Obergrenze von 15 Minuten
Polling-Timeout von CapSkip300 Sekunden für reCAPTCHA, Turnstile und GeeTest, 120 für Bild-Captchas und ALTCHAJa, beide sind Optionen im Konstruktor

Ein synchroner API-Aufruf kann ein langsames reCAPTCHA also nicht abdecken. Die Funktion hat Platz dafür, das Gateway davor nicht, und der Aufrufer sieht eine 504, während das Lösen noch läuft und weiter abgerechnet wird.

Der Workaround, zu dem man als Nächstes greift, ist schlimmer. Früh zurückzukehren und das Lösen in einem Hintergrund-Thread zu beenden, funktioniert nicht, denn nachdem der Handler zurückgekehrt ist, friert Lambda die Ausführungsumgebung ein. AWS sagt es deutlich: Hintergrundprozesse oder Callbacks, die beim Ende der Funktion nicht fertig waren, nehmen ihre Arbeit wieder auf, wenn Lambda die Umgebung wiederverwendet. Wieder aufnehmen, nicht weiterlaufen. Ihr Thread wacht Minuten später auf, mitten im Pollen für ein Captcha, dessen Token längst abgelaufen ist, innerhalb eines Aufrufs, der nichts damit zu tun hat. Nichts wirft einen Fehler. Die Arbeit landet einfach an der falschen Stelle. AWS beschreibt den Lebenszyklus in seinem Leitfaden zur Ausführungsumgebung.

Schritt 1: Das SDK paketieren und die Funktion konfigurieren

Installieren Sie in einen Ordner und zippen Sie ihn mit Ihrem Handler, oder installieren Sie in einen Ordner namens python, zippen Sie diesen und hängen Sie ihn als Layer an. Legen Sie Plattform und Interpreter auf das fest, was die Funktion ausführt, und nicht auf das, was Ihr Laptop ausführt, sonst scheitert der Import beim Cold Start, ohne etwas Brauchbares im Log. Ohne diese Flags löst pip die Wheels für Ihr lokales Python auf, und ein Wheel, das für einen neueren Interpreter gebaut wurde, lädt auf der Runtime nicht.

# pip install capskip
pip install capskip --target package/ \
  --platform manylinux2014_x86_64 --implementation cp \
  --python-version 3.12 --only-binary=:all:

cp lambda_function.py package/
cd package && zip -r ../function.zip . > /dev/null && cd ..

aws lambda update-function-code \
  --function-name solve-captcha --zip-file fileb://function.zip

Legen Sie danach Timeout und Verbindungsdaten als Konfiguration fest statt im Code, damit dasselbe Paket gegen einen Test-Solver und einen Produktiv-Solver läuft.

# Timeout in seconds, and the Server mode address
aws lambda update-function-configuration \
  --function-name solve-captcha \
  --timeout 330 \
  --environment "Variables={CAPSKIP_HOST=203.0.113.10,CAPSKIP_PORT=8080}"

Setzen Sie das Timeout der Funktion etwas über das eigene Polling-Timeout des Clients, nicht darunter. Darunter beendet Lambda den Aufruf zuerst, und Sie bekommen ein nacktes Task-Timeout in CloudWatch statt der TimeoutException, die Ihnen gesagt hätte, was passiert ist.

Schritt 2: Der Funktion ein NAT gateway und eine Elastic IP geben

Das ist der Schritt, der entscheidet, ob überhaupt etwas funktioniert, und die Antwort hängt an einer Einstellung, die Sie vielleicht nicht als Netzwerkthema gesehen haben.

Konfiguration der FunktionWas sie erreichen kannWas Sie in Ihrer Firewall freigeben
Nicht an ein VPC gehängtDas öffentliche Internet, sofortNichts Brauchbares. Der Egress kommt von AWS-eigenen Adressen, die wechseln, es lässt sich also keine einzelne IP freigeben
An ein VPC gehängt, kein NAT gatewayNur das, was in diesem VPC liegt. Ihr Solver liegt nicht darinNichts. Die Verbindung läuft in ein Timeout, statt abgelehnt zu werden
An ein VPC gehängt, über ein NAT gateway geroutetDas öffentliche Internet, von einer Adresse ausDie Elastic IP des NAT gateway, und genau diese Form wollen Sie

Lambda dokumentiert die ersten beiden Zeilen direkt: Funktionen haben standardmäßig Zugang zum öffentlichen Internet, und wer eine an ein VPC hängt, begrenzt sie auf die Ressourcen in diesem VPC, bis die Subnetze der Funktion einen Weg nach draußen haben. Dieser Weg ist ein NAT gateway in einem öffentlichen Subnetz, beschrieben im Leitfaden zum Internetzugang von Lambda. Der Nebeneffekt ist der nützliche Teil. Ein NAT gateway hält eine Elastic IP, jede Lösung kommt also von einer einzigen stabilen Adresse bei Ihrem Rechner an, und Ihre Firewall-Regel kann eine einzige Zeile sein.

Hängen Sie die Funktion an die privaten Subnetze, nicht an das öffentliche. Das ist die Falle, die trotz vorhandenem NAT gateway zu einem Hänger führt: Eine Funktion, die an einem öffentlichen Subnetz hängt, hat keinen Internetzugang, egal was die Routing-Tabelle sagt, die Pakete laufen also einfach ins Leere. Derselbe Leitfaden wiederholt das zweimal.

Ein NAT gateway ist die einfachste Form, die Ihnen eine stabile Quelladresse gibt, nicht die einzige. Ein Site-to-Site VPN oder Direct Connect aus demselben VPC erreicht einen Solver in Ihrem eigenen Netz, ohne dessen Port überhaupt ins Internet zu stellen, und beides lohnt den Aufwand, wenn die Maschine an einem Ort steht, an dem Sie lieber keinen Port öffnen. So oder so: Halten Sie den Port des Solvers für alles andere geschlossen. Der Server-Modus ist weiterhin Ihre Hardware und weiterhin ohne Abrechnung pro Lösung, er ändert nur, wo der Solver lauscht, damit ihn etwas anderes als derselbe Desktop aufrufen kann.

Schritt 3: Das Lösen vom Anfragepfad holen

Angesichts der Grenzen oben gehört ein Captcha-Job in AWS Lambda auf eine Queue und nicht auf die Anfrage. Der Handler, der API Gateway antwortet, sollte nicht der Handler sein, der löst: Nehmen Sie den Job an, legen Sie ihn auf eine Queue und antworten Sie sofort. Eine zweite Funktion liest die Queue und erledigt die Arbeit mit einem Timeout, das zu einem Captcha passt und nicht zu einer Web-Anfrage.

import json, os, uuid, boto3

sqs = boto3.client("sqs")
QUEUE_URL = os.environ["QUEUE_URL"]

def lambda_handler(event, context):
    """API Gateway calls this. It never solves anything."""
    body = json.loads(event["body"])
    job_id = str(uuid.uuid4())
    sqs.send_message(
        QueueUrl=QUEUE_URL,
        MessageBody=json.dumps({
            "job_id": job_id,
            "sitekey": body["sitekey"],
            "pageurl": body["pageurl"],
        }),
    )
    return {"statusCode": 202,
            "body": json.dumps({"job_id": job_id})}

Die Job-ID ist da, damit der Aufrufer später etwas hat, wonach er fragen kann. Legen Sie den Consumer so an, dass er die Arbeit selbst zu Ende bringt, statt einen Token zurückzureichen, denn ein Token, der auf einen zweiten HTTP-Roundtrip wartet, läuft unterwegs meist ab.

Nehmen Sie eine Queue statt eines asynchronen Aufrufs. Lambda wiederholt einen fehlgeschlagenen asynchronen Aufruf standardmäßig zweimal, und ein Captcha-Lösen ist das Falsche, um es blind zu wiederholen: Der zweite Versuch startet mit einem sitekey, dessen Seitenkontext weitergezogen ist, und Sie zahlen für das Lösen so oder so. Eine Queue nimmt die Wiederholungen nicht weg, sie macht sie sichtbar und begrenzt. Sie bekommen einen Visibility Timeout, den Sie steuern, eine Redrive-Policy und eine Dead Letter Queue, in der ein Job, der immer wieder scheitert, an einer Stelle landet, die Sie ansehen können. AWS empfiehlt einen Maximum Receive Count von mindestens fünf, was Raum für einen gedrosselten Versuch lässt, bevor die Nachricht geparkt wird.

Setzen Sie den Visibility Timeout der Queue auf mindestens das Sechsfache des Timeouts der Consumer-Funktion, was AWS aus demselben Drosselungsgrund empfiehlt. Die Reihenfolge ist nicht optional: Lambda prüft das Event Source Mapping und lehnt es ab, wenn das Timeout der Funktion größer ist als der Visibility Timeout. Bei der Funktion mit 330 Sekunden von oben heißt das ein Visibility Timeout von rund 1980 Sekunden.

Vollständiges lauffähiges Beispiel

Der Consumer. Er baut den Client einmal auf, außerhalb des Handlers, damit eine warme Umgebung ihn wiederverwendet, statt sich bei jeder Nachricht neu zu verbinden.

# pip install capskip
import json, os
from urllib.parse import urlencode
from urllib.request import urlopen
from capskip import (CapSkip, ApiException, NetworkException,
                     TimeoutException, ValidationException)

# Built at cold start and reused while the environment stays warm.
solver = CapSkip(
    host=os.environ["CAPSKIP_HOST"],      # Server mode address
    port=int(os.environ.get("CAPSKIP_PORT", 8080)),
    recaptchaTimeout=300,
)

def lambda_handler(event, context):
    failures = []
    for record in event["Records"]:
        job = json.loads(record["body"])
        try:
            result = solver.recaptcha(
                sitekey=job["sitekey"],
                url=job["pageurl"],
            )
        except NetworkException:
            # No route to the solver. Retry this one message.
            failures.append({"itemIdentifier": record["messageId"]})
            continue
        except (ApiException, TimeoutException, ValidationException) as exc:
            print("giving up on this job:", exc)
            continue

        # Use the token here. It is short lived, so do not park it.
        urlopen(job["pageurl"], data=urlencode(
            {"g-recaptcha-response": result["code"]}).encode())

    # Needs ReportBatchItemFailures on the event source mapping.
    return {"batchItemFailures": failures}

Melden Sie die fehlgeschlagene Nachricht, statt eine Exception zu werfen. Ein Werfen lässt den ganzen Batch scheitern, und SQS legt dann jede Nachricht darin zurück auf die Queue, auch die, die Sie schon gelöst haben, und genau das ist das Problem doppelter Lösungen, vor dem die Tabelle unten warnt. Eine partielle Batch-Antwort wiederholt nur den Datensatz, der fehlgeschlagen ist, und dafür muss ReportBatchItemFailures am Event Source Mapping gesetzt sein, sonst wird sie nicht beachtet.

Wiederholen Sie bei einer NetworkException und schlucken Sie die anderen drei. Kein Weg zum Solver heißt, der Job kann später gelingen, während ein unlösbares Captcha, ein Timeout oder ein falscher Parameter bei jedem Versuch gleich scheitert und ein erneuter Versuch nur dieselbe Zeit noch einmal verbraucht. Alle vier SDK-Exceptions leiten sich von CapSkipError ab, wenn Sie lieber eine einzige Sache fangen.

Dieses Absenden ist der Sinn des ganzen Entwurfs: Tun Sie das, wofür der Token da ist, im selben Aufruf. Ein reCAPTCHA-Token hält etwa zwei Minuten, wer ihn in eine Datenbank schreibt, damit ein späterer Schritt ihn abholt, holt also meist etwas ab, das bereits abgelaufen ist. Die Einzelheiten zu diesem Zeitfenster stehen im Leitfaden zum reCAPTCHA-v2-Löser, und die rohen Endpunkte hinter jedem SDK-Aufruf sind dokumentiert in der API-Referenz.

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
Eine 504 von API Gateway nach 29 Sekunden, während CloudWatch die Funktion noch als laufend zeigtDas Integrations-Timeout, nicht das Timeout der FunktionBeantworten Sie die Anfrage sofort und lösen Sie auf einer Queue
Task timed out after 3.00 secondsDas voreingestellte Timeout der Funktion, das niemand ändert, bis es zubeißtSetzen Sie es über das Polling-Timeout des Clients
Eine NetworkException, die 127.0.0.1 nenntLoopback in der Sandbox erreicht die Sandbox, und CapSkip ist nicht darinWechseln Sie in den Server-Modus und setzen Sie die Host-Umgebungsvariable
Eine Verbindung, die hängt, bis die Funktion in ein Timeout läuftDie Funktion hängt an einem VPC ohne Weg nach draußen, die Pakete laufen also ins Leere, statt abgelehnt zu werdenFügen Sie ein NAT gateway hinzu. Die Funktion vom VPC zu lösen stellt den Internetzugang ebenfalls wieder her, aber dann kann Ihre Firewall keine einzelne Adresse freigeben
Derselbe Hänger, obwohl ein NAT gateway schon vorhanden istDie Funktion hängt am öffentlichen Subnetz statt an den privatenHängen Sie sie an die privaten Subnetze, denn das sind die, die auf das NAT gateway geroutet sind
Vom Laptop aus funktioniert es, von der Funktion aus nichtIhre Adresse zu Hause ist in der Firewall freigegeben, die von AWS nichtGeben Sie die Elastic IP des NAT gateway frei
Jeder Job in einem Batch zweimal gelöstEin Datensatz hat geworfen, also hat SQS den ganzen Batch zurückgegeben, auch die Datensätze, die schon erfolgreich warenMelden Sie den fehlgeschlagenen Datensatz, statt zu werfen, und schalten Sie ReportBatchItemFailures ein
Lambda weigert sich, das Event Source Mapping anzulegenDas Timeout der Funktion ist größer als der Visibility Timeout der Queue, und Lambda prüft dasSetzen Sie den Visibility Timeout auf mindestens das Sechsfache des Funktions-Timeouts
Eine Lösung, die während eines fremden Aufrufs fertig wirdEin Hintergrund-Thread wurde eingefroren, als der Handler zurückkehrte, und beim nächsten Aufruf wieder aufgetautBeenden Sie das Lösen, bevor Sie zurückkehren. Fire and forget gibt es hier nicht
Eine TimeoutException, die 300 Sekunden nenntCapSkip hat nicht innerhalb des reCAPTCHA-Polling-Timeouts geantwortetPrüfen Sie, ob der Solver läuft und nicht ausgelastet ist. Die Obergrenze anzuheben verzögert nur dieselbe Antwort
CAPCHA_NOT_READY in einer selbst gebauten Polling-SchleifeDie Antwort ist noch nicht fertig, und das ist ein normaler Zwischenzustand und kein FehlerLassen Sie das SDK pollen, oder lesen Sie die Beschreibung in dem Leitfaden zu diesem Code
Unable to import module lambda_function, no module named capskipDas Paket wurde für die falsche Architektur oder den falschen Interpreter installiert, oder es liegt im Layer am falschen PfadInstallieren Sie mit den Flags für Plattform, Implementierung und Python-Version, und legen Sie den Layer-Inhalt unter python in die Wurzel des Zips

FAQ

Kann CapSkip selbst in Lambda laufen?

Nein, und das muss es auch nicht. CapSkip ist eine Windows-Anwendung, die auf Hardware läuft, die Ihnen gehört, und das SDK in Ihrer Funktion ist ein schlanker Client dafür über HTTP. Schalten Sie den Server-Modus ein, richten Sie die Funktion auf diese Adresse, und die Funktion ruft es genauso auf wie ein Skript auf demselben Schreibtisch. Das Lösen bleibt auf Ihrem Rechner, und deshalb zählt auch niemand die Zahl der Lösungen ab.

Kann ich hinter API Gateway überhaupt lösen?

Manchmal, und es hängt am Typ und nicht an Ihrer Konfiguration. Ein Bild-Captcha oder ein ALTCHA-Proof-of-Work ist oft in ein, zwei Sekunden fertig, und das passt mit Luft in 29 Sekunden. Ein reCAPTCHA oder eine Turnstile-Challenge-Seite schafft das häufig nicht, und wenn nicht, bekommt der Aufrufer eine 504, während die Arbeit weiterläuft und weiter kostet. Ist das ganze Produkt ein einziger synchroner Endpunkt, beantragen Sie die Erhöhung des Integrations-Timeouts für Ihre REST-API und messen Sie, was Ihr eigener Verkehr tatsächlich braucht. Die Form mit der Queue ist trotzdem die, die Sie um drei Uhr nachts nicht überrascht.

Wie lasse ich nur meine Funktion an den Solver?

Hängen Sie die Funktion an ein VPC, routen Sie ihren ausgehenden Verkehr über ein NAT gateway und geben Sie die Elastic IP dieses Gateways in Ihrer Firewall frei. Das ist die einfachste Form, die Ihnen eine stabile Quelladresse gibt, denn eine Funktion außerhalb eines VPC geht von AWS-eigenen Adressen hinaus, die sich unter Ihnen ändern. Ein Site-to-Site VPN oder Direct Connect erledigt dieselbe Aufgabe, ohne überhaupt einen Port ins Internet zu öffnen. Halten Sie den Port des Solvers für alles andere geschlossen und behandeln Sie den API-Schlüssel als zweites Schloss und nicht als einziges.

Wird die Funktion durch ein langes Lösen teuer?

Lambda rechnet die tatsächlich verstrichene Zeit ab, eine Funktion, die auf eine Antwort wartet, wird also zum gleichen Satz bezahlt wie eine, die rechnet. Das ist ein zweites Argument für die Queue: Der Consumer braucht keine große Speichergröße, denn er wartet auf das Netz, statt zu rechnen, und flussaufwärts wird nichts blockiert, während er wartet. Das Lösen selbst kostet Sie pro Captcha nichts, denn es passiert auf Ihrem eigenen Rechner. Dieselbe Abwägung taucht auf anderen gehosteten Plattformen auf, und der Azure-Functions-Leitfaden spielt das dortige Äquivalent durch.

Die Kurzfassung

Ein Captcha-Lösen in AWS Lambda braucht drei Entscheidungen, und alle fallen, bevor Sie den Handler schreiben. Stellen Sie CapSkip auf den Server-Modus um, denn Loopback in der Lambda-Sandbox erreicht nichts. Hängen Sie die Funktion an ein VPC und routen Sie sie über ein NAT gateway, damit Ihre Firewall eine einzige Elastic IP freigeben kann. Hören Sie dann auf, auf dem Anfragepfad zu lösen: API Gateway gibt Ihnen 29 Sekunden, ein langsames reCAPTCHA braucht mehr, und früh zurückzukehren hilft nicht, weil die Umgebung in dem Moment einfriert, in dem Ihr Handler es tut. Legen Sie den Job auf die Queue, lösen Sie ihn in einem Consumer, dessen Timeout über dem des Clients liegt, und verwenden Sie den Token in demselben Aufruf, der ihn verdient hat.

Jede Methode, die das Python-Paket bereitstellt, samt den Optionen, die sie jeweils annimmt, finden Sie aufgelistet auf der Python-Captcha-Solver-Seite.

Noch ein letzter Punkt zur Wirtschaftlichkeit, denn sie ist es, die den Entwurf mit der Queue bequem macht. Ein verworfener Job kostet Sie die Lambda-Millisekunden und sonst nichts, denn die Captcha-Umgehung läuft auf Hardware, die Sie bereits bezahlt haben, ein erneuter Versuch oder ein weggeworfener abgelaufener Token taucht also nie auf einer Rechnung von irgendjemandem auf.