So lösen Sie ALTCHA in Node.js und senden den Token mit fetch ab

solve altcha in node.js - How to Solve ALTCHA in Node.js and Submit It with Fetch

Sie können ALTCHA in Node.js mit einem einzigen Aufruf lösen, ganz ohne Browser im Stack. ALTCHA ist Proof of Work und keine Erkennung: Die Website gibt eine Challenge heraus, und der Client muss so lange hashen, bis er den Zähler findet, der sie erfüllt. Es muss nichts angeschaut werden, also sind kein WebDriver, kein Headless-Chrome und kein User-Agent beteiligt, und die Antwort wird berechnet und nicht geraten. CapSkip hat den Typ in Version 1.2.6 hinzugefügt, und das Node SDK stellt ihn als eine einzige Methode bereit. Das macht ihn zu dem seltenen Captcha-Typ, bei dem der ganze Durchlauf ein gewöhnliches HTTP-Skript ist: Seite abrufen, die Challenge daraus lesen, lösen, den Token zurücksenden, alles mit dem globalen fetch und einem einzigen SDK-Aufruf.

Was Sie brauchen

  • CapSkip 1.2.6 oder neuer auf einem Windows-Rechner. Die ALTCHA-Unterstützung kam mit diesem Release.
  • Node 18 oder neuer, was das Paket voraussetzt und woher auch das unten verwendete globale fetch kommt. Die TypeScript-Definitionen stecken im Paket, es gibt also kein zusätzliches types-Paket zu installieren.
  • Die URL der Seite, auf der das Widget sitzt, sowie der Endpunkt, von dem das Widget seine Challenge abruft.
  • Eine Adresse für den Solver. Der Local-Modus antwortet auf 127.0.0.1 nur für dieses Gerät; der Server-Modus lauscht auf Ihrer Netzwerkadresse oder öffentlichen IP, damit ein anderer Rechner ihn erreichen kann. Schritt 4 erklärt, welcher davon gilt, und beide finden Sie unter Verbindungseinstellungen.
# npm install capskip
npm install capskip

Schritt 1: Der Aufruf zum Lösen und woher die Challenge kommt

Eine Methode, zwei Argumente: die Seiten-URL, dann ein Options-Objekt, das die Challenge enthält. Geben Sie ihm den Endpunkt, und CapSkip ruft die Challenge selbst ab.

// npm install capskip
const { CapSkip } = require('capskip');

const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });

// CapSkip fetches the challenge, then hashes until the counter fits.
const result = await solver.altcha('https://example.com/signup', {
  challengeUrl: 'https://example.com/altcha/challenge',
});

console.log(result.token);   // base64 payload for the form field
console.log(result.number);  // the counter that satisfied it

Zwei Felder des Ergebnisses gibt es nur bei ALTCHA. token ist die base64-Payload, die das Formular erwartet, und number ist der Zähler, der die Challenge gelöst hat. Das Feld code enthält dieselbe Zeichenfolge wie token, beides funktioniert also, aber token ist nach dem Feld benannt, in das es gehört, und liest sich an der Aufrufstelle besser. Die GeeTest-Felder und der Turnstile-User-Agent fehlen hier.

Für den Optionsnamen gibt es mehr als eine akzeptierte Schreibweise. Sowohl challengeUrl als auch challenge_url erreichen denselben API-Parameter, und für challengeJson und challenge_json gilt dasselbe. Die Camel-Case-Schreibweise ist die, die in den Node-Docs steht und zum Rest des SDK passt, bevorzugen Sie sie also und bleiben Sie konsistent; die Snake-Case-Aliase gibt es, damit ein aus der PHP- oder Python-Anleitung kopiertes Beispiel trotzdem läuft.

Den Endpunkt finden, den das Widget abruft

Öffnen Sie die DevTools, wechseln Sie auf den Tab Network und laden Sie die Seite neu, auf der das Widget sitzt. Das Widget stellt eine Anfrage für seine Challenge, meist an einen Pfad, der altcha enthält. Diese Anfrage-URL ist das, was Sie übergeben, und das JSON, das sie zurückgibt, ist das Challenge-Dokument, das Sie stattdessen übergeben können.

