So lösen Sie reCAPTCHA in CefSharp-OffScreen-Konsolen-Apps

Um reCAPTCHA in CefSharp zu lösen, laden Sie die Seite in einen OffScreen-ChromiumWebBrowser, lesen den sitekey mit einem Skript aus, das über browser.GetMainFrame() läuft, und übergeben ihn an RecaptchaAsync von CapSkip. Schreiben Sie dann den Token in jedes Feld g-recaptcha-response und rufen Sie den Callback der Seite auf oder senden Sie das Formular ab. An zwei Regeln von CefSharp scheitern die meisten ersten Versuche. Ein Aufruf von EvaluateScriptAsync auf dem Browser selbst direkt nach einer Navigation löst fast jedes Mal eine Exception aus, weil der JavaScript-Kontext der Seite noch nicht bereit ist. Und Cef.Shutdown muss auf dem Thread laufen, der CEF gestartet hat, und genau das verletzt ein asynchrones Main stillschweigend. Dieser Leitfaden baut eine kleine Konsolen-App, die beides behandelt, dazu unsichtbare Widgets, explizit gerenderte Widgets und mehrere Seiten gleichzeitig.
Was Sie brauchen
- CapSkip auf einem Windows-Rechner, mit eingeschaltetem reCAPTCHA-Löser. CefSharp läuft ebenfalls nur unter Windows, daher können sich beide einen PC teilen.
- Das .NET 10 SDK, oder das .NET 8 SDK mit net8.0-windows in Schritt 1. Der Code unten wurde unter .NET 8 und .NET 10 ausgeführt, gebaut mit dem SDK 10.0.100.
- Die Pakete CefSharp.OffScreen.NETCore und CapSkip von NuGet. Dieser Leitfaden verwendet CefSharp 152.0.100, das Chromium 152 mitbringt.
- Das Visual C++ 2022 Redistributable für x64. Die eigenen DLLs von CefSharp sind dagegen gelinkt, und das README von CefSharp führt VC++ 2022 ab Version 138 als Voraussetzung auf.
- Eine Adresse für den Solver. Im Local-Modus antwortet CapSkip auf 127.0.0.1 nur für dieses Gerät. Im Server-Modus lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, damit eine App auf einem anderen Rechner es über die API aufrufen kann. Eingestellt werden beide Modi unter Verbindungseinstellungen.
Schritt 1: Ein Projekt mit Windows-Ziel und Runtime anlegen
Gehen Sie von der Konsolenvorlage aus, fügen Sie die Pakete aber noch nicht hinzu. Die Vorlage zielt auf ein schlichtes net10.0, mit dem .NET 8 SDK auf net8.0, und die .NET-Pakete von CefSharp unterstützen nur Windows-Ziele.
# create a console app, then edit CefApp.csproj as shown below before adding packages dotnet new console -n CefApp cd CefApp
Öffnen Sie CefApp.csproj und ersetzen Sie die PropertyGroup darin durch diese hier.
<PropertyGroup> <OutputType>Exe</OutputType> <!-- CefSharp's .NET packages only install into a Windows target (net8.0-windows works too) --> <TargetFramework>net10.0-windows</TargetFramework> <!-- without it, the Chromium binaries never reach bin and the app fails at startup --> <RuntimeIdentifier>win-x64</RuntimeIdentifier> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup>
Fügen Sie dann die beiden Pakete hinzu.
# the first restore downloads about 250 MB, most of it Chromium for win-x64 dotnet add package CefSharp.OffScreen.NETCore dotnet add package CapSkip
Beide Zeilen in dieser PropertyGroup verhindern einen echten Fehler. Ohne das Windows-Ziel bricht dotnet add package mit NU1202 ab und meldet, das Paket sei nicht mit net10.0 kompatibel, weil es net6.0-windows7.0 und neuer unterstützt. Ohne den RuntimeIdentifier läuft der Build ohne Warnungen durch, und die App scheitert dann beim Start mit einer FileNotFoundException für CefSharp.Core.Runtime.dll. Diese Meldung führt in die Irre: Die genannte Datei ist vorhanden. Es fehlt libcef.dll, die Chromium-Engine selbst, die nur dann in bin landet, wenn der Build seine Runtime kennt. PlatformTarget allein auf x64 zu setzen führte in unseren Durchläufen zum selben Fehler.
Rechnen Sie mit einem großen Build. Der erste Restore lädt etwa 250 MB herunter, und der Ausgabeordner enthält etwa 420 MB in 248 Dateien, davon 285 MB in libcef.dll. CefSharp.BrowserSubprocess.exe liegt neben Ihrer exe, weil Chromium seine Renderer als separate Prozesse ausführt.
Schritt 2: CEF einmal starten und auf demselben Thread wieder stoppen
CEF startet einmal pro Prozess und muss auf dem Thread heruntergefahren werden, der es gestartet hat. Der General-Usage-Leitfaden von CefSharp warnt, dass ein Verstoß gegen diese Regel die App zum Hängen bringt. In Version 152 wird stattdessen eine Exception ausgelöst. Deshalb bleibt der Code auf oberster Ebene im Beispiel unten synchron, und nur RunAsync verwendet await.
// Program.cs. Cef.Initialize and Cef.Shutdown must run on the same thread, so the
// top-level code stays synchronous and blocks while the async work runs
using System.Text.Json;
using CapSkip;
using CefSharp;
using CefSharp.OffScreen;
const string SiteUrl = "https://example.com/login";
var settings = new CefSettings
{
// CEF locks this folder: give each app, and each running copy, its own
RootCachePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefApp"),
};
// InitializeAsync returns only when CEF is fully up
if (!Cef.InitializeAsync(settings).GetAwaiter().GetResult())
throw new InvalidOperationException("CEF did not start: " + Cef.GetExitCode());
try
{
RunAsync(SiteUrl).GetAwaiter().GetResult();
}
finally
{
Cef.Shutdown();
}
static async Task RunAsync(string url)
{
using var browser = new ChromiumWebBrowser(url);
// the wait has no timeout of its own: a malformed URL never finishes loading
var load = await browser.WaitForInitialLoadAsync().WaitAsync(TimeSpan.FromSeconds(30));
Console.WriteLine($"Loaded {url}: HTTP {load.HttpStatusCode}");
}Der Grund dafür: In einer Konsolen-App läuft Code nach einem await meist auf einem Thread-Pool-Thread weiter. Mit await auf oberster Ebene lief Cef.Shutdown auf einem Thread-Pool-Thread, nicht auf dem Haupt-Thread, der Cef.Initialize ausgeführt hatte. Die App stürzte dann mit einer unbehandelten Exception ab, die so beginnt: Cef.Shutdown must be called on the same thread that Cef.Initialize was called. Ein await auf Cef.InitializeAsync scheitert auf dieselbe Weise, weil seine Fortsetzung auf dem eigenen UI-Thread von CEF läuft. Blockieren mit GetAwaiter().GetResult() hält Cef.Shutdown auf dem Haupt-Thread, und der finally-Block fährt CEF auch dann herunter, wenn RunAsync eine Exception auslöst.
Verwenden Sie InitializeAsync, auch wenn der Code synchron darauf wartet. Die Methode ist erst abgeschlossen, wenn CEF vollständig hochgefahren ist. Auch das OffScreen-Beispiel von CefSharp selbst startet CEF auf diese Weise.
RootCachePath muss ein absoluter Pfad sein. Ein relativer Pfad löst sofort eine Exception aus, deren Meldung genau das sagt. Lassen Sie ihn weg, verwendet CEF einen gemeinsamen Standardordner unter den lokalen App-Daten, wovor sein eigenes Log warnt. Cookies und lokaler Speicher bleiben im Arbeitsspeicher, sofern Sie nicht zusätzlich CachePath auf einen Ordner innerhalb von RootCachePath setzen, daher startet ein neuer Durchlauf ohne Session. Der Start dauerte in unseren Durchläufen etwa 0,3 Sekunden, Shutdown etwa 0,2.
Schritt 3: Den sitekey über den Hauptframe auslesen
Das ist die Falle, die es nur in CefSharp gibt. Direkt nach LoadUrlAsync oder WaitForNavigationAsync löste browser.EvaluateScriptAsync unter .NET 10 in 20 von 20 Durchläufen eine Exception aus. Die Meldung beginnt so: Unable to execute javascript at this time, scripts can only be executed within a V8Context. Der JavaScript-Kontext der Seite erschien 8 bis 18 Millisekunden später. Nach WaitForInitialLoadAsync scheiterte der Aufruf weit seltener, aber immer noch bei einem oder zwei der ersten drei Browser, die direkt nach dem Hochfahren von CEF parallel gestartet wurden. Dasselbe Skript, über browser.GetMainFrame().EvaluateScriptAsync gesendet, scheiterte nie, daher läuft jedes Skript in diesem Leitfaden über den Hauptframe.
// data-sitekey covers most pages. A grecaptcha.render() call leaves no attributes,
// but Google's anchor iframe carries the key in k= and the widget size in size=
static async Task<(string Sitekey, bool Invisible, string? Callback)?> FindRecaptchaAsync(
ChromiumWebBrowser browser)
{
var r = await browser.GetMainFrame().EvaluateScriptAsync("""
(() => {
const el = document.querySelector('.g-recaptcha[data-sitekey]');
return el && [el.dataset.sitekey, el.dataset.size ?? '', el.dataset.callback ?? ''];
})()
""");
var attrs = r.Result as List<object>;
var anchor = browser.GetBrowser().GetAllFrames()
.Select(f => f.Url)
.FirstOrDefault(u => u.Contains("/recaptcha/") && u.Contains("/anchor?"));
var query = anchor is null
? null
: System.Web.HttpUtility.ParseQueryString(new Uri(anchor).Query);
var sitekey = attrs?[0] as string ?? query?["k"];
if (sitekey is null)
return null;
var invisible = (query?["size"] ?? attrs?[1] as string) == "invisible";
var callback = attrs?[2] as string;
return (sitekey, invisible, string.IsNullOrEmpty(callback) ? null : callback);
}CefSharp gibt echte .NET-Werte zurück, kein JSON. Ein JavaScript-String kommt als String ohne Anführungszeichen an, ein Array als List von object, eine ganze Zahl als Int32, und null oder undefined als null. Ein Skript, das eine Exception wirft, löst in C# keine Exception aus: In der Antwort ist Success auf false gesetzt, und Message enthält den JavaScript-Fehler. Auf ein zurückgegebenes Promise wird automatisch gewartet.
Eine Seite, die grecaptcha.render() aufruft, hinterlässt kein Attribut data-sitekey. Das Anchor-iframe von Google trägt den Schlüssel trotzdem in seinem Parameter k und die Größe des Widgets in seinem Parameter size, und GetAllFrames() listet dieses iframe mit seiner Adresse auf, ob Cross-Origin oder nicht, ganz ohne JavaScript. Die Reihenfolge der Frames änderte sich zwischen den Durchläufen, daher sucht der Code nach der Adresse statt nach der Position.
Schritt 4: Das Captcha mit CapSkip lösen
Senden Sie den sitekey und die Seitenadresse an CapSkip. Ein unsichtbares Widget braucht die Option invisible, und alles andere ist der Standardaufruf für die Checkbox.
// CapSkip solves the widget for this sitekey and page and returns the token.
// CapSkip.TimeoutException is written in full: System has a TimeoutException too
static async Task<string?> SolveAsync(string sitekey, string pageUrl, bool invisible)
{
var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);
var options = invisible ? new Dictionary<string, object?> { ["invisible"] = 1 } : null;
try
{
var result = await solver.RecaptchaAsync(sitekey, pageUrl, options);
return result.Code;
}
catch (NetworkException ex) { Console.WriteLine("Is CapSkip running? " + ex.Message); }
catch (ApiException ex) { Console.WriteLine("CapSkip could not solve it: " + ex.Message); }
catch (CapSkip.TimeoutException) { Console.WriteLine("No answer before the timeout"); }
catch (CapSkipError ex) { Console.WriteLine("CapSkip error: " + ex.Message); }
return null;
}Bei einem Namen ist Vorsicht geboten. Mit den impliziten usings eines Konsolenprojekts lässt sich ein bloßes catch von TimeoutException nicht kompilieren und scheitert mit CS0104, weil System.TimeoutException und CapSkip.TimeoutException beide im Gültigkeitsbereich liegen. Schreiben Sie den vollständigen CapSkip-Namen. RecaptchaAsync gibt nach dem recaptchaTimeout des SDK auf, standardmäßig 300 Sekunden, das Sie im Konstruktor von CapSkipClient festlegen. Ein langsamer Lösevorgang scheitert meist schon früher innerhalb von CapSkip selbst und kommt dann als ApiException an. Das await an dieser Stelle kann auf einem beliebigen Thread weiterlaufen, und das ist in Ordnung: Anders als WebView2 akzeptiert CefSharp Skriptaufrufe von jedem Thread.
Schritt 5: Den Token einsetzen und das Formular absenden
Setzen Sie den Token dort ein, wo das Skript von Google ihn eingesetzt hätte, lösen Sie dann aus, was die Seite als Nächstes tut, und warten Sie auf die Seite, an die das Formular sendet.
// fill every response field, call the widget's callback or submit its form,
// then wait for the page the form posts to and read it
static async Task<string> SubmitTokenAsync(
ChromiumWebBrowser browser, string? callback, string token)
{
// start waiting before the submit, or the navigation can finish first
var navigation = browser.WaitForNavigationAsync(TimeSpan.FromSeconds(30));
var r = await browser.GetMainFrame().EvaluateScriptAsync($$"""
((token, cb) => {
const boxes = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
boxes.forEach(b => { b.value = token; });
if (cb && typeof window[cb] === 'function') { window[cb](token); return 'called back'; }
const form = boxes[0]?.form;
if (!form) return 'no form found';
if (!form.checkValidity()) return 'form has invalid or empty fields';
form.requestSubmit();
return 'submitted';
})({{JsonSerializer.Serialize(token)}}, {{JsonSerializer.Serialize(callback)}})
""");
var how = r.Result as string ?? r.Message;
if (how is not ("submitted" or "called back"))
return how;
try
{
var page = await navigation;
var text = await browser.GetMainFrame().EvaluateScriptAsync("document.body.innerText");
return $"{how}, HTTP {page.HttpStatusCode}: {text.Result}";
}
catch (System.TimeoutException) { return how + ", but the page did not navigate"; }
}Füllen Sie jede textarea g-recaptcha-response, nicht nur die erste. Ein Formular mit zwei Widgets oder mit einem eigenen Feld neben dem von Google hat zwei Felder mit diesem Namen, und ein Server wie PHP behält nur das letzte. Hat das Widget ein Attribut data-callback, ruft das Skript diese Funktion mit dem Token auf, weil die Seite das erwartet. Andernfalls ruft es form.requestSubmit() auf, das die eigenen Submit-Handler der Seite und die HTML5-Validierung ausführt, wie ein Klick auf den Button. Ist ein Pflichtfeld leer, sendet requestSubmit() nichts ab, und ohne die Zeile mit checkValidity würde die App die vollen 30 Sekunden warten und dann melden, dass die Seite nicht navigiert hat. Die Prüfung nennt sofort die tatsächliche Ursache.
Starten Sie WaitForNavigationAsync vor dem Submit-Skript, nicht danach. 50 Millisekunden nach dem Absenden gestartet, verpasste das Warten die Navigation und lief ins Timeout. Vorher gestartet, war das Warten 10 bis 30 Millisekunden nach der Rückkehr des Submit-Skripts beendet. WaitForNavigationAsync wartet standardmäßig fünf Sekunden, daher übergibt der Code 30. Wenn diese Zeit abläuft, löst die Methode System.TimeoutException aus. Deshalb schreibt dieser catch-Block den Namen mit System aus. Der Token gelangt über JSON-Stringliterale, erzeugt mit JsonSerializer.Serialize, in das Skript, sodass Anführungszeichen darin das Skript nicht kaputt machen können. Übergeben Sie ihn nicht stattdessen als zweites Argument an EvaluateScriptAsync: Diese Überladung liest den ersten String als Namen einer aufzurufenden Funktion.
Setzen Sie den Token sofort ein. Laut dem Leitfaden zur reCAPTCHA-Verifizierung von Google läuft ein Token zwei Minuten nach seiner Ausgabe ab. In unseren Durchläufen wurde das Formular 16 bis 21 Millisekunden nach der Rückkehr von CapSkip abgesendet. Das vollständige Beispiel unten ruft diese drei Funktionen aus RunAsync auf.
Das vollständige Konsolenbeispiel
Ersetzen Sie Program.cs durch den Code unten, ändern Sie SiteUrl auf Ihre Seite und starten Sie die App mit dotnet run.
// Program.cs: load a page in a windowless CefSharp browser, solve its reCAPTCHA v2
// with CapSkip, submit the form and print the page that comes back
using System.Text.Json;
using CapSkip;
using CefSharp;
using CefSharp.OffScreen;
const string SiteUrl = "https://example.com/login";
var settings = new CefSettings
{
// CEF locks this folder: give each app, and each running copy, its own
RootCachePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefApp"),
};
// InitializeAsync returns only when CEF is fully up
if (!Cef.InitializeAsync(settings).GetAwaiter().GetResult())
throw new InvalidOperationException("CEF did not start: " + Cef.GetExitCode());
try
{
RunAsync(SiteUrl).GetAwaiter().GetResult();
}
finally
{
Cef.Shutdown();
}
static async Task RunAsync(string url)
{
using var browser = new ChromiumWebBrowser(url);
// the wait has no timeout of its own: a malformed URL never finishes loading
var load = await browser.WaitForInitialLoadAsync().WaitAsync(TimeSpan.FromSeconds(30));
Console.WriteLine($"Loaded {url}: HTTP {load.HttpStatusCode}");
if (await FindRecaptchaAsync(browser) is not { } widget)
{
Console.WriteLine("No reCAPTCHA on this page");
return;
}
Console.WriteLine($"Sitekey {widget.Sitekey}, invisible: {widget.Invisible}");
var token = await SolveAsync(widget.Sitekey, browser.Address, widget.Invisible);
if (token is null)
return;
Console.WriteLine($"Token received, {token.Length} characters");
Console.WriteLine(await SubmitTokenAsync(browser, widget.Callback, token));
}
// data-sitekey covers most pages. A grecaptcha.render() call leaves no attributes,
// but Google's anchor iframe carries the key in k= and the widget size in size=
static async Task<(string Sitekey, bool Invisible, string? Callback)?> FindRecaptchaAsync(
ChromiumWebBrowser browser)
{
var r = await browser.GetMainFrame().EvaluateScriptAsync("""
(() => {
const el = document.querySelector('.g-recaptcha[data-sitekey]');
return el && [el.dataset.sitekey, el.dataset.size ?? '', el.dataset.callback ?? ''];
})()
""");
var attrs = r.Result as List<object>;
var anchor = browser.GetBrowser().GetAllFrames()
.Select(f => f.Url)
.FirstOrDefault(u => u.Contains("/recaptcha/") && u.Contains("/anchor?"));
var query = anchor is null
? null
: System.Web.HttpUtility.ParseQueryString(new Uri(anchor).Query);
var sitekey = attrs?[0] as string ?? query?["k"];
if (sitekey is null)
return null;
var invisible = (query?["size"] ?? attrs?[1] as string) == "invisible";
var callback = attrs?[2] as string;
return (sitekey, invisible, string.IsNullOrEmpty(callback) ? null : callback);
}
// CapSkip solves the widget for this sitekey and page and returns the token.
// CapSkip.TimeoutException is written in full: System has a TimeoutException too
static async Task<string?> SolveAsync(string sitekey, string pageUrl, bool invisible)
{
var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);
var options = invisible ? new Dictionary<string, object?> { ["invisible"] = 1 } : null;
try
{
var result = await solver.RecaptchaAsync(sitekey, pageUrl, options);
return result.Code;
}
catch (NetworkException ex) { Console.WriteLine("Is CapSkip running? " + ex.Message); }
catch (ApiException ex) { Console.WriteLine("CapSkip could not solve it: " + ex.Message); }
catch (CapSkip.TimeoutException) { Console.WriteLine("No answer before the timeout"); }
catch (CapSkipError ex) { Console.WriteLine("CapSkip error: " + ex.Message); }
return null;
}
// fill every response field, call the widget's callback or submit its form,
// then wait for the page the form posts to and read it
static async Task<string> SubmitTokenAsync(
ChromiumWebBrowser browser, string? callback, string token)
{
// start waiting before the submit, or the navigation can finish first
var navigation = browser.WaitForNavigationAsync(TimeSpan.FromSeconds(30));
var r = await browser.GetMainFrame().EvaluateScriptAsync($$"""
((token, cb) => {
const boxes = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
boxes.forEach(b => { b.value = token; });
if (cb && typeof window[cb] === 'function') { window[cb](token); return 'called back'; }
const form = boxes[0]?.form;
if (!form) return 'no form found';
if (!form.checkValidity()) return 'form has invalid or empty fields';
form.requestSubmit();
return 'submitted';
})({{JsonSerializer.Serialize(token)}}, {{JsonSerializer.Serialize(callback)}})
""");
var how = r.Result as string ?? r.Message;
if (how is not ("submitted" or "called back"))
return how;
try
{
var page = await navigation;
var text = await browser.GetMainFrame().EvaluateScriptAsync("document.body.innerText");
return $"{how}, HTTP {page.HttpStatusCode}: {text.Result}";
}
catch (System.TimeoutException) { return how + ", but the page did not navigate"; }
}Die App gibt jede Phase aus: den HTTP-Status der Seite, den sitekey und ob das Widget unsichtbar ist, die Länge des Tokens und den Text der Seite, an die das Formular gesendet wurde. Auf einer Seite ohne Widget meldet sie das und hört auf. Wir haben sie unter .NET 8 und .NET 10 gegen neun Seiten laufen lassen. Das waren eine Checkbox mit eigener textarea, dieselbe Checkbox mit dem echten api.js von Google und seinem öffentlichen Testschlüssel sowie zwei unsichtbare Widgets mit Callback, eines mit dem Skript von Google und eines ohne. Der Rest waren explizit gerenderte Widgets mit und ohne die Größe invisible, zwei Antwortfelder, ein leeres Pflichtfeld und eine Seite ohne Widget. Außerdem meldet die App einen Solver, der nicht läuft, etwa zwei Sekunden nach dem ersten Versuch, ihn zu erreichen.
Ein Durchlauf besteht größtenteils aus Warten. Mit dem Skript von Google auf der Seite erreichte die Löseanfrage CapSkip 0,3 bis 1,3 Sekunden, nachdem die Seite angefordert wurde, am längsten beim ersten Besuch nach dem Leeren des Caches. Den Großteil davon macht das Laden von api.js und seinem iframe aus. Alles nach dem Lösevorgang dauerte ein paar Dutzend Millisekunden.
Zwei Wartevorgänge haben kein eigenes Timeout. WaitForInitialLoadAsync und LoadUrlAsync geben nie auf, und eine fehlerhafte Adresse lädt nie zu Ende: Ein ungeschützter Test wartete noch, als wir ihn nach zweieinhalb Minuten abbrachen. Deshalb verpackt RunAsync das erste Laden in WaitAsync. Ein Ladevorgang, der scheitert, wird trotzdem abgeschlossen, mit Success auf false. Eine 404-Seite meldet ihren Statuscode, und ein geschlossener Port meldet nach etwa zwei Sekunden ConnectionRefused.
Mehrere Seiten gleichzeitig
Ein einziges Cef.Initialize kann viele Browser bedienen, und die Lösevorgänge können gleichzeitig laufen. Geben Sie aber jedem Browser seinen eigenen RequestContext. Ohne ihn teilen sich alle Browser einen Cookie-Speicher, sodass jede Login-Seite das Session-Cookie der anderen überschreibt.
// several pages at once: one Cef.Initialize, one browser per page. Each browser gets
// its own RequestContext, or they all share one cookie jar and one session
static Task RunManyAsync(string[] urls) =>
Task.WhenAll(urls.Select(async url =>
{
using var browser = new ChromiumWebBrowser(url, requestContext: new RequestContext());
await browser.WaitForInitialLoadAsync().WaitAsync(TimeSpan.FromSeconds(30));
if (await FindRecaptchaAsync(browser) is not { } widget)
return;
var token = await SolveAsync(widget.Sitekey, browser.Address, widget.Invisible);
if (token is not null)
Console.WriteLine(url + ": " + await SubmitTokenAsync(browser, widget.Callback, token));
}));Drei Browser im gemeinsamen Kontext lösten ihre Captchas parallel, und zwei ihrer drei abgesendeten Formulare wurden wegen eines CSRF-Konflikts abgelehnt, weil das Cookie nicht mehr zum Formular passte. Mit je einem eigenen RequestContext wurden alle drei akzeptiert. Rufen Sie RunManyAsync auf oberster Ebene anstelle von RunAsync auf, innerhalb derselben try- und finally-Blöcke. Der Compiler warnt dann, dass RunAsync und SiteUrl unbenutzt sind, was harmlos ist. Der reCAPTCHA-Bereich von CapSkip führt standardmäßig 10 Lösevorgänge gleichzeitig aus, festgelegt über Max. Threads, und stellt den Rest in eine Warteschlange.
Zwei Instanzen der App können sich keinen RootCachePath teilen. Die zweite Instanz scheiterte nach etwa 0,1 Sekunden beim Start, daher löst der Code oben seine InvalidOperationException aus, mit der Meldung CEF did not start: NormalExitProcessNotified. Schlimmer noch: Die bereits laufende Instanz stürzte später ab, bei Cef.Shutdown, und zwar in acht von acht Durchläufen. Geben Sie jeder laufenden Instanz einen eigenen Ordner. Wenn ein zweiter Start trotzdem vorkommen kann, fügen Sie diesen Handler hinzu, der den Absturz in jedem Durchlauf verhindert hat.
// at the very end of Program.cs, then start CEF with
// Cef.InitializeAsync(settings, browserProcessHandler: new RelaunchHandler()).
// A second copy on the same RootCachePath still cannot start, but CEF no longer opens
// a window of its own in the running copy
class RelaunchHandler : CefSharp.Handler.BrowserProcessHandler
{
protected override bool OnAlreadyRunningAppRelaunch(
IReadOnlyDictionary<string, string> commandLine, string currentDirectory) => true;
}Was sich in WinForms oder WPF ändert
Um reCAPTCHA in CefSharp innerhalb einer Desktop-App zu lösen, verwenden Sie das Paket CefSharp.WinForms.NETCore oder CefSharp.Wpf.NETCore und dieselben Schritte. Das Steuerelement startet und stoppt CEF selbst, Sie rufen Cef.Shutdown also nicht auf. Wenn Sie eigene Einstellungen wollen, rufen Sie Cef.Initialize damit auf, bevor das erste Steuerelement erstellt wird. Skripte, Frames und der Aufruf von CapSkip funktionieren genau wie oben. Was sich ändert, ist, wo die Events ankommen.
// WinForms (CefSharp.WinForms.NETCore): CefSharp raises LoadingStateChanged, FrameLoadEnd
// and AddressChanged on its own thread, not the form's, so marshal control updates back
browser.LoadingStateChanged += (_, e) =>
{
if (e.IsLoading)
return;
statusLabel.BeginInvoke(() => statusLabel.Text = "Loaded " + browser.Address);
};CefSharp löst LoadingStateChanged, FrameLoadEnd und AddressChanged auf seinem eigenen Thread aus, nicht auf dem Thread, der Ihr Formular besitzt. Die Skriptaufrufe selbst sind an keinen Thread gebunden: EvaluateScriptAsync funktionierte vom UI-Thread aus, nach ConfigureAwait(false) und innerhalb von Task.Run. Das ist das Gegenteil von WebView2, wo jeder Aufruf von einem anderen Thread eine Exception auslöst. Die Regel in CefSharp betrifft also Ihre eigenen Steuerelemente. In WinForms führte das Setzen eines Labels aus dem Event heraus nur bei angehängtem Debugger zu Cross-thread operation not valid, weil WinForms threadübergreifende Aufrufe nur dann prüft. Ohne Debugger änderte sich der Text, und nichts wurde gemeldet. Ein Release-Build verbirgt den Bug also, und das Formular kann sich später trotzdem fehlerhaft verhalten. BeginInvoke führt die Aktualisierung auf dem Thread des Formulars aus. In WPF löst dieselbe Aktualisierung immer eine Exception aus, mit der Meldung The calling thread cannot access this object because a different thread owns it. Verwenden Sie dort Dispatcher.InvokeAsync des Steuerelements.
CapSkip auf einem anderen Rechner betreiben
Die App und der Solver müssen sich keinen PC teilen. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, dann lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP. Richten Sie den Client anschließend auf diese Adresse.
// CapSkip in Server mode on another Windows machine: replace the client line in SolveAsync var solver = new CapSkipClient(apiKey: "YOUR_API_KEY", host: "192.168.1.50", port: 8080);
Verwenden Sie eine statische öffentliche IP, wenn der Weg über das Internet führt, und aktivieren Sie die Validierung des API-Schlüssels, mit demselben Schlüssel im Client. CapSkip öffnet seinen Port beim Start des Server-Modus für jede Adresse. Um ihn auf die Rechner zu beschränken, die Sie erwarten, fügen Sie in der Windows Firewall eine Blockierregel für alle anderen hinzu. Auf demselben Rechner schreiben Sie 127.0.0.1 statt localhost, denn localhost versuchte in einem früheren Test zuerst die IPv6-Adresse und kostete so etwa zwei Sekunden pro neuer Verbindung. Der Solver bleibt Ihr eigener Windows-Rechner, und abgerechnet wird pro Lösung weiterhin nichts.
Häufige Fehler beim Lösen von reCAPTCHA in CefSharp
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| NU1202 beim Hinzufügen des Pakets | Das Projekt zielt auf net10.0 ohne das Windows-Suffix | Setzen Sie das Ziel auf net10.0-windows oder net8.0-windows |
| FileNotFoundException für CefSharp.Core.Runtime.dll | Kein RuntimeIdentifier, daher kam libcef.dll nie in bin an | Fügen Sie RuntimeIdentifier win-x64 hinzu und bauen Sie neu |
| Cef.Shutdown must be called on the same thread | Ein await auf oberster Ebene hat den Code auf einen anderen Thread verlegt | Halten Sie die oberste Ebene synchron und warten Sie blockierend auf RunAsync |
| Unable to execute javascript at this time | Das Skript lief auf dem Browser, bevor die neue Seite einen JavaScript-Kontext hatte | Führen Sie es über browser.GetMainFrame() aus |
| Die App lädt nie zu Ende | Die Adresse ist fehlerhaft, und das Warten auf das Laden hat kein Timeout | Verpacken Sie das Warten in WaitAsync mit einem Timeout |
| Das Absenden wird für einen Browser akzeptiert, für die anderen aber abgelehnt | Parallele Browser teilen sich einen Cookie-Speicher | Geben Sie jedem Browser seinen eigenen RequestContext |
| Die Seite lädt nach dem Absenden erneut das Login-Formular | Nur ein Antwortfeld wurde gefüllt, der Callback lief nie, oder der Token war abgelaufen | Füllen Sie jedes Feld, rufen Sie den Callback auf und setzen Sie den Token direkt nach dem Lösevorgang ein |
| Is CapSkip running? No connection could be made | CapSkip läuft nicht, oder Host oder Port sind falsch | Starten Sie CapSkip und gleichen Sie die Adresse mit seinen Verbindungseinstellungen ab |
FAQ
Funktioniert das auch für reCAPTCHA v3 und Enterprise?
Ja, mit Optionen statt mit einer anderen Methode. Eine Enterprise-Seite lädt enterprise.js statt api.js, und die Anchor-Adresse enthält dann /recaptcha/enterprise/, worauf die Frame-Suche oben bereits passt. Fügen Sie den Optionen von RecaptchaAsync enterprise mit dem Wert 1 hinzu. Eine v3-Seite hat keine Checkbox und keine textarea im Formular: Ihr sitekey steht im Parameter render von api.js, und die Seite sendet den Token aus ihrem eigenen Skript. Ihr Anchor-iframe enthält size=invisible, daher meldet FindRecaptchaAsync ein unsichtbares Widget. Prüfen Sie zuerst die Adresse von api.js auf render=, übergeben Sie dann version mit dem Wert v3 sowie die action der Seite, und lassen Sie invisible weg, denn das lehnt das SDK für v3 ab. Ausführlicher behandelt werden die Optionen invisible und enterprise in diesem Leitfaden zum Lösen von reCAPTCHA v2 in C#.
Wirkt ein OffScreen-Browser auf die Website wie ein Headless-Browser?
Nicht an den offensichtlichen Merkmalen. Sein User-Agent ist ein gewöhnlicher Chrome-152-String ohne HeadlessChrome darin, navigator.webdriver ist false, und die Standardansicht misst 1366 mal 768 Pixel. Zwei Unterschiede sind sichtbar. Die Browser-Brands, die er sendet, nennen Chromium, aber nicht Google Chrome, und die Außengröße des Fensters entspricht seiner Innengröße, weil kein Browserrahmen die Seite umgibt. Der reCAPTCHA-Token selbst stammt aus dem Lösevorgang von CapSkip, nicht aus diesem Browser, und unsere Tests verwendeten den Testschlüssel von Google. Wie eine Website den CefSharp-Browser bewertet, haben wir daher nicht gemessen.
Kann ich Cef.Shutdown weglassen?
Eine App, die es wegließ, beendete sich in unseren Durchläufen normal, etwa 0,1 Sekunden nach der Rückkehr von Main. CefSharp dokumentiert es trotzdem als erforderlich, und die Abstürze, die wir gesehen haben, hatten andere Ursachen: Shutdown auf dem falschen Thread oder eine zweite Instanz der App mit demselben Cache-Ordner. Behalten Sie den Aufruf im finally-Block, auf dem Haupt-Thread, nachdem InitializeAsync abgeschlossen ist.
Kann die App auf einem anderen Rechner laufen als CapSkip?
Ja. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, damit es auf einer Netzwerkadresse statt auf Loopback lauscht, und übergeben Sie diese Adresse als Host von CapSkipClient. Die App ruft es dann über dieselbe HTTP-API von jedem Rechner aus auf, der den Port erreichen kann. Fügen Sie in der Windows Firewall eine Blockierregel für die Adressen hinzu, die es nicht erreichen sollen. Der Solver läuft weiterhin auf Hardware, die Ihnen gehört, und kein Lösevorgang läuft über einen Drittanbieterdienst.
Die Kurzfassung
Um reCAPTCHA in CefSharp zu lösen, zielen Sie mit einem RuntimeIdentifier auf net10.0-windows oder net8.0-windows, starten CEF mit InitializeAsync und halten die oberste Ebene synchron, damit Cef.Shutdown auf demselben Thread läuft. Führen Sie jedes Skript über GetMainFrame() aus, lesen Sie den sitekey aus data-sitekey oder dem Anchor-iframe und lösen Sie das reCAPTCHA mit RecaptchaAsync. Füllen Sie jedes Antwortfeld, rufen Sie den Callback oder form.requestSubmit() auf, und beginnen Sie mit dem Warten auf die Navigation, bevor Sie absenden.
- Alle Captcha-Typen, die das .NET-Paket verarbeitet, finden Sie auf der C#-Captcha-Solver-Seite.
- Wie das Checkbox-reCAPTCHA funktioniert und was der Solver abdeckt: die reCAPTCHA-v2-Solver-Seite.
Ein Browser, der auf Ihrem eigenen Windows-PC läuft, verdient einen Solver, der das ebenfalls tut. CapSkip ist ein unbegrenzter Captcha-Löser auf genau diesem Rechner, sodass das Lösen eines ganzen Stapels von Seiten nicht mehr kostet als das einer einzigen.
