CefSharp OffScreen Konsol Uygulamalarında reCAPTCHA Nasıl Çözülür

solve recaptcha in cefsharp - How to Solve reCAPTCHA in CefSharp OffScreen Console Apps

CefSharp’ta reCAPTCHA çözmek için sayfayı OffScreen bir ChromiumWebBrowser içinde yükleyin, sitekey’i browser.GetMainFrame() üzerinden çalıştırılan bir script ile okuyun ve onu CapSkip’in RecaptchaAsync metoduna verin. Ardından token’ı her g-recaptcha-response alanına yazın ve sayfanın callback’ini çağırın ya da formu gönderin. İlk denemelerin çoğunu iki CefSharp kuralı bozar. Bir gezinmenin hemen ardından EvaluateScriptAsync’i doğrudan tarayıcının kendisi üzerinde çağırmak neredeyse her seferinde istisna fırlatır, çünkü sayfanın JavaScript bağlamı henüz hazır değildir. Ayrıca Cef.Shutdown, CEF’i başlatan thread üzerinde çalışmalıdır ve async bir Main bu kuralı sessizce bozar. Bu rehber ikisini de ele alan küçük bir konsol uygulaması oluşturur; görünmez widget’ları, açıkça render edilen widget’ları ve aynı anda birkaç sayfayı da kapsar.

Neye ihtiyacınız var

  • reCAPTCHA çözücüsü açık hâlde bir Windows makinesinde çalışan CapSkip. CefSharp de yalnızca Windows’ta çalışır, bu yüzden ikisi aynı bilgisayarı paylaşabilir.
  • SDK olarak .NET 10 ya da (1. adımda net8.0-windows ile) .NET 8. Aşağıdaki kod .NET 8 ve .NET 10 üzerinde çalıştırıldı ve SDK 10.0.100 ile derlendi.
  • NuGet’ten CefSharp.OffScreen.NETCore ve CapSkip paketleri. Bu rehber, Chromium 152 ile gelen CefSharp 152.0.100 sürümünü kullanır.
  • x64 için Visual C++ 2022 Redistributable. CefSharp’ın kendi DLL’leri ona bağlanır ve CefSharp README dosyası, 138 sürümünden itibaren VC++ 2022’yi gerekli olarak listeler.
  • Çözücü için bir adres. Local modunda CapSkip 127.0.0.1 üzerinden yalnızca o cihaza yanıt verir. Server modunda ise ağ adresinizi veya genel IP’nizi dinler, böylece başka bir makinedeki bir uygulama onu API üzerinden çağırabilir. İkisi de şurada ayarlanır: bağlantı ayarları.

1. Adım: Windows hedefli ve runtime tanımlı bir proje oluşturun

Konsol şablonundan başlayın, ancak paketleri henüz eklemeyin. Şablon düz net10.0 hedefini kullanır (.NET 8 SDK’sında net8.0), CefSharp’ın .NET paketleri ise yalnızca Windows hedeflerini destekler.

# create a console app, then edit CefApp.csproj as shown below before adding packages
dotnet new console -n CefApp
cd CefApp

CefApp.csproj dosyasını açın ve içindeki PropertyGroup bölümünü bununla değiştirin.

<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>

Ardından iki paketi ekleyin.

# the first restore downloads about 250 MB, most of it Chromium for win-x64
dotnet add package CefSharp.OffScreen.NETCore
dotnet add package CapSkip

O PropertyGroup içindeki iki satır da gerçek bir hatayı önler. Windows hedefi olmadan dotnet add package NU1202 ile durur ve paketin net10.0 ile uyumlu olmadığını söyler, çünkü paket net6.0-windows7.0 ve sonrasını destekler. RuntimeIdentifier olmadan derleme hiçbir uyarı vermeden başarılı olur, ardından uygulama başlangıçta CefSharp.Core.Runtime.dll için bir FileNotFoundException ile başarısız olur. Bu mesaj yanıltıcıdır: adını verdiği dosya oradadır. Eksik olan, Chromium motorunun kendisi olan libcef.dll dosyasıdır ve bu dosya bin klasörüne ancak derleme kendi runtime’ını bildiğinde ulaşır. Bizim çalıştırmalarımızda yalnızca PlatformTarget değerini x64 yapmak aynı hatayı verdi.