Raten Sie nicht, welches Attribut sie benennt, denn das hat sich zwischen den Widget-Generationen geändert. Lesen Sie den Seitenquelltext.

Widget-GenerationAttribut, das die Challenge benennt
v1 und v2challengeurl für einen Endpunkt, mit einem separaten Attribut challengejson für eine Inline-Challenge
v3 und neuerchallenge, und dasselbe Attribut nimmt entweder eine URL oder die Challenge-Daten
<!-- v1 and v2 name the endpoint on its own attribute -->
<altcha-widget challengeurl="https://example.com/altcha/challenge"></altcha-widget>

<!-- v3 and later put both forms behind one attribute -->
<altcha-widget challenge="https://example.com/altcha/challenge"></altcha-widget>

Die drei Darstellungsvarianten native, checkbox und switch sind rein optisch. Alle senden dieselbe Payload, und der Unterschied erreicht den Solver nie, Sie müssen also nicht herausfinden, welche davon Sie vor sich haben. Für die Attribute selbst pflegt ALTCHA die eigene Integrationsanleitung.

Stattdessen das Challenge-Dokument übergeben

Wenn Ihr Scraper die Challenge bereits von der Seite gelesen hat, übergeben Sie das Dokument, und es findet überhaupt keine Netzwerkanfrage statt.

// No fetch happens: the document is already here.
const result = await solver.altcha('https://example.com/signup', {
  challengeJson: {
    algorithm: 'SHA-256',
    challenge: 'YOUR_CHALLENGE_HASH',
    salt: 'YOUR_SALT',
    signature: 'YOUR_SIGNATURE',
    maxnumber: 1000000,
  },
});

Diese Option nimmt ein Objekt, das für Sie serialisiert wird, oder einen JSON-String, wenn Sie schon einen haben. Beides zu senden, den Endpunkt und das Dokument, ist erlaubt, und das Inline-Dokument gewinnt, weil ein Abruf nur noch einmal holen würde, was Sie gerade geliefert haben. Unter Last verhalten sich die beiden Wege allerdings unterschiedlich. Eine Inline-Challenge, die bereits abgelaufen ist, wird sofort abgelehnt, statt sinnlos gehasht zu werden, während ein Endpunkt dem Solver erlaubt, eine frische Challenge zu holen, falls die erste gestorben ist, während der Auftrag in der Warteschlange lag.

Welche Algorithmen der Solver abdeckt

Dieselbe Methode bedient beide Generationen. Das Legacy-Verfahren ist mit SHA-1, SHA-256, SHA-384 und SHA-512 abgedeckt, und Proof of Work v2 ist mit PBKDF2 und iterativem SHA abgedeckt. PBKDF2 ist der Standard, den ALTCHA selbst empfiehlt, die abgedeckte Menge ist also die große Mehrheit der Live-Websites.

Argon2id und scrypt sind die Ausnahmen, und sie werden abgelehnt statt versucht: Ein Task, der eines von beiden nutzt, kommt nach etwa einer Drittelsekunde mit ERROR_CAPTCHA_UNSOLVABLE zurück und wird nie wiederholt. Das ist so gewollt. Eine speicherharte Funktion lässt sich durch einen erneuten Versuch nicht beheben, deshalb ist sofortiges Scheitern besser, als beschäftigt zu wirken. Bei ALTCHA deutet dieses Ergebnis auf den Algorithmus und nicht auf ein unlesbares Bild.

Schritt 2: Den ganzen Durchlauf mit fetch erledigen, ohne Browser

Weil es nichts zu rendern gibt, ist die Seite, von der Sie die Challenge brauchen, nur ein Dokument, das Sie abrufen können. Das ist es wert, deutlich gesagt zu werden, denn bei jedem Widget-Captcha-Typ gehört zur ehrlichen Antwort irgendwo ein Browser. Hier nicht. Rufen Sie die Seite ab, ziehen Sie das Attribut aus dem Markup und übergeben Sie es direkt an den Solver.

