Captcha in k6-Lasttests mit der Raw API lösen

k6 captcha - How to Solve CAPTCHA in k6 Load Tests With the Raw API

Ein Captcha-Schritt in k6 muss über die rohe HTTP-API laufen, denn k6 ist kein Node, und das SDK lässt sich dort nicht installieren. Das ist der einfache Teil: zwei Aufrufe, absenden und abfragen. Der Teil, der entscheidet, ob Ihr Test überhaupt etwas taugt, ist, wo das Lösen stattfindet. Legen Sie es in die default-Funktion, löst jeder virtuelle Nutzer bei jeder Iteration, was den Solver misst statt Ihrer Anwendung und eine Maschine flutet, die dafür nie gedacht war. Legen Sie es in die setup-Phase, erhalten Sie saubere Zahlen, mit einer ehrlichen Grenze, die Sie kennen sollten.

Was Sie brauchen

  • k6, jede aktuellere Version. Keine Pakete zu installieren, weil es nichts gibt, worin man sie installieren könnte.
  • Der sitekey und die Seiten-URL des geschützten Endpunkts, den Sie testen.
  • CapSkip läuft im Local-Modus, wenn k6 auf derselben Maschine wie der Löser läuft, oder im Server-Modus, wenn es auf einem Lastgenerator oder in CI läuft. Näheres dazu finden Sie unter den Verbindungseinstellungen.

Wann Lösen die richtige Wahl ist

Das sei klar gesagt, bevor Sie auch nur eine Zeile Code schreiben. Gehört die getestete Seite Ihnen, ist es meist die bessere Wahl, den Lastgenerator an der Challenge vorbeizulassen: Setzen Sie seine Adresse auf eine Allowlist, oder senden Sie einen Header, den Ihre Staging-Umgebung erkennt, und überspringen Sie das Widget ganz. Sie wollen Ihre Anwendung messen, und jede gelöste Challenge fügt Latenz hinzu, die jemand anderem gehört.

Lösen ist in zwei Fällen die richtige Wahl. Entweder ist der Endpunkt, den Sie treffen müssen, geschützt und Sie haben keine Kontrolle über diesen Schutz, oder der geschützte Pfad selbst ist das, was getestet wird, und ihn zu überspringen würde eine Route testen, die echter Traffic nie nimmt. Beide Fälle sind real, und für beide ist der Rest dieses Beitrags gedacht.

Warum das SDK hier nicht funktioniert

k6-Skripte sehen aus wie JavaScript, laufen aber nicht auf Node. Die require-Implementierung stammt von k6 selbst, und die Dokumentation ist deutlich, was die Grenze angeht: Sie lädt eingebaute k6-Module, lokale Dateien und entfernte Skripte, unterstützt aber nicht den Node-Modulauflösungsalgorithmus. Kein npm, kein node_modules, kein fs und kein crypto. Das CapSkip-Paket lässt sich also nicht importieren, und ebenso wenig alles andere, wonach Sie sonst greifen würden.

Das klingt schlimmer, als es ist. Die API ist 2captcha-kompatibel und hat zwei Endpunkte, sodass das eigene http-Modul von k6 dies in etwa fünfzehn Zeilen abdeckt. Falls sich Ihr Lastgenerator am Ende doch als einfacher Node-Prozess herausstellt: die Node.js-Captcha-Solver-Seite behandelt den Client, der tatsächlich ein SDK hat.

Schritt 1: das Lösen als einfache Funktion schreiben

Senden Sie an in.php, holen Sie sich eine ID, und fragen Sie dann res.php ab, bis die Antwort eintrifft. Fordern Sie JSON an, damit Sie Felder auslesen, statt Strings an einem Pipe-Zeichen aufzuteilen.

// No install step. Both modules are built into k6.
import http from 'k6/http';
import { sleep } from 'k6';

const SOLVER = 'http://127.0.0.1:8080';
const KEY = 'YOUR_API_KEY';

function solve(sitekey, pageurl) {
  const submitted = http.post(SOLVER + '/in.php', {
    key: KEY,
    method: 'userrecaptcha',
    googlekey: sitekey,
    pageurl: pageurl,
    json: '1',
  });

  return poll(submitted.json('request'));   // the captcha ID
}

Achten Sie auf den Parameternamen. reCAPTCHA erwartet googlekey, während Turnstile mit der turnstile-Methode sitekey erwartet. Den falschen zu senden ist die übliche Ursache für eine ERROR_GOOGLEKEY-Antwort.

function poll(id) {
  const url = SOLVER + '/res.php?key=' + KEY +
              '&action=get&json=1&id=' + id;

  // Roughly three minutes of headroom at five seconds a try.
  for (let i = 0; i < 36; i++) {
    sleep(5);
    const res = http.get(url);
    if (res.json('status') === 1) {
      return res.json('request');   // the token
    }
  }
  throw new Error('solve did not finish in time');
}