Büyük bir derleme bekleyin. İlk restore yaklaşık 250 MB indirir ve çıktı klasörü 248 dosyada yaklaşık 420 MB tutar; bunun 285 MB’ı libcef.dll dosyasıdır. CefSharp.BrowserSubprocess.exe, exe dosyanızın yanında durur, çünkü Chromium render süreçlerini ayrı süreçler olarak çalıştırır.

2. Adım: CEF’i bir kez başlatın ve aynı thread üzerinde durdurun

CEF her süreçte bir kez başlar ve onu başlatan thread üzerinde kapatılmalıdır. CefSharp’ın General Usage rehberi bu kuralı çiğnemenin uygulamayı kilitlediği konusunda uyarır. 152 sürümünde ise kilitlenme yerine bir istisna fırlatılır. Bu yüzden aşağıdaki üst düzey kod senkron kalır ve yalnızca RunAsync await kullanır.

// 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}");
}

Nedeni şu. Bir konsol uygulamasında await’ten sonraki kod genellikle thread havuzundaki bir thread üzerinde devam eder. Üst düzeyde await kullanıldığında Cef.Shutdown, Cef.Initialize’ı çalıştıran ana thread üzerinde değil, thread havuzundaki bir thread üzerinde çalıştı. Uygulama ardından şöyle başlayan işlenmemiş bir istisnayla çöktü: Cef.Shutdown must be called on the same thread that Cef.Initialize was called. Cef.InitializeAsync’i await etmek de aynı şekilde başarısız olur, çünkü devamı (continuation) CEF’in kendi UI thread’inde çalışır. GetAwaiter().GetResult() ile engelleyerek beklemek Cef.Shutdown’ı ana thread üzerinde tutar ve RunAsync istisna fırlattığında finally bloğu yine de CEF’i kapatır.

Kod onu senkron olarak beklese de InitializeAsync kullanın. Bu metot ancak CEF tamamen ayağa kalktığında tamamlanır; CefSharp’ın kendi OffScreen örneği de CEF’i bu şekilde başlatır.

RootCachePath mutlak bir yol olmalıdır. Göreli bir yol, bunu söyleyen bir mesajla hemen istisna fırlatır. Hiç belirtmezseniz CEF, yerel uygulama verileri (local app data) altında paylaşılan varsayılan bir klasör kullanır ve kendi logu da bu konuda uyarır. CachePath değerini de RootCachePath içindeki bir klasöre ayarlamadığınız sürece çerezler ve local storage bellekte kalır; bu yüzden yeni bir çalıştırma oturumsuz başlar. Bizim çalıştırmalarımızda başlatma yaklaşık 0.3 saniye, Shutdown ise yaklaşık 0.2 saniye sürdü.

3. Adım: sitekey’i ana frame üzerinden okuyun

CefSharp’a özgü tuzak budur. LoadUrlAsync ya da WaitForNavigationAsync’ten hemen sonra browser.EvaluateScriptAsync, .NET 10 üzerinde 20 çalıştırmanın 20’sinde istisna fırlattı. Mesaj şöyle başlar: Unable to execute javascript at this time, scripts can only be executed within a V8Context. Sayfanın JavaScript bağlamı 8 ile 18 milisaniye sonra ortaya çıktı. WaitForInitialLoadAsync sonrasında çok daha seyrek başarısız oldu, ancak CEF ayağa kalktıktan hemen sonra paralel başlatılan ilk üç tarayıcının bir ya da ikisinde yine başarısız oldu. browser.GetMainFrame().EvaluateScriptAsync üzerinden gönderilen aynı script ise hiç başarısız olmadı; bu yüzden bu rehberdeki her script ana frame üzerinden gider.

// 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, JSON değil gerçek .NET değerleri döndürür. Bir JavaScript dizesi etrafında tırnak olmayan bir dize olarak, bir dizi (array) object türünde bir List olarak, bir tam sayı Int32 olarak, null ya da undefined ise null olarak gelir. Hata fırlatan bir script C# tarafında istisna oluşturmaz: yanıtta Success false olur ve JavaScript hatası Message içinde yer alır. Döndürülen bir Promise sizin yerinize await edilir.

