Как решить reCAPTCHA в консольных приложениях CefSharp OffScreen

Чтобы решить reCAPTCHA в CefSharp, загрузите страницу в ChromiumWebBrowser из пакета OffScreen, прочитайте sitekey скриптом, запущенным через browser.GetMainFrame(), и передайте его в RecaptchaAsync из CapSkip. Затем запишите токен в каждое поле g-recaptcha-response и вызовите колбэк страницы или отправьте форму. Из-за двух правил CefSharp срывается большинство первых попыток. Вызов EvaluateScriptAsync у самого браузера сразу после навигации почти всегда выбрасывает исключение, потому что контекст JavaScript страницы ещё не готов. А Cef.Shutdown должен выполняться в том потоке, который запустил CEF, и async Main незаметно нарушает это правило. В этом руководстве мы соберём небольшое консольное приложение, которое справляется с обоими правилами, а также с невидимыми виджетами, явной отрисовкой виджета и несколькими страницами одновременно.
Что понадобится
- CapSkip, запущенный на машине с Windows, с включённым решателем reCAPTCHA. CefSharp тоже работает только в Windows, поэтому оба можно запустить на одном ПК.
- SDK .NET 10 или SDK .NET 8 с net8.0-windows в шаге 1. Код ниже запускался на .NET 8 и .NET 10 и собирался с SDK 10.0.100.
- Пакеты CefSharp.OffScreen.NETCore и CapSkip из NuGet. В этом руководстве используется CefSharp 152.0.100, который поставляется с Chromium 152.
- Visual C++ 2022 Redistributable для x64. От него зависят собственные DLL CefSharp, а README CefSharp указывает VC++ 2022 как обязательный компонент начиная с версии 138.
- Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства. В режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы приложение на другой машине могло обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.
Шаг 1: создаём проект с целевой платформой Windows и средой выполнения
Начните с консольного шаблона, но пакеты пока не добавляйте. Шаблон нацелен на обычный net10.0 (или net8.0 на SDK .NET 8), а пакеты CefSharp для .NET поддерживают только целевые платформы Windows.
# create a console app, then edit CefApp.csproj as shown below before adding packages dotnet new console -n CefApp cd CefApp
Откройте CefApp.csproj и замените его PropertyGroup на этот.
<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>
Затем добавьте оба пакета.
# the first restore downloads about 250 MB, most of it Chromium for win-x64 dotnet add package CefSharp.OffScreen.NETCore dotnet add package CapSkip
Каждая из двух строк в этом PropertyGroup предотвращает реальный сбой. Без целевой платформы Windows команда dotnet add package останавливается с NU1202 и сообщает, что пакет несовместим с net10.0, потому что он поддерживает net6.0-windows7.0 и новее. Без RuntimeIdentifier сборка проходит без предупреждений, а затем приложение падает при запуске с FileNotFoundException для CefSharp.Core.Runtime.dll. Это сообщение вводит в заблуждение: названный в нём файл на месте. Не хватает libcef.dll, то есть самого движка Chromium, а он попадает в bin, только когда сборка знает свою среду выполнения. В наших запусках одна лишь установка PlatformTarget в x64 приводила к той же ошибке.
Готовьтесь к большой сборке. Первое восстановление пакетов скачивает около 250 МБ, а папка вывода занимает около 420 МБ в 248 файлах, из них 285 МБ приходится на libcef.dll. CefSharp.BrowserSubprocess.exe лежит рядом с вашим exe, потому что Chromium запускает свои рендереры в отдельных процессах.
Шаг 2: запускаем CEF один раз и останавливаем в том же потоке
CEF запускается в процессе только один раз, и останавливать его нужно в том же потоке, который его запустил. В руководстве General Usage по CefSharp предупреждают, что нарушение этого правила приводит к зависанию приложения. В версии 152 вместо зависания выбрасывается исключение. Поэтому код верхнего уровня ниже остаётся синхронным, и await используется только в RunAsync.
// 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}");
}Вот почему. В консольном приложении код после await обычно продолжается в потоке из пула потоков. С await на верхнем уровне Cef.Shutdown выполнялся в потоке из пула, а не в главном потоке, который выполнил Cef.Initialize. После этого приложение падало с необработанным исключением, которое начинается так: Cef.Shutdown must be called on the same thread that Cef.Initialize was called. Ожидание Cef.InitializeAsync через await ломается так же, потому что его продолжение выполняется в собственном UI-потоке CEF. Блокировка через GetAwaiter().GetResult() оставляет Cef.Shutdown в главном потоке, а блок finally всё равно останавливает CEF, если RunAsync выбрасывает исключение.
Используйте InitializeAsync, хотя код и ждёт его синхронно. Он завершается только тогда, когда CEF полностью запущен, и именно так CEF запускается в собственном примере OffScreen от CefSharp.
RootCachePath должен быть абсолютным путём. Относительный путь сразу приводит к исключению с сообщением именно об этом. Если не указать его вовсе, CEF использует общую папку по умолчанию в локальных данных приложений, о чём предупреждает его собственный лог. Cookies и локальное хранилище остаются в памяти, если вы не зададите ещё и CachePath с папкой внутри RootCachePath, поэтому каждый новый запуск начинается без сессии. В наших запусках старт занимал около 0.3 секунды, а Shutdown около 0.2.
Шаг 3: читаем sitekey через главный фрейм
Это ловушка, характерная именно для CefSharp. Сразу после LoadUrlAsync или WaitForNavigationAsync вызов browser.EvaluateScriptAsync выбрасывал исключение в 20 запусках из 20 на .NET 10. Сообщение начинается так: Unable to execute javascript at this time, scripts can only be executed within a V8Context. Контекст JavaScript страницы появлялся спустя от 8 до 18 миллисекунд. После WaitForInitialLoadAsync сбой случался гораздо реже, но всё же происходил в одном или двух из первых трёх браузеров, запущенных параллельно сразу после старта CEF. Тот же скрипт, отправленный через browser.GetMainFrame().EvaluateScriptAsync, ни разу не дал сбоя, поэтому каждый скрипт в этом руководстве идёт через главный фрейм.
// 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 возвращает настоящие значения .NET, а не JSON. Строка JavaScript приходит как строка без кавычек, массив как List с элементами типа object, целое число как Int32, а null или undefined как null. Скрипт, который выбрасывает ошибку, не вызывает исключения в C#: в ответе Success равен false, а ошибка JavaScript лежит в Message. Если скрипт возвращает Promise, CefSharp сам дожидается его результата.
Страница, которая вызывает grecaptcha.render(), не оставляет атрибута data-sitekey. Но iframe anchor от Google всё равно несёт ключ в параметре k, а размер виджета в параметре size, и GetAllFrames() перечисляет этот iframe вместе с его адресом, независимо от того, кросс-доменный он или нет, причём без всякого JavaScript. Порядок фреймов от запуска к запуску менялся, поэтому код ищет iframe по адресу, а не по позиции.
Шаг 4: решаем капчу с помощью CapSkip
Отправьте sitekey и адрес страницы в CapSkip. Невидимому виджету нужна опция invisible, а во всём остальном это обычный вызов для виджета с чекбоксом.
// 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;
}Одно имя требует внимания. С неявными using консольного проекта голый catch по TimeoutException не компилируется и выдаёт CS0104, потому что в области видимости находятся и System.TimeoutException, и CapSkip.TimeoutException. Пишите полное имя из CapSkip. RecaptchaAsync сдаётся по истечении recaptchaTimeout из SDK (по умолчанию 300 секунд), который задаётся в конструкторе CapSkipClient. Медленное решение обычно проваливается раньше внутри самого CapSkip и тогда приходит как ApiException. Здесь await может продолжиться в любом потоке, и это нормально: в отличие от WebView2, CefSharp принимает вызовы скриптов из любого потока.
Шаг 5: внедряем токен и отправляем форму
Положите токен туда, куда его положил бы скрипт Google, затем запустите то, что страница делает дальше, и дождитесь страницы, на которую форма отправляет данные.
// 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"; }
}Заполняйте каждую textarea g-recaptcha-response, а не только первую. В форме с двумя виджетами или с собственным полем рядом с полем Google два поля с таким именем, а сервер, например на PHP, оставляет только последнее. Если у виджета есть атрибут data-callback, скрипт вызывает эту функцию с токеном, потому что страница этого ожидает. Иначе он вызывает form.requestSubmit(), который запускает собственные обработчики отправки страницы и валидацию HTML5, как клик по кнопке. Если обязательное поле пустое, requestSubmit() ничего не отправляет, поэтому без строки с checkValidity приложение прождало бы все 30 секунд и сообщило бы, что навигации не произошло. Эта проверка сразу называет настоящую причину.
Запускайте WaitForNavigationAsync до скрипта отправки, а не после него. Запущенное через 50 миллисекунд после отправки, ожидание пропустило навигацию и завершилось по таймауту. Запущенное до отправки, ожидание завершалось спустя от 10 до 30 миллисекунд после того, как скрипт отправки вернул результат. По умолчанию WaitForNavigationAsync ждёт пять секунд, поэтому код передаёт 30. Когда это время истекает, метод выбрасывает System.TimeoutException, и именно из-за этого в том catch имя System указано полностью. Токен попадает в скрипт в виде строковых литералов JSON, созданных через JsonSerializer.Serialize, поэтому кавычки в нём не могут сломать скрипт. Не передавайте его вместо этого вторым аргументом в EvaluateScriptAsync: эта перегрузка воспринимает первую строку как имя вызываемой функции.
Внедряйте токен сразу. В своём руководстве по проверке reCAPTCHA Google указывает, что токен истекает через две минуты после выдачи. В наших запусках форма отправлялась спустя от 16 до 21 миллисекунды после того, как CapSkip вернул ответ. Полный пример ниже вызывает эти три функции из RunAsync.
Полный консольный пример
Замените Program.cs кодом ниже, поменяйте SiteUrl на адрес своей страницы и запустите через 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"; }
}Приложение выводит каждый этап: HTTP-статус страницы, sitekey и признак невидимого виджета, длину токена и текст страницы, на которую форма отправила данные. На странице без виджета оно сообщает об этом и останавливается. Мы запускали его на .NET 8 и .NET 10 на девяти страницах. Среди них были чекбокс с собственной textarea, тот же чекбокс с настоящим api.js от Google и публичным тестовым ключом Google, а также два невидимых виджета с колбэком, один со скриптом Google и один без него. Остальные страницы содержали явную отрисовку виджета с размером invisible и без него, два поля ответа и пустое обязательное поле, а на последней виджета не было вовсе. Кроме того, приложение сообщает о незапущенном решателе примерно через две секунды после первой попытки к нему обратиться.
Большую часть запуска занимает ожидание. Когда на странице был скрипт Google, запрос на решение доходил до CapSkip спустя от 0.3 до 1.3 секунды после запроса страницы, и дольше всего это длилось при первом посещении после очистки кэша. Большая часть этого времени уходит на загрузку api.js и его iframe. Всё, что происходило после решения, занимало несколько десятков миллисекунд.
У двух ожиданий нет собственного таймаута. WaitForInitialLoadAsync и LoadUrlAsync никогда не сдаются, а некорректный адрес никогда не догружается: тест без защиты всё ещё ждал, когда мы остановили его через две с половиной минуты. Поэтому RunAsync оборачивает первую загрузку в WaitAsync. Неудачная загрузка всё равно завершается, но с Success, равным false. Страница 404 сообщает свой код статуса, а закрытый порт примерно через две секунды сообщает ConnectionRefused.
Несколько страниц одновременно
Один Cef.Initialize может обслуживать много браузеров, и все решения могут идти одновременно. Но дайте каждому браузеру собственный RequestContext. Без него все браузеры делят одно хранилище cookies, и каждая страница входа перезаписывает сессионный cookie остальных.
// 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));
}));Три браузера с общим контекстом решали капчу параллельно, и две из трёх их отправок были отклонены из-за несовпадения CSRF, потому что cookie уже не соответствовал форме. Когда у каждого был свой RequestContext, все три отправки были приняты. Вызывайте RunManyAsync с верхнего уровня вместо RunAsync, внутри тех же try и finally. Компилятор тогда предупредит, что RunAsync и SiteUrl не используются, и это безобидно. Раздел reCAPTCHA в CapSkip по умолчанию выполняет 10 решений одновременно (настройка Max. Threads), а остальные ставит в очередь.
Две копии приложения не могут использовать один RootCachePath. Вторая копия завершилась ошибкой запуска примерно через 0.1 секунды, поэтому код выше выбрасывает свой InvalidOperationException с сообщением CEF did not start: NormalExitProcessNotified. Хуже того, копия, которая уже работала, позже падала на Cef.Shutdown в восьми запусках из восьми. Дайте каждой запущенной копии собственную папку. Если второй запуск всё же может случиться, добавьте этот обработчик: с ним падение не повторилось ни в одном запуске.
// 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;
}Что меняется в WinForms или WPF
Чтобы решить reCAPTCHA в CefSharp внутри настольного приложения, используйте пакет CefSharp.WinForms.NETCore или CefSharp.Wpf.NETCore и те же шаги. Элемент управления сам запускает и останавливает CEF, поэтому Cef.Shutdown вы не вызываете. Если нужны собственные настройки, вызовите с ними Cef.Initialize до создания первого элемента управления. Скрипты, фреймы и вызов CapSkip работают точно так же, как выше. Меняется только то, куда приходят события.
// 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 вызывает LoadingStateChanged, FrameLoadEnd и AddressChanged в собственном потоке, а не в потоке, которому принадлежит ваша форма. Сами вызовы скриптов к потоку не привязаны: EvaluateScriptAsync работал из UI-потока, после ConfigureAwait(false) и внутри Task.Run. В WebView2 всё наоборот: там любой вызов из другого потока выбрасывает исключение. Поэтому правило в CefSharp касается ваших собственных элементов управления. В WinForms изменение Label из события выбрасывало Cross-thread operation not valid только с подключённым отладчиком, потому что WinForms проверяет межпоточные вызовы только в этом случае. Без отладчика текст менялся и ни о чём не сообщалось, так что релизная сборка скрывает баг, а форма всё равно может позже повести себя неправильно. BeginInvoke выполняет обновление в потоке формы. В WPF то же обновление выбрасывает исключение всегда, с сообщением The calling thread cannot access this object because a different thread owns it. Там используйте Dispatcher.InvokeAsync элемента управления.
Запуск CapSkip на другой машине
Приложению и решателю не обязательно работать на одном ПК. Переключите CapSkip в режим Server в настройках подключения, и он будет слушать ваш сетевой адрес или публичный IP. Затем направьте клиент на этот адрес.
// 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);
Если маршрут идёт через интернет, используйте статический публичный IP и включите проверку API-ключа, указав тот же ключ в клиенте. При запуске режима Server CapSkip открывает свой порт для всех адресов, поэтому, чтобы доступ к нему был только у ожидаемых машин, добавьте в Windows Firewall правило, блокирующее все остальные. На той же машине пишите 127.0.0.1, а не localhost: в одном из прежних тестов localhost сначала пробовал адрес IPv6, и это стоило около двух секунд на каждое новое подключение. Решателем по-прежнему остаётся ваша собственная машина с Windows, и решения по-прежнему не тарифицируются.
Частые ошибки при решении reCAPTCHA в CefSharp
| Что вы видите | Причина | Исправить |
|---|---|---|
| NU1202 при добавлении пакета | Проект нацелен на net10.0 без суффикса Windows | Укажите целевую платформу net10.0-windows или net8.0-windows |
| FileNotFoundException для CefSharp.Core.Runtime.dll | Нет RuntimeIdentifier, поэтому libcef.dll так и не попал в bin | Добавьте RuntimeIdentifier win-x64 и пересоберите проект |
| Cef.Shutdown must be called on the same thread | await на верхнем уровне перенёс код в другой поток | Оставьте верхний уровень синхронным и дожидайтесь RunAsync блокирующим вызовом |
| Unable to execute javascript at this time | Скрипт был вызван у браузера раньше, чем у новой страницы появился контекст JavaScript | Выполняйте его через browser.GetMainFrame() |
| Загрузка страницы никогда не завершается | Адрес некорректен, а у ожидания загрузки нет таймаута | Оберните ожидание в WaitAsync с таймаутом |
| Отправка формы принимается для одного браузера, но отклоняется для остальных | Параллельные браузеры делят одно хранилище cookies | Дайте каждому браузеру собственный RequestContext |
| После отправки страница снова загружает форму входа | Заполнено только одно поле ответа, колбэк так и не вызвался или токен истёк | Заполните каждое поле, вызовите колбэк и внедряйте токен сразу после решения |
| Is CapSkip running? No connection could be made | CapSkip не запущен либо неверно указаны хост или порт | Запустите CapSkip и сверьте адрес с его настройками подключения |
FAQ
Работает ли это для reCAPTCHA v3 и Enterprise?
Да, с помощью опций, а не другого метода. Страница Enterprise загружает enterprise.js вместо api.js, и тогда адрес anchor содержит /recaptcha/enterprise/, а поиск фрейма выше такой адрес уже находит. Добавьте в опции RecaptchaAsync параметр enterprise со значением 1. На странице v3 нет ни чекбокса, ни textarea внутри формы: её sitekey находится в параметре render у api.js, а токен страница отправляет из собственного скрипта. Её iframe anchor содержит size=invisible, поэтому FindRecaptchaAsync сообщает о ней как о невидимом виджете. Сначала проверьте, есть ли в адресе api.js параметр render=, затем передайте version со значением v3 и action страницы и не указывайте invisible: для v3 SDK эту опцию отклоняет. Опции invisible и enterprise подробнее разобраны в этом руководстве по решению reCAPTCHA v2 на C#.
Выглядит ли браузер OffScreen для сайта как headless?
По очевидным признакам нет. Его user agent представляет собой обычную строку Chrome 152 без HeadlessChrome, navigator.webdriver равен false, а область просмотра по умолчанию имеет размер 1366 на 768 пикселей. Заметны два отличия. В списке брендов, который отправляет браузер, есть Chromium, но нет Google Chrome, а внешний размер окна равен внутреннему, потому что вокруг страницы нет рамки браузера. Сам токен reCAPTCHA получается из решения CapSkip, а не из этого браузера, и в наших тестах использовался тестовый ключ Google, поэтому у нас нет замеров того, какую оценку сайт ставит браузеру CefSharp.
Можно ли не вызывать Cef.Shutdown?
В наших запусках приложение без этого вызова завершалось нормально, примерно через 0.1 секунды после возврата из Main. Но в документации CefSharp он по-прежнему указан как обязательный, а падения, которые мы видели, имели другие причины: Shutdown в неправильном потоке или вторая копия приложения с той же папкой кэша. Оставьте его в блоке finally, в главном потоке, после завершения InitializeAsync.
Может ли приложение работать не на той машине, где запущен CapSkip?
Да. Переведите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю, и передайте этот адрес как host в CapSkipClient. Тогда приложение обращается к нему через тот же HTTP API с любой машины, которой доступен порт. Добавьте блокирующее правило Windows Firewall для адресов, у которых не должно быть к нему доступа. Решатель по-прежнему работает на вашем собственном железе, и ни одно решение не проходит через сторонний сервис.
Коротко
Чтобы решить reCAPTCHA в CefSharp, укажите целевую платформу net10.0-windows или net8.0-windows вместе с RuntimeIdentifier, запускайте CEF через InitializeAsync и оставьте верхний уровень синхронным, чтобы Cef.Shutdown выполнялся в том же потоке. Выполняйте каждый скрипт через GetMainFrame(), читайте sitekey из data-sitekey или из iframe anchor и решайте капчу через RecaptchaAsync. Заполните каждое поле ответа, вызовите колбэк или form.requestSubmit() и начинайте ждать навигацию до отправки формы.
- Все типы капчи, которые поддерживает пакет для .NET, перечислены на странице сервиса распознавания капчи для C#.
- Как работает reCAPTCHA с чекбоксом и что покрывает решатель, описано на странице сервиса распознавания reCAPTCHA v2.
Браузер, который работает на вашем собственном ПК с Windows, заслуживает и решателя, который работает там же. CapSkip представляет собой безлимитный сервис распознавания капчи на той же машине, поэтому решение целой пачки страниц обходится не дороже, чем решение одной.