Eine noch ausstehende Antwort kommt als CAPCHA_NOT_READY mit dem status null zurück, weshalb die Schleife das status-Feld prüft, statt jede Antwort als fertig zu behandeln. Ergebnisse sind nur einmal lesbar, behalten Sie das Token also, sobald Sie es haben.

Schritt 2: in der setup-Phase lösen

k6 führt setup einmal aus, bevor auch nur ein virtueller Nutzer startet, und übergibt, was auch immer zurückgegeben wird, an die default-Funktion. Genau diese Form braucht es hier. Lösen Sie dort, verteilen Sie die Tokens, und kein VU zahlt für ein Lösen innerhalb der eigenen Iteration.

const PAGE = 'https://example.com/page-with-recaptcha';
const SITEKEY = 'YOUR_SITEKEY';

export const options = {
  vus: 10,
  duration: '90s',
  setupTimeout: '5m',   // the 60s default expires mid solve
};

export function setup() {
  // One token per VU. They are single use.
  const tokens = [];
  for (let i = 0; i < 10; i++) {
    tokens.push(solve(SITEKEY, PAGE));
  }
  return { tokens };
}

Die setupTimeout-Zeile ist wichtiger, als sie aussieht. k6 gibt setup standardmäßig 60 Sekunden, und ein einzelner reCAPTCHA-Lösevorgang kann davon allein schon das meiste verbrauchen. Zehn davon passen nicht hinein, die Phase wird abgebrochen, und die Fehlermeldung macht setup verantwortlich statt irgendetwas, wo Sie zuerst nachsehen würden.

Die Grenze, die Sie kennen sollten, bevor Sie darauf aufbauen

Tokens sind nur einmal verwendbar und bleiben etwa zwei Minuten gültig. Beide Hälften machen sich hier bemerkbar. Einmal verwendbar bedeutet, Sie brauchen mindestens so viele Tokens wie geschützte Requests, sodass ein Test, der den abgesicherten Endpunkt tausendmal trifft, tausend Lösevorgänge braucht und keinem Lasttest mehr ähnelt. Zwei Minuten bedeutet, dass die in setup gelösten Tokens schon altern, wenn der erste VU startet, sodass ein langer Dauerlauf die meiste Zeit damit verbringt, abgelaufene Tokens einzureichen.

Damit passt dieses Muster zu einem kurzen Burst gegen einen geschützten Endpunkt, nicht zu einem dreißigminütigen Dauerlauf. Der Beitrag zur Gültigkeitsdauer eines reCAPTCHA-Tokens hat die Zeitangaben parat. Wenn Sie dauerhafte Last durch das Widget benötigen, ist die Allowlist-Methode von vorhin der einzig ehrliche Weg dazu.

Schritt 3: den Solver aus Ihren Metriken heraushalten

Alles, was Sie mit dem http-Modul von k6 senden, landet in http_req_duration, einschließlich der Solver-Aufrufe. Ein p95, das stillschweigend eine fünfzehnsekündige Abfrage mit einrechnet, ist keine Zahl, mit der Sie etwas anfangen können. Versehen Sie die Requests, die Ihnen wichtig sind, mit einem Tag und richten Sie die Schwellenwerte auf dieses Tag aus.

export const options = {
  vus: 10,
  duration: '90s',
  setupTimeout: '5m',
  thresholds: {
    // Measure the app, not the solve.
    'http_req_duration{target:app}': ['p(95)<500'],
  },
};

export default function (data) {
  const token = data.tokens[__VU - 1];

  http.post(PAGE, { 'g-recaptcha-response': token }, {
    tags: { target: 'app' },
  });
}

Die Variable __VU nummeriert virtuelle Nutzer ab eins, sodass das Indizieren des Token-Arrays damit jedem VU sein eigenes Token gibt. Tagging ist die sauberere Lösung, als das Lösen irgendwohin zu verschieben, wo k6 es nicht sieht, denn die Löse-Requests tauchen weiterhin in der Ausgabe auf, wenn Sie wissen wollen, wie lange sie gedauert haben.

Wo der Löser stehen muss

Lastgeneratoren sind selten Ihr Desktop-Rechner. k6 läuft in CI, auf einer dedizierten Maschine oder in einem Managed Service, und die Loopback-Adresse bei keinem davon ist die Maschine, auf der Ihr Solver läuft.

ModusLauscht aufSinnvoll, wenn
Lokal127.0.0.1, nur dieses Gerätk6 und der Solver auf einer Maschine
ServerIhre Netzwerkadresse oder öffentliche IPCI, eine Flotte von Lastgeneratoren, ein Managed Runner