grecaptcha.render() çağıran bir sayfa geride data-sitekey özniteliği bırakmaz. Google’ın anchor iframe’i yine de anahtarı k parametresinde, widget boyutunu da size parametresinde taşır; GetAllFrames() da bu iframe’i cross-origin olsun ya da olmasın, hiç JavaScript gerekmeden adresiyle birlikte listeler. Frame sırası çalıştırmadan çalıştırmaya değişti, bu yüzden kod konuma göre değil adrese göre arar.

4. Adım: CAPTCHA’yı CapSkip ile çözün

Sitekey’i ve sayfa adresini CapSkip’e gönderin. Görünmez bir widget invisible seçeneğine ihtiyaç duyar; geri kalan her şey varsayılan onay kutusu (checkbox) çağrısıdır.

// 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;
}

Bir ada dikkat etmek gerekir. Bir konsol projesinin örtük using listesiyle, TimeoutException’ı nitelemeden yakalayan bir catch bloğu CS0104 hatasıyla derlenmez, çünkü hem System.TimeoutException hem de CapSkip.TimeoutException kapsam içindedir. Tam CapSkip adını yazın. RecaptchaAsync, SDK’nın recaptchaTimeout süresi dolunca vazgeçer; bu süre varsayılan olarak 300 saniyedir ve CapSkipClient yapıcısında (constructor) ayarlanır. Yavaş bir çözüm genellikle daha önce CapSkip’in kendi içinde başarısız olur ve size bir ApiException olarak ulaşır. Buradaki await herhangi bir thread üzerinde devam edebilir ve bu sorun değildir: WebView2’nin aksine CefSharp, script çağrılarını her thread’den kabul eder.

5. Adım: token’ı enjekte edin ve formu gönderin

Token’ı Google’ın script’inin koyacağı yere koyun, ardından sayfanın bir sonraki adımı neyse onu tetikleyin ve formun gönderildiği sayfayı bekleyin.

// 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"; }
}

Yalnızca ilkini değil, her g-recaptcha-response textarea’sını doldurun. İki widget’ı olan ya da Google’ınkinin yanında kendi alanı bulunan bir formda bu ada sahip iki alan vardır ve PHP gibi bir sunucu yalnızca sonuncusunu tutar. Widget’ın bir data-callback özniteliği varsa script o fonksiyonu token ile çağırır, çünkü sayfa bunu bekler. Aksi hâlde form.requestSubmit() çağırır; bu metot, düğmeye tıklanmış gibi sayfanın kendi submit işleyicilerini ve HTML5 doğrulamasını çalıştırır. Zorunlu bir alan boşsa requestSubmit() hiçbir şey göndermez; bu yüzden checkValidity satırı olmasaydı uygulama 30 saniyenin tamamını bekler ve sayfanın gezinmediğini bildirirdi. Bu kontrol asıl nedeni hemen söyler.

WaitForNavigationAsync’i gönderim script’inden sonra değil, önce başlatın. Gönderimden 50 milisaniye sonra başlatıldığında bekleme gezinmeyi kaçırdı ve zaman aşımına uğradı. Önce başlatıldığında ise bekleme, gönderim script’i döndükten 10 ile 30 milisaniye sonra tamamlandı. WaitForNavigationAsync varsayılan olarak beş saniye bekler, bu yüzden kod ona 30 saniye verir. Bu süre dolduğunda System.TimeoutException fırlatır; o catch bloğunun System adını tam yazmasının nedeni budur. Token, script’e JsonSerializer.Serialize ile oluşturulmuş JSON dize sabitleri (string literal) olarak girer, böylece içindeki tırnaklar script’i bozamaz. Bunun yerine onu EvaluateScriptAsync’e ikinci bir argüman olarak vermeyin: o overload, ilk dizeyi çağrılacak bir fonksiyonun adı olarak okur.

Token’ı hemen enjekte edin. Google, reCAPTCHA doğrulama rehberinde bir token’ın verildikten iki dakika sonra süresinin dolduğunu belgeliyor. Bizim çalıştırmalarımızda form, CapSkip yanıt döndürdükten 16 ile 21 milisaniye sonra gönderildi. Aşağıdaki tam örnek bu üç fonksiyonu RunAsync içinden çağırır.

Tam konsol örneği

Program.cs dosyasını aşağıdaki kodla değiştirin, SiteUrl değerini kendi sayfanıza ayarlayın ve dotnet run ile çalıştırın.

// 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"; }
}