// npm install capskip
const PAGE = 'https://example.com/signup';

// The page is only a document here: no browser, no rendering.
const html = await (await fetch(PAGE)).text();

// v1 and v2 use challengeurl; v3 and later use challenge.
const found = html.match(/(?:challengeurl|challenge)="([^"]+)"/i);
if (!found) throw new Error('no ALTCHA widget on this page');

const result = await solver.altcha(PAGE, { challengeUrl: found[1] });

Ein regulärer Ausdruck genügt für eine einzelne bekannte Seite und ist für einen Crawler eine schlechte Idee, greifen Sie also zu einem echten HTML-Parser, sobald Sie Markup verarbeiten, das Sie nicht selbst geschrieben haben. Der Sinn des Beispiels ist die Form und nicht das Parsen: eine Anfrage, eine Zeichenfolge, ein Lösungsvorgang, und kein Prozess, der gestartet und wieder abgebaut werden muss. Auch deshalb macht sich dieser Typ in einer Serverless Function oder einem kurzlebigen Worker gut, wo der Start von Chromium den Lösungsvorgang in den Schatten stellen würde.

Ein Vorbehalt zum v3-Attribut. Es enthält entweder eine URL oder das Challenge-Dokument selbst, prüfen Sie also, was davon Sie bekommen haben, bevor Sie es übergeben. Beginnt der Wert mit einer geschweiften Klammer statt mit einem Schema, ist es eine Inline-Challenge, und sie gehört stattdessen in die Dokument-Option aus dem vorigen Abschnitt.

Das Ergebnis typisieren, wenn Sie mit TypeScript arbeiten

Die Definitionen stecken im Paket, es gibt also kein types-Paket zu installieren. Ein einziger Ergebnistyp deckt jeden Captcha-Typ ab, den das SDK löst, also ist jedes Feld, das nur zu einem davon gehört, als optional deklariert. token und number sind ALTCHA-Felder, der Compiler typisiert token daher als string oder undefined und lässt Sie ihn nicht an etwas übergeben, das einen einfachen string erwartet.

// npm install capskip
import { CapSkip, SolveResult, AltchaOptions } from 'capskip';

const options: AltchaOptions = { challengeUrl: found[1] };
const result: SolveResult = await solver.altcha(PAGE, options);

// One check, right after the call, and the type is settled.
if (!result.token) throw new Error('no ALTCHA token on this result');

const token: string = result.token;

Denselben Hinweis gibt Ihnen der Turnstile-User-Agent, siehe die Node.js-Turnstile-Anleitung, allerdings mit schärferen Folgen: Ein fehlender User-Agent kostet Sie ein abgelehntes Absenden, während ein fehlender Token bedeutet, dass Sie überhaupt nichts abzusenden haben. Greifen Sie nur dann zur Non-Null-Assertion, wenn Sie sicher sind, denn sie bringt genau die eine Prüfung zum Schweigen, die Ihnen sagt, dass die falsche Methode aufgerufen wurde.

Eines fangen die Typen nicht ab. Das Options-Interface trägt eine Index-Signatur, jeder zusätzliche Schlüssel, den Sie schreiben, wird also vom Compiler akzeptiert. Eine falsch geschriebene Option baut deshalb sauber und scheitert dann beim Ausführen, weil das SDK einen Parameter ablehnt, den ALTCHA nicht annimmt. Das Options-Objekt wie oben zu annotieren, prüft wenigstens die Schlüssel, die es kennt.

Schritt 3: Den Token unverändert zurücksenden, bevor er abläuft

Das Widget sendet seine Payload in einem Formularfeld namens altcha, dort gehört also Ihr Token hin. Das ist der Schritt, der unauffällig kaputtgeht.

// Send it exactly as it came back: no trimming,
// no re-encoding, no reordering.
const response = await fetch('https://example.com/signup', {
  method: 'POST',
  body: new URLSearchParams({
    email: '[email protected]',
    altcha: token,
  }),
});

