So bauen Sie einen Minimal-API-Captcha-Solver-Endpunkt in C#

Um einen Minimal-API-Captcha-Solver zu bauen, registrieren Sie in ASP.NET Core einen einzigen CapSkipClient als Singleton. Stellen Sie einen POST-Endpunkt bereit, der einen sitekey und eine Seiten-URL entgegennimmt. Umhüllen Sie jeden Aufruf mit einer Frist und einer SemaphoreSlim und ordnen Sie die vier Exceptions des SDK HTTP-Statuscodes zu. Ihre Scraper, Worker und Skripte in jeder Sprache rufen dann eine einzige interne Adresse auf. Sie sehen nie den Port oder den Schlüssel von CapSkip. CapSkip läuft auf Ihrem eigenen Windows-Rechner, daher fällt auch beim Dienst keine Gebühr pro Lösung an. Dieser Leitfaden baut den Dienst in etwa 120 Zeilen und zeigt, was jeder Teil tut, wenn CapSkip langsam, geschlossen oder stumm ist.
Was Sie brauchen
- CapSkip läuft auf einem Windows-PC, mit eingeschaltetem reCAPTCHA-Löser. Die Einrichtungsanleitung führt durch den ersten Start.
- Das .NET 10 SDK. Alles unten lief unter Windows 11 mit SDK 10.0.100, ASP.NET Core 10.0.0 und dem NuGet-Paket CapSkip 1.3.0.
- Eine PowerShell-Eingabeaufforderung. Die Befehle verwenden PowerShell-Syntax.
- Eine Seite mit einer reCAPTCHA-v2-Checkbox. Der Dienst braucht ihren sitekey und ihre Adresse, sonst nichts über die Seite.
- Eine Adresse für CapSkip. Im Local-Modus antwortet es auf 127.0.0.1 nur für dieses Gerät. Im Server-Modus lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, damit ein Dienst auf einem anderen Rechner es über die API aufrufen kann. Eingestellt werden beide Modi unter Verbindungseinstellungen.
Die Zahlen in diesem Leitfaden stammen von einem Mock-CapSkip (einer nachgebauten Testattrappe) auf demselben PC. ASP.NET Core und das SDK sind die echten. Der Mock hält jeden Lösevorgang eine festgelegte Zeit, zwei Sekunden, sofern ein Test nichts anderes sagt, und echte Lösezeiten weichen davon ab. Nichts hier wurde gegen ein echtes CapSkip oder eine echte Website ausgeführt.
Warum ein Dienst vor CapSkip sinnvoll ist
Löst nur eine C#-App Captchas, rufen Sie das SDK direkt aus dieser App auf. Die C#-Captcha-Solver-Seite behandelt das. Ein Dienst lohnt sich, wenn mehrere Aufrufer ein CapSkip teilen. Dann gehören vier Aufgaben an einen Ort.
- Eine Frist. Das .NET SDK pollt standardmäßig bis zu 300 Sekunden, daher bräuchte jeder Aufrufer einen eigenen Timer.
- Ein Limit. CapSkip führt eine feste Zahl von Lösevorgängen gleichzeitig aus, und weitere Aufgaben warten. Eine Warteschlange, die Sie selbst kontrollieren, lässt sich leichter durchschauen als eine Warteschlange innerhalb von CapSkip.
- Ein Schlüssel. Aufrufer erhalten einen Dienstschlüssel. Adresse und API-Schlüssel von CapSkip bleiben in den Einstellungen des Dienstes.
- Jede Sprache. Ein Python-Skript, eine PHP-Website und eine PowerShell-Aufgabe sprechen alle HTTP und JSON.
Schritt 1: Das Projekt anlegen
Die Web-Vorlage ist das leere ASP.NET-Core-Projekt: eine Program.cs, keine Controller. Legen Sie das Projekt in einen eigenen Ordner. Die Paketinstallation schreibt nur in die Projektdatei.
# "web" is the empty ASP.NET Core template. This guide uses .NET SDK 10. dotnet new web -n CapSkipSolver -f net10.0 cd CapSkipSolver dotnet add package CapSkip
Schritt 2: Die Einstellungen in appsettings.json ablegen
Der Abschnitt CapSkip enthält die Adresse, den API-Schlüssel für CapSkip, die zu CapSkip passende Thread-Zahl und die Frist in Sekunden. Der Abschnitt Solver enthält den Schlüssel, den Ihre Aufrufer senden. Ersetzen Sie beide Platzhalter, bevor Sie irgendetwas ausführen.
{
"CapSkip": {
"Host": "127.0.0.1",
"Port": 8080,
"ApiKey": "YOUR_API_KEY",
"MaxThreads": 10,
"DeadlineSeconds": 120
},
"Solver": {
"ApiKey": "YOUR_SERVICE_KEY"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}Jeder Wert lässt sich durch eine Umgebungsvariable überschreiben. Ein doppelter Unterstrich steht für den Doppelpunkt, also überschreibt CapSkip__Port den Wert CapSkip:Port. Der Abschnitt Betrieb des Dienstes getrennt von CapSkip nutzt das, um den Dienst auf einen anderen Rechner zu richten, ohne die Datei zu bearbeiten.
Schritt 3: Die Einstellungen lesen und ohne Schlüssel den Start verweigern
Ersetzen Sie den gesamten Inhalt der Program.cs aus der Vorlage durch die folgenden Blöcke und fügen Sie jeden neuen Block unter dem vorigen ein. Das Projekt lässt sich erst nach Schritt 10 bauen, weil die Records und Hilfsfunktionen zuletzt kommen.
Am Anfang der Program.cs wird der Abschnitt CapSkip an einen Record gebunden und der Dienstschlüssel gelesen. Die Schlüsselprüfung nutzt IsNullOrWhiteSpace, nicht eine Null-Prüfung mit throw. Beim Mock bestand ein leerer String eine Null-Prüfung, der Dienst startete und akzeptierte dann jede Anfrage, die einen leeren Header sendete.
Dieser Block enthält auch die eine .NET-spezifische Falle. System und CapSkip definieren beide eine Klasse namens TimeoutException. Mit impliziten Usings und using CapSkip lässt sich ein unqualifiziertes TimeoutException nicht kompilieren. Ein switch-Zweig auf System.TimeoutException kompiliert zwar, trifft aber nie die Exception des SDK. Im Dienst schickte das einen SDK-Timeout in den Auffangzweig und lieferte 500 statt 504. Die letzte using-Zeile gibt der Klasse des SDK einen eigenen Namen.
// Program.cs
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using CapSkip;
// System and CapSkip both define TimeoutException, so give the SDK's
// one its own name.
using SolverTimeout = CapSkip.TimeoutException;
var builder = WebApplication.CreateBuilder(args);
// Reads the "CapSkip" section of appsettings.json. The environment
// variable CapSkip__Port overrides CapSkip:Port (__ stands for a colon).
var cfg = builder.Configuration.GetSection("CapSkip").Get<CapSkipSettings>()
?? new CapSkipSettings();
var serviceKey = builder.Configuration["Solver:ApiKey"];
if (string.IsNullOrWhiteSpace(serviceKey))
throw new InvalidOperationException("Set Solver:ApiKey before starting.");Schritt 4: Einen Client und eine Schleuse registrieren
CapSkipClient lässt sich gefahrlos als Singleton verwenden. Im Quelltext des SDK hält er keinen Zustand pro Aufruf, er kopiert bei jedem Aufruf das Optionen-Dictionary, und alle Instanzen teilen einen statischen HttpClient. Es gibt nichts freizugeben, und IHttpClientFactory ist nicht nötig. Das SDK nutzt einen privaten statischen HttpClient und übernimmt Ihren nicht. Eine langlebige Verbindung würde eine DNS-Änderung nicht bemerken, verwenden Sie daher für den CapSkip-Host eine IP-Adresse.
Die SemaphoreSlim ist die Schleuse. Geben Sie ihr einen Slot pro CapSkip-Thread, also die Einstellung Max. Threads in CapSkip (Standardwert 10). AddProblemDetails zusammen mit UseStatusCodePages sorgt dafür, dass die Antworten 400, 404, 405 und 415, die ASP.NET Core selbst schreibt, im selben Format zurückkommen wie unsere. Ohne UseStatusCodePages hatten diese Antworten keinen Body.
builder.Services.AddSingleton(cfg);
// One client for the whole process. It keeps no per-call state.
builder.Services.AddSingleton(new CapSkipClient(
cfg.ApiKey, cfg.Host, cfg.Port, recaptchaTimeout: cfg.SdkTimeoutSeconds));
// One slot per CapSkip thread (Max. Threads in the CapSkip settings).
builder.Services.AddSingleton(new SemaphoreSlim(cfg.MaxThreads));
// With these two calls, 400, 404, 405 and 415 also come back as
// ProblemDetails.
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseStatusCodePages();Schritt 5: Den Dienstschlüssel prüfen
Ein ungeschützter Endpunkt zum Lösen auf einer Netzwerkadresse ist ein kostenloser Solver für jeden, der den Port erreichen kann. Diese Middleware schützt den Pfad /solve. FixedTimeEquals vergleicht die Bytes in derselben Zeit, egal wo sie sich unterscheiden, und gibt bei unterschiedlicher Länge false zurück, statt eine Exception zu werfen. Ein fehlender, ein falscher und ein zu langer Schlüssel bekamen auf dem Mock alle 401.
Es gibt 401 zurück, nicht 403, weil es eine einzige Anmeldeinformation gibt und sie fehlt oder falsch ist. Die Middleware läuft vor dem Model Binding, daher bekommt eine Anfrage ohne Schlüssel und mit kaputtem JSON-Body 401, nicht 400.
// Only callers that know the service key may use /solve. FixedTimeEquals
// takes the same time wherever the first wrong byte is.
var expected = Encoding.UTF8.GetBytes(serviceKey);
app.Use(async (ctx, next) =>
{
var sent = Encoding.UTF8.GetBytes(ctx.Request.Headers["X-API-Key"].ToString());
if (ctx.Request.Path.StartsWithSegments("/solve")
&& !CryptographicOperations.FixedTimeEquals(sent, expected))
{
await Fail(401, "Missing or wrong X-API-Key").ExecuteAsync(ctx);
return;
}
await next(ctx);
});Schritt 6: Einen Health-Check hinzufügen
Das SDK hat keinen Ping-Aufruf, daher öffnet der Health-Endpunkt eine TCP-Verbindung zum Port von CapSkip und gibt nach drei Sekunden auf. Bei geschlossenem CapSkip antwortete er nach etwa zwei Sekunden mit 503, weil Windows eine Loopback-Verbindung so langsam verweigert. Er sagt einem Load Balancer oder einem Überwachungsskript, dass der Dienst läuft und CapSkip nicht. Er prüft nicht, ob ein Lösevorgang funktioniert.
// CapSkip has no ping call, so check that its port accepts a connection.
app.MapGet("/health", async (CapSkipSettings s, CancellationToken ct) =>
{
using var probe = CancellationTokenSource.CreateLinkedTokenSource(ct);
probe.CancelAfter(TimeSpan.FromSeconds(3));
try
{
using var tcp = new TcpClient();
await tcp.ConnectAsync(s.Host, s.Port, probe.Token);
return Results.Ok(new { status = "ok" });
}
catch (Exception ex) when (ex is SocketException or OperationCanceledException)
{
return Fail(503, "CapSkip is not reachable");
}
});Schritt 7: Die Anfrage validieren und die Frist starten
Ein Record-Parameter macht ein fehlendes Feld nicht zum Fehler. System.Text.Json lässt die Eigenschaft auf null, daher prüft der Handler sitekey und URL selbst und antwortet mit einem Validierungsproblem.
Die Frist ist eine verknüpfte CancellationTokenSource. Sie wird abgebrochen, wenn der Aufrufer die Verbindung beendet (RequestAborted) oder wenn die Frist abläuft (CancelAfter). Jeder spätere Schritt übernimmt ihren Token, es gibt also nur eine Uhr, auf die man schauen muss.
app.MapPost("/solve", async (SolveRequest req, CapSkipClient solver,
SemaphoreSlim gate, CapSkipSettings s, HttpContext http) =>
{
if (string.IsNullOrWhiteSpace(req.Sitekey)
|| !Uri.IsWellFormedUriString(req.Url, UriKind.Absolute))
return Results.ValidationProblem(new Dictionary<string, string[]>
{
["request"] = ["sitekey and an absolute url are required"]
});
// One token for the whole request. It is cancelled when the caller
// hangs up (RequestAborted) or when the deadline passes.
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(
http.RequestAborted);
deadline.CancelAfter(TimeSpan.FromSeconds(s.DeadlineSeconds));Schritt 8: Auf einen Slot warten, lösen und freigeben
Das Warten auf einen Slot verwendet denselben Token, plus ein eigenes Limit von QueueSeconds (20 im Einstellungs-Record). Bleiben alle Slots so lange belegt, bekommt der Aufrufer 503 und kann es erneut versuchen. Ohne Limit würde die Anfrage in der Warteschlange sitzen, bis die Frist sie holt.
CapSkipError ist die Basisklasse der vier Exceptions des SDK, und der catch-Block nennt sie zusammen mit der Abbruch-Exception. RecaptchaAsync bekommt den Token ebenfalls. Das SDK reicht ihn an jede Abfrage weiter, sodass die Frist den laufenden HTTP-Aufruf abbricht, statt auf ihn zu warten. Der finally-Block gibt den Slot nur frei, wenn er belegt wurde. Einen Slot freizugeben, den man nie hielt, ließe einen Lösevorgang mehr durch, als CapSkip ausführen kann.
var held = false;
try
{
// Wait for a free slot, but not longer than QueueSeconds.
if (!await gate.WaitAsync(TimeSpan.FromSeconds(s.QueueSeconds), deadline.Token))
return Fail(503, "All solver slots are busy");
held = true;
var result = await solver.RecaptchaAsync(
req.Sitekey, req.Url, req.Options(), deadline.Token);
return Results.Ok(new SolveResponse(result.Code));
}
catch (Exception ex) when (ex is CapSkipError or OperationCanceledException)
{
return ToProblem(ex, http.RequestAborted.IsCancellationRequested,
deadline.IsCancellationRequested);
}
finally
{
if (held) gate.Release();
}
});Schritt 9: Exceptions in Statuscodes umwandeln
Das ist der Teil, den Aufrufer bemerken. Die Reihenfolge der switch-Fälle ist wichtig, weil das SDK einen abgebrochenen Aufruf auf zwei verschiedene Arten meldet. Eine abgebrochene Übermittlung kommt als NetworkException heraus (“The operation was canceled”). Eine abgebrochene Abfrage kommt als TaskCanceledException heraus. Eine Zuordnung nach Exception-Typ würde einem Aufrufer sagen, dass CapSkip ausgefallen ist, obwohl in Wahrheit die Frist abgelaufen ist. Deshalb werden zuerst die beiden Flags geprüft.
app.Run();
static IResult Fail(int status, string title, string? detail = null) =>
Results.Problem(statusCode: status, title: title, detail: detail);
// Check the two flags first: a cancelled submit surfaces as
// NetworkException, a cancelled poll as TaskCanceledException.
static IResult ToProblem(Exception ex, bool callerLeft, bool deadlinePassed) => ex switch
{
_ when callerLeft => Results.StatusCode(499),
_ when deadlinePassed => Fail(504, "Solve deadline exceeded"),
SolverTimeout => Fail(504, "CapSkip timed out"),
ValidationException => Fail(400, "Bad solve parameters", ex.Message),
ApiException when ex.Message.Contains("UNSOLVABLE") => Fail(422, "CAPTCHA could not be solved"),
ApiException => Fail(502, "CapSkip refused the request"),
NetworkException => Fail(503, "CapSkip is not reachable"),
_ => Fail(500, "Unexpected error"),
};Die ApiException des SDK trägt ihren Text in der Meldung und hat keine Code-Eigenschaft. Der unlösbare Fall wird am Wort UNSOLVABLE in dieser Meldung erkannt. Die Statuscodes unten wurden jeweils gegen den Mock gezielt ausgelöst.
| Status | Was passiert ist | Wie lange es dauerte |
|---|---|---|
| 503 | CapSkip ist geschlossen, daher wurde die Übermittlung verweigert | 2.1 s |
| 422 | CapSkip meldete ERROR_CAPTCHA_UNSOLVABLE | 3.8 s |
| 502 | CapSkip lehnte den eigenen API-Schlüssel des Dienstes ab | unter 0,1 s |
| 400 | Eine v2-Anfrage enthielt eine Action, die nur v3 akzeptiert | unter 0,1 s |
| 504 | Die Frist lief bei einem stummen CapSkip ab (Test-Frist von 6 s) | 6.0 s |
| 504 | Das eigene 5-s-Timeout des SDK löste zuerst aus | 7.8 s |
Die 502 ist Absicht. Ein abgelehnter CapSkip-Schlüssel ist der Fehler des Dienstes, nicht des Aufrufers, daher ist es keine 401 und keine 403. Die letzte Zeile ist länger als 5 Sekunden, weil das SDK seine Uhr erst nach jedem Warten prüft. Es wartet 0,25 s und verdoppelt dann die Wartezeit bis zu 5 s. Das Warten, das bei 3,75 s beginnt, dauert 4 s, daher löst das Timeout bei 7,75 s aus. Derselbe Ablauf erklärt die Zeile mit 3,8 s: Das Ergebnis ist nach 2 s fertig und wird bei der Prüfung nach 3,75 s gefunden.
Ein Fehlschlag bekommt nie eine frühe Antwort. Schließt CapSkip, nachdem es die Aufgabe angenommen hat, behandelt das SDK jede verweigerte Abfrage als noch nicht bereit. Der Aufrufer wartet dann die ganze Frist (etwa 8,0 s bei einer Frist von 8 s auf dem Mock) und bekommt 504. Nur eine verweigerte Übermittlung liefert schnell 503.
Schritt 10: Die Records hinzufügen
Der Einstellungs-Record trägt die Standardwerte. Der Anfrage-Record baut das Optionen-Dictionary nur aus Feldern, die der Aufrufer gesendet hat. Ein v3-Aufrufer kann eine Version und eine Action senden, ein v2-Aufrufer sendet keines von beiden. SdkTimeoutSeconds ist das eigene Limit des SDK, und QueueSeconds ist die Zeit, die eine Anfrage auf einen Slot warten darf. Keines von beiden steht in appsettings.json, daher gelten die Standardwerte. Halten Sie das Limit des SDK über der Frist, damit die Frist zuerst auslöst.
record CapSkipSettings(string Host = "127.0.0.1", int Port = 8080,
string ApiKey = "capskip", int MaxThreads = 10, int DeadlineSeconds = 120,
int QueueSeconds = 20, int SdkTimeoutSeconds = 300);
record SolveRequest(string Sitekey, string Url, string? Version = null, string? Action = null)
{
// Extra reCAPTCHA options, only when the caller sent them.
public Dictionary<string, object?> Options()
{
var o = new Dictionary<string, object?>();
if (Version is not null) o["version"] = Version;
if (Action is not null) o["action"] = Action;
return o;
}
}
record SolveResponse(string Token);Unter Windows ausführen
Verwenden Sie die Option urls von dotnet run und verlassen Sie sich nicht auf die Variable ASPNETCORE_URLS. Die Web-Vorlage liefert eine Datei launchSettings.json mit einem zufälligen Port mit, und dotnet run liest sie. Auf dem Test-PC ignorierte ein einfaches dotnet run mit gesetztem ASPNETCORE_URLS die Variable, lauschte auf dem Port des Profils und wechselte in die Umgebung Development. Die Option urls schlug das Profil, und der Schalter no-launch-profile ließ die Variable wieder wirken und ergab die Umgebung Production. Die Option urls allein lässt die Umgebung Development des Profils weiter aktiv, fügen Sie also auch den Schalter hinzu, wenn Sie Production wollen. Ohne Profil und ohne gesetzte URL fällt Kestrel auf localhost:5000 zurück, was hier nicht getestet wurde.
Für denselben PC binden Sie an 127.0.0.1. Dann kann nichts anderes im Netzwerk den Dienst erreichen.
# Local mode: CapSkip and this service run on the same PC. The service # listens on 127.0.0.1 only, so no other machine can reach it. dotnet run --urls http://127.0.0.1:5080
Für Aufrufer auf anderen Rechnern binden Sie an 0.0.0.0, was jeden Netzwerkadapter bedeutet. Auf dem Test-PC antwortete diese Adresse unter ihrer eigenen LAN-IP mit dem richtigen Schlüssel und verweigerte den falschen mit 401. Ein an 127.0.0.1 gebundener Dienst verweigerte die LAN-Adresse. Setzen Sie zuerst einen echten Dienstschlüssel. Der Dienstschlüssel wandert über reines HTTP außerdem im Klartext durchs Netzwerk, verwenden Sie das also nur in einem Netzwerk, dem Sie vertrauen. Die Windows Firewall fragt möglicherweise, ob Sie das Programm zulassen wollen, prüfen Sie daher, welche Regel sie für den Port anlegt.
# Callers on other machines: 0.0.0.0 means every network adapter. # Put the service key in place first, because anyone on the network can # reach this port. dotnet run --urls http://0.0.0.0:5080
Betrieb des Dienstes getrennt von CapSkip
Der Dienst muss nicht auf demselben PC laufen. Schalten Sie CapSkip auf einem Windows-Rechner, den Sie kontrollieren, in den Server-Modus, möglichst mit statischer öffentlicher IP. Richten Sie den Dienst mit zwei Umgebungsvariablen darauf. Der Test lief in dieser Form gegen einen Mock, der an eine LAN-Adresse gebunden war, und ein Lösevorgang dauerte 1,85 s.
# The service runs on another PC and CapSkip runs in Server mode on # 203.0.113.10. Environment variables override appsettings.json. $env:CapSkip__Host = '203.0.113.10' $env:CapSkip__Port = '8080' dotnet run --urls http://0.0.0.0:5080
Drei Warnungen. Aktivieren Sie in CapSkip die Validierung des API-Schlüssels und verwenden Sie denselben Schlüssel im Abschnitt CapSkip der Einstellungen. Das SDK baut eine reine http-Adresse, daher wandert dieser Schlüssel im Klartext durchs Netzwerk. CapSkip legt außerdem eine Regel an, die für seinen Port alles zulässt, fügen Sie also für jede Adresse, die ihn nicht erreichen soll, eine Blockierregel hinzu. Der Abschnitt Verbindungseinstellungen der Einrichtungsanleitung listet beide Modi auf.
Von PowerShell oder Python aufrufen
Der Aufrufer sendet den sitekey, die Seiten-URL und den Dienstschlüssel. Die Antwort ist ein JSON-Objekt mit einem Feld, token. Verwenden Sie den Token innerhalb von etwa zwei Minuten, weil ein reCAPTCHA-Token abläuft, und verwenden Sie ihn nur einmal. Die Anleitung zur Gültigkeit von Token erklärt diese Frist.
# Ask the service for a token. It answers when the solve finishes.
$body = @{
sitekey = 'YOUR_SITEKEY'
url = 'https://example.com/page-with-recaptcha'
} | ConvertTo-Json
$r = Invoke-RestMethod -Method Post -Uri http://127.0.0.1:5080/solve `
-Headers @{ 'X-API-Key' = 'YOUR_SERVICE_KEY' } `
-ContentType 'application/json' -Body $body
$r.tokenJede andere Sprache funktioniert genauso. Der Python-Aufrufer unten setzt ein Timeout, das länger ist als die Frist des Dienstes. So trifft die 504 des Dienstes ein, bevor der eigene Timer des Aufrufers aufgibt.
import requests
# The caller's timeout must be longer than DeadlineSeconds (120 here),
# so the 504 from the service arrives before your own timeout fires.
r = requests.post(
"http://127.0.0.1:5080/solve",
headers={"X-API-Key": "YOUR_SERVICE_KEY"},
json={"sitekey": "YOUR_SITEKEY", "url": "https://example.com/page-with-recaptcha"},
timeout=130,
)
print(r.status_code, r.json())Was die Schleuse leistet und was nicht
Bei einem Thread-Limit von 3 für CapSkip im Mock und zehn gleichzeitigen Anfragen hielt die Schleuse höchstens drei Aufgaben beim Mock. Ohne Schleuse erreichten alle zehn ihn, und sieben warteten auf einen Thread. Die Schleuse macht nichts schneller, denn drei Threads bestimmen in beiden Fällen das Tempo. Bei einer Haltezeit von einer Sekunde brauchten zehn Anfragen mit Schleuse etwa 7,2 s und ohne sie 7,9 s. Der Gewinn liegt unter einer Sekunde. Ihr Wert zeigt sich, wenn Aufrufer aufgeben.
In diesem Lauf gaben alle Aufrufer nach 4 Sekunden auf, und jeder Lösevorgang brauchte 6. Mit Schleuse erreichten drei bis fünf Aufgaben CapSkip, weil ein Wartender einen Slot übernehmen kann, der kurz vor seiner eigenen Frist frei wird. Acht Sekunden später war keine mehr offen. Ohne sie erreichten alle zehn CapSkip, und sieben waren sechs Sekunden nach dem Aufgeben der Aufrufer noch offen. Auf diese Lösevorgänge wartete niemand mehr, aber sie belegten weiter Threads.
Eine Lücke bleibt. Wenn ein Aufrufer die Verbindung beendet, hört der Dienst auf zu pollen und gibt seinen Slot frei. Das SDK hat keinen Abbruch-Aufruf, nehmen Sie also an, dass CapSkip die Aufgabe beendet und bis dahin einen Thread hält. Der Mock verhält sich so. Die Zahl der Slots kann der Zahl der echten Threads von CapSkip nach einer Welle von Abbrüchen daher vorauslaufen, bis diese Aufgaben fertig sind. Ein echtes CapSkip wurde nicht getestet, und seine Einstellung Row Timeout begrenzt, wie lange eine Aufgabe laufen kann.
Die vollständige Program.cs
Das sind die Schritte 3 bis 10, der Reihe nach zusammengefügt. Fügen Sie sie über die Program.cs der Vorlage ein.
// Program.cs
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using CapSkip;
// System and CapSkip both define TimeoutException, so give the SDK's
// one its own name.
using SolverTimeout = CapSkip.TimeoutException;
var builder = WebApplication.CreateBuilder(args);
// Reads the "CapSkip" section of appsettings.json. The environment
// variable CapSkip__Port overrides CapSkip:Port (__ stands for a colon).
var cfg = builder.Configuration.GetSection("CapSkip").Get<CapSkipSettings>()
?? new CapSkipSettings();
var serviceKey = builder.Configuration["Solver:ApiKey"];
if (string.IsNullOrWhiteSpace(serviceKey))
throw new InvalidOperationException("Set Solver:ApiKey before starting.");
builder.Services.AddSingleton(cfg);
// One client for the whole process. It keeps no per-call state.
builder.Services.AddSingleton(new CapSkipClient(
cfg.ApiKey, cfg.Host, cfg.Port, recaptchaTimeout: cfg.SdkTimeoutSeconds));
// One slot per CapSkip thread (Max. Threads in the CapSkip settings).
builder.Services.AddSingleton(new SemaphoreSlim(cfg.MaxThreads));
// With these two calls, 400, 404, 405 and 415 also come back as
// ProblemDetails.
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseStatusCodePages();
// Only callers that know the service key may use /solve. FixedTimeEquals
// takes the same time wherever the first wrong byte is.
var expected = Encoding.UTF8.GetBytes(serviceKey);
app.Use(async (ctx, next) =>
{
var sent = Encoding.UTF8.GetBytes(ctx.Request.Headers["X-API-Key"].ToString());
if (ctx.Request.Path.StartsWithSegments("/solve")
&& !CryptographicOperations.FixedTimeEquals(sent, expected))
{
await Fail(401, "Missing or wrong X-API-Key").ExecuteAsync(ctx);
return;
}
await next(ctx);
});
// CapSkip has no ping call, so check that its port accepts a connection.
app.MapGet("/health", async (CapSkipSettings s, CancellationToken ct) =>
{
using var probe = CancellationTokenSource.CreateLinkedTokenSource(ct);
probe.CancelAfter(TimeSpan.FromSeconds(3));
try
{
using var tcp = new TcpClient();
await tcp.ConnectAsync(s.Host, s.Port, probe.Token);
return Results.Ok(new { status = "ok" });
}
catch (Exception ex) when (ex is SocketException or OperationCanceledException)
{
return Fail(503, "CapSkip is not reachable");
}
});
app.MapPost("/solve", async (SolveRequest req, CapSkipClient solver,
SemaphoreSlim gate, CapSkipSettings s, HttpContext http) =>
{
if (string.IsNullOrWhiteSpace(req.Sitekey)
|| !Uri.IsWellFormedUriString(req.Url, UriKind.Absolute))
return Results.ValidationProblem(new Dictionary<string, string[]>
{
["request"] = ["sitekey and an absolute url are required"]
});
// One token for the whole request. It is cancelled when the caller
// hangs up (RequestAborted) or when the deadline passes.
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(
http.RequestAborted);
deadline.CancelAfter(TimeSpan.FromSeconds(s.DeadlineSeconds));
var held = false;
try
{
// Wait for a free slot, but not longer than QueueSeconds.
if (!await gate.WaitAsync(TimeSpan.FromSeconds(s.QueueSeconds), deadline.Token))
return Fail(503, "All solver slots are busy");
held = true;
var result = await solver.RecaptchaAsync(
req.Sitekey, req.Url, req.Options(), deadline.Token);
return Results.Ok(new SolveResponse(result.Code));
}
catch (Exception ex) when (ex is CapSkipError or OperationCanceledException)
{
return ToProblem(ex, http.RequestAborted.IsCancellationRequested,
deadline.IsCancellationRequested);
}
finally
{
if (held) gate.Release();
}
});
app.Run();
static IResult Fail(int status, string title, string? detail = null) =>
Results.Problem(statusCode: status, title: title, detail: detail);
// Check the two flags first: a cancelled submit surfaces as
// NetworkException, a cancelled poll as TaskCanceledException.
static IResult ToProblem(Exception ex, bool callerLeft, bool deadlinePassed) => ex switch
{
_ when callerLeft => Results.StatusCode(499),
_ when deadlinePassed => Fail(504, "Solve deadline exceeded"),
SolverTimeout => Fail(504, "CapSkip timed out"),
ValidationException => Fail(400, "Bad solve parameters", ex.Message),
ApiException when ex.Message.Contains("UNSOLVABLE") => Fail(422, "CAPTCHA could not be solved"),
ApiException => Fail(502, "CapSkip refused the request"),
NetworkException => Fail(503, "CapSkip is not reachable"),
_ => Fail(500, "Unexpected error"),
};
record CapSkipSettings(string Host = "127.0.0.1", int Port = 8080,
string ApiKey = "capskip", int MaxThreads = 10, int DeadlineSeconds = 120,
int QueueSeconds = 20, int SdkTimeoutSeconds = 300);
record SolveRequest(string Sitekey, string Url, string? Version = null, string? Action = null)
{
// Extra reCAPTCHA options, only when the caller sent them.
public Dictionary<string, object?> Options()
{
var o = new Dictionary<string, object?>();
if (Version is not null) o["version"] = Version;
if (Action is not null) o["action"] = Action;
return o;
}
}
record SolveResponse(string Token);Häufige Fehler bei einem Minimal-API-Captcha-Solver
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| CS0104 zu TimeoutException | System und CapSkip definieren beide eine TimeoutException | Verwenden Sie den Alias SolverTimeout aus Schritt 3 |
| 500 bei einem SDK-Timeout | Ein switch-Zweig nennt System.TimeoutException | Fangen Sie die Klasse des SDK über den Alias ab |
| Der Dienst ignoriert ASPNETCORE_URLS | dotnet run wendet launchSettings.json an | Übergeben Sie stattdessen die Option urls |
| Jeder Aufruf liefert 401 | Der Header X-API-Key ist nicht gleich Solver:ApiKey | Senden Sie den Dienstschlüssel, nicht den von CapSkip |
| 503 vom Health-Check | Am Port von CapSkip nimmt nichts Verbindungen an | Starten Sie CapSkip und prüfen Sie Local- oder Server-Modus |
| 502 bei jedem Lösevorgang | CapSkip hat den Schlüssel im Abschnitt CapSkip abgelehnt | Gleichen Sie ihn mit dem in CapSkip gesetzten Schlüssel ab |
| 504 nach der vollen Frist | CapSkip ist stumm, oder der Lösevorgang ist langsam | Sehen Sie sich den Lösevorgang im Tab Monitor von CapSkip an und erhöhen Sie dann die Frist |
Microsofts Übersicht über Minimal APIs behandelt die Framework-Seite, und jeder CapSkip-Fehlercode steht in der API-Dokumentation.
FAQ
Brauche ich einen Dienst, wenn nur eine App Captchas löst?
Nein. Rufen Sie das SDK aus dieser App auf. Ein Dienst hilft, wenn mehrere Aufrufer oder Aufrufer in anderen Sprachen ein CapSkip teilen und Sie eine Frist, ein Limit und einen Schlüssel wollen. Die Worker-Service-Anleitung zeigt den Weg im selben Prozess für einen Hintergrundjob.
Kann ich den Dienst bei Azure oder einem anderen Anbieter hosten?
Ja, aber ein gehosteter Dienst läuft auf dem Rechner eines Fremden und kann daher 127.0.0.1 auf Ihrem PC nicht erreichen. Betreiben Sie CapSkip im Server-Modus auf einem Windows-Rechner, den Sie kontrollieren, möglichst mit statischer öffentlicher IP, und setzen Sie CapSkip__Host auf diese Adresse. Aktivieren Sie die Validierung des API-Schlüssels, fügen Sie für jede andere Adresse eine Blockierregel hinzu und denken Sie daran, dass der Schlüssel über reines HTTP im Klartext wandert. Für diesen Leitfaden wurde kein Hosting-Anbieter getestet.
Stoppt eine abgebrochene Anfrage den Lösevorgang in CapSkip?
Nein. Der Dienst hört auf zu pollen und gibt seinen Slot frei. Das SDK hat keinen Abbruch-Aufruf, nehmen Sie also an, dass CapSkip weiter an der Aufgabe arbeitet und einen Thread hält, bis sie fertig ist. Halten Sie die Frist lang genug für einen normalen Lösevorgang, damit Abbrüche selten bleiben.
Kann derselbe Endpunkt reCAPTCHA v3 lösen?
Ja. Senden Sie im Anfrage-Body die version v3 und eine action, dann erreichen die Optionen das SDK unverändert. Der Dienst sendet keinen score-Wert, weil CapSkip keine score-Einstellung hat. Die Seite zum reCAPTCHA-v2-Löser behandelt den Checkbox-Typ, den dieser Leitfaden verwendet.
Die Kurzfassung
Um einen Minimal-API-Captcha-Solver zu bauen, verwenden Sie einen CapSkipClient, eine SemaphoreSlim und einen verknüpften Abbruch-Token pro Anfrage. Prüfen Sie zuerst den Dienstschlüssel. Prüfen Sie die beiden Abbruch-Flags vor dem Exception-Typ. Starten Sie den Dienst mit der Option urls, nicht mit der Umgebungsvariablen. Setzen Sie eine Blockierregel vor CapSkip, wenn es im Server-Modus läuft.
- Alle Captcha-Typen, die das .NET-Paket verarbeitet, finden Sie auf der C#-Captcha-Solver-Seite.
- Wie der Checkbox-Solver funktioniert: die reCAPTCHA-v2-Solver-Seite.
- Der einfache SDK-Aufruf in C#: reCAPTCHA v2 in C# lösen.
Ein gemeinsam genutzter Dienst kann ein ganzes Team bedienen, ohne dass jemand Lösungen zählt. Steht hinter dem Dienst ein lokaler Captcha-Löser auf Ihrem eigenen Windows-Rechner, bleibt jede Lösung frei von Kosten pro Aufruf.