Uygulama her aşamayı yazdırır: sayfanın HTTP durumu, sitekey ve widget’ın görünmez olup olmadığı, token’ın uzunluğu ve formun gönderildiği sayfanın metni. Widget olmayan bir sayfada bunu söyler ve durur. Uygulamayı .NET 8 ve .NET 10 üzerinde dokuz sayfaya karşı çalıştırdık. Bu sayfalar şunlardı: kendi textarea’sı olan bir onay kutusu, Google’ın gerçek api.js dosyası ve herkese açık test anahtarıyla aynı onay kutusu ve biri Google’ın script’iyle, diğeri onsuz, callback içeren iki görünmez widget. Geri kalanlar ise invisible boyutu olan ve olmayan açıkça render edilmiş widget’lar, iki yanıt alanı, boş bir zorunlu alan ve widget içermeyen bir sayfaydı. Uygulama, çalışmayan bir çözücüyü de ona ulaşmaya yönelik ilk denemeden yaklaşık iki saniye sonra bildirir.

Bir çalıştırmanın çoğu beklemeyle geçer. Sayfada Google’ın script’i varken çözüm isteği, sayfa istendikten 0.3 ile 1.3 saniye sonra CapSkip’e ulaştı; en uzunu önbellek temizlendikten sonraki ilk ziyaretti. Bu sürenin çoğu api.js ve iframe’inin yüklenmesidir. Çözümden sonraki her şey birkaç düzine milisaniye sürdü.

İki beklemenin kendine ait bir zaman aşımı yoktur. WaitForInitialLoadAsync ve LoadUrlAsync asla vazgeçmez ve hatalı biçimlendirilmiş bir adres hiçbir zaman yüklenmeyi bitirmez: korumasız bir test, iki buçuk dakika sonra durdurduğumuzda hâlâ bekliyordu. RunAsync’in ilk yüklemeyi WaitAsync içine sarmasının nedeni budur. Başarısız olan bir yükleme de yine tamamlanır; Success değeri false olur. Bir 404 sayfası kendi durum kodunu bildirir, kapalı bir port ise yaklaşık iki saniye sonra ConnectionRefused bildirir.

Aynı anda birkaç sayfa

Tek bir Cef.Initialize birçok tarayıcıya hizmet edebilir ve her çözüm aynı anda çalışabilir. Yine de her tarayıcıya kendi RequestContext’ini verin. Bu olmadan tüm tarayıcılar tek bir çerez deposunu paylaşır; bu yüzden her giriş sayfası diğerlerinin oturum çerezinin üzerine yazar.

// 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));
    }));

Paylaşılan bağlamdaki üç tarayıcı paralel olarak çözdü ve üç gönderimden ikisi CSRF uyuşmazlığı nedeniyle reddedildi, çünkü çerez artık formla eşleşmiyordu. Her birine ayrı bir RequestContext verildiğinde üçü de kabul edildi. Üst düzeyden RunAsync yerine RunManyAsync’i, aynı try ve finally blokları içinde çağırın. Derleyici bu durumda RunAsync ile SiteUrl kullanılmıyor diye uyarı verir; bu zararsızdır. CapSkip’in reCAPTCHA bölümü varsayılan olarak aynı anda 10 çözüm çalıştırır (Max. Threads ayarı) ve geri kalanını sıraya alır.

Uygulamanın iki kopyası tek bir RootCachePath klasörünü paylaşamaz. İkinci kopya yaklaşık 0.1 saniye içinde başlayamadı, bu yüzden yukarıdaki kod CEF did not start: NormalExitProcessNotified mesajıyla kendi InvalidOperationException’ını fırlatır. Daha kötüsü, zaten çalışmakta olan kopya sekiz çalıştırmanın sekizinde de daha sonra, Cef.Shutdown sırasında çöktü. Çalışan her kopyaya kendi klasörünü verin. İkinci bir başlatma yine de olabiliyorsa, her çalıştırmada çökmeyi önleyen şu işleyiciyi ekleyin.

// 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 veya WPF’te neler değişir

Bir masaüstü uygulamasında CefSharp ile reCAPTCHA çözmek için CefSharp.WinForms.NETCore ya da CefSharp.Wpf.NETCore paketini ve aynı adımları kullanın. Kontrol CEF’i kendisi başlatıp durdurur, bu yüzden Cef.Shutdown çağırmazsınız. Kendi ayarlarınızı kullanmak istiyorsanız ilk kontrol oluşturulmadan önce Cef.Initialize’ı bu ayarlarla çağırın. Script’ler, frame’ler ve CapSkip çağrısı tam olarak yukarıdaki gibi çalışır. Değişen şey, olayların nereye ulaştığıdır.