Der Token ist base64 eines JSON-Dokuments, dessen Felder von der HMAC-Signatur des Servers abgedeckt werden. Jede Änderung macht ihn ungültig, alles, was nach Aufräumen aussieht, zerstört also das Absenden: Leerzeichen abschneiden, ihn dekodieren und neu kodieren oder das JSON mit den Schlüsseln in anderer Reihenfolge neu aufbauen. Manche Integrationen lesen die Payload aus einem JSON-Body-Feld statt aus einem Formularfeld, prüfen Sie also, was das Absenden der Seite selbst sendet, und machen Sie es genauso.

Die andere Art, wie dieser Schritt scheitert, ist das Timing. Challenge-Fenster sind kurz, und manche Websites schließen sie innerhalb von zwei Minuten. Wenn eines abläuft, weist die Website die Antwort mit einem nackten Verifizierungsfehler ab, der genauso aussieht wie eine falsche Antwort, und in der Antwort steht nichts, was Ihnen sagt, welcher der beiden Fälle eingetreten ist. Drei Gewohnheiten verhindern das: Rufen Sie die Challenge unmittelbar vor dem Lösen ab und nicht am Anfang eines langen Durchlaufs, senden Sie den Token in derselben Arbeitseinheit ab, die ihn gelöst hat, und halten Sie niemals einen Token, während ein Mensch ein Formular ausfüllt.

Die Polling-Timeouts des Clients sind hier nicht das, was Sie begrenzt, denn das Challenge-Fenster schließt sich lange, bevor eines von beiden greift. ALTCHA ist CPU-Arbeit und keine Browser-Sitzung, es läuft also auf dem Standard-Polling-Timeout und nicht auf dem längeren für reCAPTCHA.

Konstruktor-OptionStandardWas sie abdeckt
defaultTimeout120 SekundenPolling für ALTCHA und Bild-Captchas
recaptchaTimeout300 SekundenPolling für reCAPTCHA, Turnstile und GeeTest
pollingIntervalMaximal 5 SekundenDas Polling beginnt bei 0,25 Sekunden und steigt per Backoff bis auf diesen Wert

Schritt 4: Wo der Solver läuft und welchen Verbindungsmodus das erfordert

Die Beispiele oben verwenden 127.0.0.1, weil das richtig ist, wenn Ihr Node-Prozess und der Solver auf demselben Rechner liegen. Sobald der aufrufende Code woanders läuft, etwa in einem Container, auf einem CI-Runner, einem VPS oder bei einem Managed-Host, zeigt Loopback nicht mehr auf den Solver, und der erste Lösungsversuch wird mit einer NetworkException abgewiesen.

Schalten Sie CapSkip in den Server-Modus, dann lauscht er stattdessen auf Ihrer Netzwerkadresse oder öffentlichen IP, sodass jede dieser Umgebungen ihn über dieselbe HTTP-API erreichen kann. Eine statische öffentliche IP ist empfehlenswert, wenn der Weg über das Internet geht, zusammen mit einer Firewall-Regel, die nur die erwarteten Adressen zulässt. Der Server-Modus ändert nur, wo der Solver lauscht, und sonst nichts: Es bleibt Ihre Hardware, und es bleibt ohne Abrechnung pro Lösung. Lesen Sie Host und Port aus der Umgebung, damit ein Build an beiden Orten funktioniert. Der Client liest weder CAPSKIP_HOST noch CAPSKIP_PORT von sich aus, übergeben Sie beide Werte also an den Konstruktor, wie es das vollständige Beispiel unten tut.

Wo der Node-Prozess läuftWelcher Verbindungsmodus
Auf dem CapSkip-Rechner, als Skript oder als lokaler ServerLocal-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 Container, auf einem VPS oder auf einer Managed-PlattformServer-Modus mit einer statischen öffentlichen IP und einer Firewallregel

Ein ALTCHA-spezifischer Hinweis zu Proxys. Ein Proxy wird hier unterstützt, aber nur für den Abruf der Challenge verwendet. Es gibt keine Browser-Sitzung, die geroutet werden müsste, er hat also keinen Einfluss auf den Proof of Work selbst.