Der Server-Modus ist die Antwort auf alles in der zweiten Zeile: Ändern Sie die Listen-Adresse in der App, richten Sie das Skript auf diesen Host aus, und jeder Generator teilt sich einen Solver. Eine statische öffentliche IP wird empfohlen, wenn die Aufrufer außerhalb Ihres Netzwerks sitzen. Es bleibt Ihre eigene Hardware und bleibt unbegrenzt nutzbar, das verschiebt also nur, wo der Solver läuft, und sonst nichts. CapSkip ist eine Windows-Anwendung, also ein Windows-Rechner, den die Generatoren ansprechen. Ein praktischer Hinweis: Stellen Sie ihn nicht hinter denselben Load Balancer, den Sie testen, sonst messen Sie Ihren eigenen Engpass doppelt.

Häufige Fehler

Was Sie sehenUrsacheBeheben
Modul ‘capskip’ nicht gefundenk6 löst npm-Pakete nicht aufRufen Sie stattdessen die API mit dem http-Modul von k6 auf
Ausführung von setup() hat das Zeitlimit überschrittenLösevorgänge dauerten länger als der Standard von 60 SekundensetupTimeout so hoch setzen, dass es jeden Lösevorgang abdeckt
p95 ist riesig, dabei ist nichts langsamSolver-Aufrufe werden in http_req_duration mitgezähltAnwendungs-Requests taggen und den Schwellenwert filtern
Spätere Iterationen abgelehnt, frühe in OrdnungTokens sind über ihre Gültigkeitsdauer hinaus gealtertDen Lauf verkürzen oder später weniger lösen
Jeder VU erhält dieselbe AblehnungEin Token wird über VUs hinweg wiederverwendetPro VU einmal lösen und mit __VU indizieren

Die vollständige Liste der Codes und der Parameter, die jede Methode benötigt, finden Sie in der CapSkip-API-Dokumentation.

FAQ

Kann ich das CapSkip-SDK in k6 installieren?

Nein. k6 implementiert einen eigenen Modul-Loader, der eingebaute Module, lokale Dateien und entfernte Skripte handhabt und bewusst nicht dem Auflösungsalgorithmus von Node folgt, npm-Pakete fallen also weg. Die API ist 2captcha-kompatibel, sodass die beiden Endpunkte alles abdecken, was das SDK für Sie erledigt hätte, in etwa fünfzehn Zeilen mit dem eigenen http-Modul von k6.

Sollte ich stattdessen innerhalb der default-Funktion lösen?

Nur wenn das Lösen selbst das ist, was Sie testen. Die default-Funktion läuft einmal pro Iteration pro VU, sodass zwanzig VUs über zwei Minuten hunderte Lösevorgänge ergeben, und Ihre Latenzzahlen werden zu einer Messung des Pollings. Lösen Sie in setup, verteilen Sie die Tokens, und beschränken Sie die Iteration auf den Request, um den es Ihnen wirklich geht.

Mein k6 läuft auf einem Managed Service. Kann es den Solver erreichen?

Ja, im Server-Modus. Der Solver lauscht auf einer Netzwerkadresse statt auf der Loopback-Adresse, und die Generatoren rufen ihn über die API auf wie jeden anderen internen Dienst. Eine statische öffentliche IP macht das stabil, wenn die Runner außerhalb Ihres Netzwerks sitzen. Aktivieren Sie die Schlüsselvalidierung und geben Sie jeder Umgebung einen eigenen Schlüssel, damit sich einer widerrufen lässt, ohne die anderen anzutasten.

Wie führe ich einen einstündigen Lasttest durch ein Widget durch?

Gar nicht, zumindest nicht mit echten Tokens. Einmal verwendbar plus zwei Minuten Gültigkeitsdauer bedeutet, dass eine Stunde dauerhafter Last einen ununterbrochenen Strom von Lösevorgängen braucht, und an dem Punkt ist der Solver das System, das getestet wird. Nehmen Sie für einen langen Lauf auf Ihrer eigenen Anwendung den Lastgenerator von der Challenge aus und testen Sie den Pfad dahinter.

Die Kurzfassung

Verwenden Sie das http-Modul von k6 gegen in.php und res.php, lösen Sie in setup mit erhöhtem setupTimeout, versehen Sie Ihre Anwendungsanfragen mit Tags, damit die Schwellenwerte aussagekräftig bleiben, und halten Sie den Lauf kurz genug, dass die Tokens noch gültig sind. Für die Crawling-Seite davon siehe die Seite zum Captcha-Solver für Web Scraping. Beim Lasttest wird eine Abrechnung pro Lösung am schnellsten absurd, weil ein einzelner Burst Hunderte von Tokens benötigen kann, die keinerlei geschäftlichen Wert erzeugen. Das Preismodell macht den ganzen Unterschied: Captcha-Umgehung auf Hardware, die Sie bereits besitzen, kostet genauso viel, ob Sie einen Test oder vierzig durchführen.