// 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 ve AddressChanged olaylarını formunuzun sahibi olan thread üzerinde değil, kendi thread’i üzerinde tetikler. Script çağrılarının kendisi ise bir thread’e bağlı değildir: EvaluateScriptAsync UI thread’inden, ConfigureAwait(false) sonrasında ve Task.Run içinde çalıştı. Bu, başka bir thread’den yapılan her çağrının istisna fırlattığı WebView2’nin tam tersidir. Dolayısıyla CefSharp’taki kural sizin kendi kontrollerinizle ilgilidir. WinForms’ta olaydan bir Label ayarlamak, Cross-thread operation not valid hatasını yalnızca debugger bağlıyken fırlattı, çünkü WinForms thread’ler arası çağrıları yalnızca o durumda denetler. Debugger olmadan metin değişti ve hiçbir şey bildirilmedi; yani bir release derlemesi hatayı gizler ve form daha sonra yine de tuhaf davranabilir. BeginInvoke, güncellemeyi formun thread’i üzerinde çalıştırır. WPF’te aynı güncelleme her zaman The calling thread cannot access this object because a different thread owns it mesajıyla istisna fırlatır. Orada kontrolün Dispatcher.InvokeAsync metodunu kullanın.

CapSkip’i başka bir makinede çalıştırmak

Uygulama ile çözücünün aynı bilgisayarı paylaşması gerekmez. Bağlantı ayarlarından CapSkip’i Server moduna alın; ağ adresinizi veya genel IP’nizi dinlemeye başlar. Ardından istemciyi oraya yönlendirin.

// 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);

Rota internetten geçiyorsa statik bir genel IP kullanın ve API anahtarı doğrulamasını açıp aynı anahtarı istemcide de kullanın. CapSkip, Server modu başladığında portunu her adrese açar; portu yalnızca beklediğiniz makinelere açık tutmak için diğerlerine yönelik bir Windows Firewall engelleme kuralı ekleyin. Aynı makinede localhost yerine 127.0.0.1 yazın; daha önceki bir testte localhost önce IPv6 adresini denedi ve her yeni bağlantıda yaklaşık iki saniyeye mal oldu. Çözücü hâlâ sizin kendi Windows makinenizdir ve çözüm başına ücretlendirme hâlâ yoktur.

CefSharp’ta reCAPTCHA çözerken sık görülen hatalar

GördüğünüzNedenDüzeltme
Paket eklenirken NU1202Proje, Windows son eki olmayan net10.0 hedefini kullanıyorHedefi net10.0-windows ya da net8.0-windows yapın
CefSharp.Core.Runtime.dll için FileNotFoundExceptionRuntimeIdentifier yok, bu yüzden libcef.dll hiç bin klasörüne ulaşmadıRuntimeIdentifier olarak win-x64 ekleyin ve yeniden derleyin
Cef.Shutdown must be called on the same threadÜst düzeydeki bir await kodu başka bir thread’e taşıdıÜst düzeyi senkron tutun ve RunAsync’i engelleyerek bekleyin
Unable to execute javascript at this timeScript, yeni sayfanın JavaScript bağlamı oluşmadan önce tarayıcı üzerinde çalıştıScript’i browser.GetMainFrame() üzerinden çalıştırın
Uygulama yüklemeyi hiç bitirmiyorAdres hatalı biçimlendirilmiş ve yükleme beklemesinin zaman aşımı yokBeklemeyi zaman aşımı olan bir WaitAsync içine sarın
Gönderim bir tarayıcı için kabul ediliyor ama diğerleri için reddediliyorParalel tarayıcılar tek bir çerez deposunu paylaşıyorHer tarayıcıya kendi RequestContext’ini verin
Sayfa gönderimden sonra giriş formunu yeniden yüklüyorYalnızca bir yanıt alanı dolduruldu, callback hiç çalışmadı ya da token’ın süresi dolduHer alanı doldurun, callback’i çağırın ve token’ı çözümden hemen sonra enjekte edin
Is CapSkip running? No connection could be madeCapSkip çalışmıyor ya da host veya port yanlışCapSkip’i başlatın ve adresi bağlantı ayarlarıyla karşılaştırın