Vollständiges lauffähiges Beispiel

// npm install capskip
import { CapSkip, ApiException, TimeoutException, NetworkException } from 'capskip';

const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST || '127.0.0.1',
  port: Number(process.env.CAPSKIP_PORT || 8080),
});

export async function signUp(email: string) {
  try {
    // Fetch, solve and submit in one unit of work.
    const result = await solver.altcha('https://example.com/signup', {
      challengeUrl: 'https://example.com/altcha/challenge',
    });

    if (!result.token) throw new Error('not an ALTCHA result');

    const response = await fetch('https://example.com/signup', {
      method: 'POST',
      body: new URLSearchParams({ email, altcha: result.token }),
    });

    console.log(response.status, 'after counter', result.number);
  } catch (err) {
    // ERROR_CAPTCHA_UNSOLVABLE here means Argon2id or scrypt.
    if (err instanceof ApiException) console.log('refused:', err.message);
    else if (err instanceof TimeoutException) console.log('gave up waiting');
    else if (err instanceof NetworkException) console.log('solver unreachable');
    else throw err;
  }
}

Die anderen Typen haben dieselbe Form mit einer anderen Methode. Der reCAPTCHA-Aufruf nimmt einen sitekey und eine Seiten-URL, Turnstile funktioniert genauso, GeeTest nimmt neben der Seiten-URL einen gt-Wert und eine Challenge, und das Lösen von Bildern nimmt einen Dateipfad, eine URL oder base64. Für die vollständige Methodenliste siehe die Node.js-Captcha-Solver-Seite, und dieselben Methoden gibt es in jedem offiziellen Paket auf der SDK-Seite.

Turnstile ist der eine Typ, der mehr als einen sitekey braucht, wenn er als vollständige Challenge-Seite ankommt. Seine zusätzlichen Werte behandelt die Node.js-Turnstile-Anleitung.

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
Der Compiler weist den Token ab und sagt, string oder undefined sei kein stringEin einziger Ergebnistyp deckt jeden Captcha-Typ ab, deshalb sind die reinen ALTCHA-Felder optionalGrenzen Sie ihn einmal nach dem Lösen ein und verwenden Sie dann den eingegrenzten Wert
Eine falsch geschriebene Option baut sauber und scheitert erst beim AusführenDas Options-Interface hat eine Index-Signatur, unbekannte Schlüssel kommen also durchAnnotieren Sie das Options-Objekt mit dem ALTCHA-Options-Typ und prüfen Sie die Schreibweise
Der Token ist zur Laufzeit undefinedDieses Feld wird nur bei ALTCHA gefülltRufen Sie die ALTCHA-Methode auf. Bei einem ALTCHA-Ergebnis enthält das Feld code dieselbe Zeichenfolge
Ein nackter Verifizierungsfehler von der Website, bei einem Token, der in Ordnung aussiehtDie Challenge ist abgelaufen, bevor das Formular abgesendet wurdeAbrufen, lösen und absenden in einer Arbeitseinheit
ERROR_CAPTCHA_UNSOLVABLE in einer ApiException, nach etwa einer DrittelsekundeDie Challenge verwendet Argon2id oder scryptNichts zu wiederholen. Diese beiden werden bewusst abgelehnt
Eine ValidationException beim AufrufKeine der beiden Challenge-Optionen wurde übergeben, oder es wurde eine Option übergeben, die ALTCHA nicht annimmtÜbergeben Sie den Challenge-Endpunkt oder das Challenge-Dokument und lassen Sie alles andere weg
Eine NetworkException beim ersten LösenCapSkip läuft nicht, oder Host und Port sind falschStarten Sie CapSkip und prüfen Sie dann, ob es im Local-Modus oder im Server-Modus laufen soll
Das Formular weist einen Token ab, den Ihre Logs als gelöst ausweisenIrgendetwas hat die Payload neu kodiert, beschnitten oder umsortiertGeben Sie die Zeichenfolge unverändert direkt weiter