FAQ

Bu, reCAPTCHA v3 ve Enterprise için de çalışır mı?

Evet, farklı bir metotla değil, seçeneklerle. Bir Enterprise sayfası api.js yerine enterprise.js yükler ve anchor adresi bu durumda /recaptcha/enterprise/ içerir; yukarıdaki frame araması bunu zaten yakalar. RecaptchaAsync seçeneklerine 1 değeriyle enterprise ekleyin. Bir v3 sayfasında form içinde ne onay kutusu ne de textarea vardır: sitekey’i api.js dosyasının render parametresinde bulunur ve sayfa token’ı kendi script’inden gönderir. Anchor iframe’inde size=invisible yazar, bu yüzden FindRecaptchaAsync onu görünmez bir widget olarak bildirir. Önce api.js adresinde render= olup olmadığını kontrol edin, ardından version değerini v3 olarak ve sayfanın action değerini verin; SDK’nın v3 için reddettiği invisible seçeneğini ise eklemeyin. invisible ve enterprise seçenekleri şurada daha ayrıntılı ele alınıyor: C# ile reCAPTCHA v2 çözme rehberi.

OffScreen bir tarayıcı siteye headless gibi mi görünür?

Bariz işaretlere bakılırsa hayır. User agent’ı, içinde HeadlessChrome geçmeyen sıradan bir Chrome 152 dizesidir, navigator.webdriver false değerindedir ve varsayılan görünüm 1366 x 768 pikseldir. İki fark görülebilir. Gönderdiği tarayıcı markaları (brands) Chromium’u içerir ama Google Chrome’u içermez; pencerenin dış boyutu da iç boyutuna eşittir, çünkü sayfanın etrafında bir tarayıcı çerçevesi yoktur. reCAPTCHA token’ının kendisi bu tarayıcıdan değil, CapSkip’in çözümünden gelir ve testlerimizde Google’ın test anahtarını kullandık; bu yüzden bir sitenin CefSharp tarayıcısını nasıl puanladığına dair bir ölçümümüz yok.

Cef.Shutdown’ı atlayabilir miyim?

Bizim çalıştırmalarımızda onu atlayan bir uygulama, Main döndükten yaklaşık 0.1 saniye sonra normal şekilde çıktı. CefSharp yine de onu zorunlu olarak belgeliyor; gördüğümüz çökmelerin ise başka nedenleri vardı: yanlış thread üzerinde çağrılan Shutdown ya da aynı önbellek klasörünü kullanan ikinci bir uygulama kopyası. Onu finally bloğunda, ana thread üzerinde ve InitializeAsync tamamlandıktan sonra tutun.

Uygulama CapSkip’ten farklı bir makinede çalışabilir mi?

Evet. Bağlantı ayarlarından CapSkip’i Server moduna alın; böylece loopback yerine bir ağ adresini dinler. Ardından bu adresi CapSkipClient’ın host değeri olarak verin. Uygulama bundan sonra porta ulaşabilen her makineden onu aynı HTTP API üzerinden çağırır. Porta ulaşmaması gereken adresler için bir Windows Firewall engelleme kuralı ekleyin. Çözücü yine sizin sahip olduğunuz donanımda çalışır ve hiçbir çözüm üçüncü taraf bir hizmetten geçmez.

Kısa özet

CefSharp’ta reCAPTCHA çözmek için bir RuntimeIdentifier ile net10.0-windows ya da net8.0-windows hedefleyin, CEF’i InitializeAsync ile başlatın ve Cef.Shutdown aynı thread üzerinde çalışsın diye üst düzeyi senkron tutun. Her script’i GetMainFrame() üzerinden çalıştırın, sitekey’i data-sitekey özniteliğinden ya da anchor iframe’inden okuyun ve RecaptchaAsync ile çözün. Her yanıt alanını doldurun, callback’i ya da form.requestSubmit() metodunu çağırın ve göndermeden önce gezinmeyi beklemeye başlayın.

Kendi Windows bilgisayarınızda çalışan bir tarayıcı, aynı yerde çalışan bir çözücüyü hak eder. CapSkip, o aynı makinede çalışan bir sınırsız captcha çözücü olduğu için, bir grup sayfayı çözmek tek bir sayfayı çözmekten daha pahalıya gelmez.