FAQ

Brauche ich Puppeteer oder Playwright für eine ALTCHA-Seite?

Nein, und genau das ist das Praktische daran. ALTCHA gibt ein Hashing-Problem heraus und nicht etwas zum Anschauen, die Arbeit ist also reine CPU-Arbeit und in Millisekunden fertig. Es sind kein Browser, kein WebDriver und kein User-Agent beteiligt. Ein einfaches Skript mit dem globalen fetch genügt, was auch bedeutet, dass es problemlos in einem Worker, einem Queue-Consumer oder einer Serverless Function läuft, wo der Start von Chromium langsam und unpraktisch wäre.

Kann eine Node-App auf einer gehosteten Plattform den Solver erreichen?

Ja. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, damit er auf einer Netzwerkadresse statt auf Loopback lauscht, und richten Sie dann die Host-Umgebungsvariable auf diese Adresse. Ein Container, ein CI-Runner, ein VPS oder eine Managed-App-Plattform verbinden sich alle auf dieselbe Weise, über dieselbe HTTP-API. Verwenden Sie eine statische öffentliche IP, wenn der Weg über das Internet führt, und beschränken Sie sie mit einer Firewall-Regel. Der Solver bleibt in jedem dieser Fälle auf Hardware, die Ihnen gehört, an der Lizenz und der Zahl der Lösungen ändert sich also nichts.

Löst der async-Client mehrere ALTCHA-Challenges schneller?

Nicht von allein. Im Node-Paket ist der async-Client ein Alias des gewöhnlichen und keine zweite Implementierung, ihn zu importieren ändert also nichts daran, wie die Arbeit erledigt wird. Jede Methode gibt ohnehin ein Promise zurück, Nebenläufigkeit entsteht also dadurch, dass Sie mehrere davon zusammen starten und das Ganze abwarten. Halten Sie dabei jeden Abruf bei seinem eigenen Lösungsvorgang, denn Challenges laufen unabhängig voneinander ab, und ein im Voraus abgerufener Batch veraltet, während die ersten noch hashen.

Muss ich TypeScript verwenden, um das SDK zu nutzen?

Nein. Die Definitionen stecken im Paket, sie sind also da, wenn Ihr Projekt sie liest, und unsichtbar, wenn nicht. Einfaches CommonJS funktioniert genau so wie im ersten Beispiel gezeigt, und der einzige Unterschied ist, dass aus dem optionalen Token eine Laufzeitprüfung wird, die Sie selbst schreiben, statt einer, auf der der Compiler besteht. Die Prüfung lohnt sich so oder so, denn ein undefinierter Token ist das deutlichste Zeichen dafür, dass die falsche Methode aufgerufen wurde.

Die Kurzfassung

Lesen Sie den Challenge-Endpunkt vom Widget ab, übergeben Sie ihn zusammen mit der Seiten-URL an die eine ALTCHA-Methode und senden Sie den Token unverändert in das Feld namens altcha zurück. Grenzen Sie den Token in einem typisierten Projekt einmal nach dem Lösen ein, denn ein einziger Ergebnistyp deckt jeden Captcha-Typ ab, und die ALTCHA-Felder sind darauf optional. Halten Sie Abruf, Lösen und Absenden im selben Block, denn das Challenge-Fenster kann sich innerhalb von zwei Minuten schließen, und eine abgelaufene Challenge sieht genauso aus wie eine falsche Antwort. Wechseln Sie in den Server-Modus, sobald der Node-Prozess nicht mehr auf demselben Rechner wie der Solver liegt.

Noch eine letzte Sache, die verändert, wie Sie Wiederholungen gestalten. Weil ein unbegrenzter Captcha-Löser den Proof of Work auf einem Rechner berechnet, der Ihnen bereits gehört, kostet der erneute Versuch bei einer abgelaufenen Challenge nur ein paar Millisekunden eigener CPU-Zeit und sonst nichts, Sie können sich also erlauben, eine frische Challenge zu holen, statt eine veraltete weiterzuschleppen.