CefSharp OffScreen console ऐप में reCAPTCHA कैसे हल करें

CefSharp में reCAPTCHA हल करने के लिए, पेज को OffScreen ChromiumWebBrowser में लोड करें, browser.GetMainFrame() के ज़रिए चलाई गई script से sitekey पढ़ें, और उसे CapSkip के RecaptchaAsync को पास करें। फिर token को हर g-recaptcha-response फ़ील्ड में लिखें और पेज का callback कॉल करें या फॉर्म सबमिट करें। ज़्यादातर पहली कोशिशें CefSharp के दो नियमों पर टूटती हैं। navigation के ठीक बाद सीधे ब्राउज़र पर EvaluateScriptAsync कॉल करने पर लगभग हर बार exception आता है, क्योंकि पेज का JavaScript context अभी तैयार नहीं होता। और Cef.Shutdown को उसी थ्रेड पर चलना चाहिए जिसने CEF शुरू किया था, और async Main इस नियम को चुपचाप तोड़ देता है। यह गाइड एक छोटा console ऐप बनाती है जो इन दोनों को संभालता है, साथ ही invisible widgets, explicit render और एक साथ कई पेजों को भी।
आपको क्या चाहिए
- किसी Windows मशीन पर चल रहा CapSkip, जिसमें reCAPTCHA सॉल्वर चालू हो। CefSharp भी सिर्फ़ Windows पर चलता है, इसलिए दोनों एक ही PC पर साथ चल सकते हैं।
- वर्ज़न 10 वाला .NET SDK, या चरण 1 में net8.0-windows के साथ वर्ज़न 8 वाला .NET SDK। नीचे का कोड .NET 8 और .NET 10 पर चलाया गया, और SDK 10.0.100 से build किया गया।
- NuGet से CefSharp.OffScreen.NETCore और CapSkip पैकेज। यह गाइड CefSharp 152.0.100 इस्तेमाल करती है, जिसके साथ Chromium 152 आता है।
- x64 के लिए Visual C++ 2022 Redistributable। CefSharp की अपनी DLLs इसी से link होती हैं, और CefSharp README में वर्ज़न 138 से VC++ 2022 को ज़रूरी बताया गया है।
- सॉल्वर के लिए एक पता। Local मोड में CapSkip केवल उसी डिवाइस के लिए 127.0.0.1 पर जवाब देता है। Server मोड में वह आपके नेटवर्क पते या पब्लिक IP पर सुनता है, ताकि किसी दूसरी मशीन पर चल रहा ऐप API के ज़रिए उसे कॉल कर सके। दोनों यहाँ सेट किए जाते हैं: कनेक्शन सेटिंग्स.
चरण 1: Windows target और runtime वाला प्रोजेक्ट बनाएँ
console टेम्पलेट से शुरू करें, लेकिन अभी पैकेज न जोड़ें। टेम्पलेट सादा net10.0 target करता है, या .NET 8 SDK पर net8.0, और CefSharp के .NET पैकेज सिर्फ़ Windows targets को सपोर्ट करते हैं।
# 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 target के बिना, dotnet add package NU1202 के साथ रुक जाता है और कहता है कि पैकेज net10.0 के साथ compatible नहीं है, क्योंकि वह net6.0-windows7.0 और उसके बाद के targets को सपोर्ट करता है। RuntimeIdentifier के बिना, build बिना किसी warning के सफल होता है, और फिर ऐप शुरू होते ही CefSharp.Core.Runtime.dll के लिए FileNotFoundException के साथ फेल हो जाता है। यह संदेश गुमराह करता है: जिस फ़ाइल का वह नाम लेता है, वह मौजूद है। असल में libcef.dll गायब है, यानी खुद Chromium engine, जो bin तक तभी पहुँचती है जब build को अपना runtime पता हो। हमारे रन में सिर्फ़ PlatformTarget को x64 पर सेट करने से भी यही error आई।
बड़े build की उम्मीद रखें। पहला restore करीब 250 MB डाउनलोड करता है, और output फ़ोल्डर में 248 फ़ाइलों में करीब 420 MB होते हैं, जिनमें से 285 MB अकेले libcef.dll के हैं। CefSharp.BrowserSubprocess.exe आपकी exe के बगल में रहती है, क्योंकि Chromium अपने renderers को अलग प्रोसेस के रूप में चलाता है।
चरण 2: CEF को एक बार शुरू करें, और उसी थ्रेड पर बंद करें
CEF हर प्रोसेस में एक ही बार शुरू होता है, और उसे उसी थ्रेड पर बंद करना ज़रूरी है जिसने उसे शुरू किया था। CefSharp की General Usage गाइड चेतावनी देती है कि यह नियम तोड़ने पर ऐप hang हो जाता है। वर्ज़न 152 में इसके बजाय exception आता है। इसलिए नीचे का top-level कोड synchronous रहता है, और सिर्फ़ RunAsync में 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}");
}वजह यह है। console ऐप में await के बाद का कोड आमतौर पर thread pool के किसी थ्रेड पर फिर से शुरू होता है। top level पर await रखने से Cef.Shutdown, thread pool के एक थ्रेड पर चला, उस मुख्य थ्रेड पर नहीं जिसने Cef.Initialize चलाया था। तब ऐप एक unhandled exception के साथ बंद हो गया, जिसकी शुरुआत यह है: Cef.Shutdown must be called on the same thread that Cef.Initialize was called। Cef.InitializeAsync को await करना भी इसी तरह फेल होता है, क्योंकि उसका continuation CEF के अपने UI थ्रेड पर चलता है। GetAwaiter().GetResult() से block करने पर Cef.Shutdown मुख्य थ्रेड पर ही रहता है, और RunAsync के exception फेंकने पर भी finally block, CEF को बंद कर देता है।
InitializeAsync इस्तेमाल करें, भले ही कोड उसका synchronously इंतज़ार करता है। यह तभी पूरा होता है जब CEF पूरी तरह चालू हो जाए, और CefSharp का अपना OffScreen उदाहरण भी CEF को इसी तरह शुरू करता है।
RootCachePath एक absolute path होना चाहिए। relative path देने पर तुरंत exception आता है, और उसका संदेश यही बताता है। इसे छोड़ दें, तो CEF, local app data के अंदर एक साझा डिफ़ॉल्ट फ़ोल्डर इस्तेमाल करता है, जिसके बारे में उसका अपना log चेतावनी देता है। cookies और local storage memory में ही रहते हैं, जब तक आप CachePath को भी RootCachePath के अंदर किसी फ़ोल्डर पर सेट न करें, इसलिए हर नया रन बिना किसी session के शुरू होता है। हमारे रन में startup में करीब 0.3 सेकंड लगे, और Shutdown में करीब 0.2।
चरण 3: main frame के ज़रिए sitekey पढ़ें
यही CefSharp का खास जाल है। LoadUrlAsync या WaitForNavigationAsync के ठीक बाद, .NET 10 पर browser.EvaluateScriptAsync ने 20 में से 20 रन में exception फेंका। संदेश की शुरुआत यह है: Unable to execute javascript at this time, scripts can only be executed within a V8Context। पेज का JavaScript context 8 से 18 मिलीसेकंड बाद बना। WaitForInitialLoadAsync के बाद यह बहुत कम बार फेल हुआ, लेकिन CEF के चालू होते ही एक साथ शुरू किए गए पहले तीन ब्राउज़रों में से एक या दो में फिर भी फेल हुआ। browser.GetMainFrame().EvaluateScriptAsync के ज़रिए भेजी गई वही script कभी फेल नहीं हुई, इसलिए इस गाइड की हर script, main frame से होकर जाती है।
// 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 string बिना quotes वाली string के रूप में आती है, array, object की List के रूप में, पूर्णांक, Int32 के रूप में, और null या undefined, null के रूप में। जो script error फेंकती है, वह C# में exception नहीं उठाती: response में Success, false पर सेट होता है और JavaScript error, Message में होती है। लौटाए गए Promise को आपके लिए await कर लिया जाता है।
जो पेज grecaptcha.render() कॉल करता है, वह कोई data-sitekey attribute पीछे नहीं छोड़ता। फिर भी Google का anchor iframe अपने k parameter में key और size parameter में widget का size रखता है, और GetAllFrames() उस iframe को उसके पते के साथ सूची में दिखाता है, चाहे वह cross-origin हो या नहीं, और इसके लिए JavaScript की ज़रूरत नहीं पड़ती। frames का क्रम हर रन में बदला, इसलिए कोड उन्हें स्थिति के बजाय पते से खोजता है।
चरण 4: CapSkip से कैप्चा हल करें
sitekey और पेज का पता CapSkip को भेजें। invisible widget को invisible ऑप्शन चाहिए, और बाकी हर मामले में डिफ़ॉल्ट 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;
}एक नाम पर ध्यान देना होगा। console प्रोजेक्ट के implicit usings के साथ, TimeoutException का सादा catch CS0104 के साथ कंपाइल नहीं होता, क्योंकि System.TimeoutException और CapSkip.TimeoutException दोनों scope में हैं। पूरा CapSkip नाम लिखें। RecaptchaAsync, SDK के recaptchaTimeout के बाद हार मान लेता है, जो डिफ़ॉल्ट रूप से 300 सेकंड है और जिसे आप CapSkipClient constructor में सेट करते हैं। धीमा हल आमतौर पर उससे पहले CapSkip के अंदर ही फेल हो जाता है, और तब ApiException के रूप में आता है। यहाँ await किसी भी थ्रेड पर फिर से शुरू हो सकता है, और इसमें कोई दिक्कत नहीं: WebView2 के उलट, CefSharp किसी भी थ्रेड से script कॉल स्वीकार करता है।
चरण 5: token inject करें और फॉर्म सबमिट करें
token को वहीं रखें जहाँ Google की script उसे रखती, फिर पेज आगे जो भी करता है उसे ट्रिगर करें, और उस पेज का इंतज़ार करें जिस पर फॉर्म पोस्ट होता है।
// 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"; }
}सिर्फ़ पहला नहीं, हर g-recaptcha-response textarea भरें। दो widgets वाले फॉर्म में, या ऐसे फॉर्म में जिसका अपना फ़ील्ड Google के फ़ील्ड के बगल में हो, उस नाम के दो फ़ील्ड होते हैं, और PHP जैसा सर्वर सिर्फ़ आख़िरी वाला रखता है। अगर widget में data-callback attribute है, तो script उस function को token के साथ कॉल करती है, क्योंकि पेज यही उम्मीद करता है। वरना वह form.requestSubmit() कॉल करती है, जो बटन पर क्लिक की तरह पेज के अपने submit handlers और HTML5 validation चलाता है। अगर कोई required फ़ील्ड खाली है, तो requestSubmit() कुछ भी पोस्ट नहीं करता, इसलिए checkValidity वाली लाइन के बिना ऐप पूरे 30 सेकंड इंतज़ार करता और रिपोर्ट करता कि पेज navigate नहीं हुआ। यह जाँच असली वजह तुरंत बता देती है।
WaitForNavigationAsync को submit script के बाद नहीं, उससे पहले शुरू करें। submit के 50 मिलीसेकंड बाद शुरू करने पर इंतज़ार navigation से चूक गया और उसका timeout हो गया। पहले शुरू करने पर इंतज़ार, submit script के लौटने के 10 से 30 मिलीसेकंड बाद पूरा हो गया। WaitForNavigationAsync डिफ़ॉल्ट रूप से पाँच सेकंड इंतज़ार करता है, इसलिए कोड 30 पास करता है। यह समय खत्म होने पर वह System.TimeoutException फेंकता है, इसीलिए वह catch, System को पूरे नाम से लिखता है। token, JsonSerializer.Serialize से बने JSON string literals के रूप में script में जाता है, इसलिए उसमें मौजूद quotes script को तोड़ नहीं सकते। इसके बजाय उसे EvaluateScriptAsync के दूसरे argument के रूप में पास न करें: वह overload पहली string को कॉल किए जाने वाले function का नाम मानता है।
token को फ़ौरन inject करें। Google अपनी reCAPTCHA वेरिफ़िकेशन गाइड में बताता है कि token जारी होने के दो मिनट बाद एक्सपायर हो जाता है। हमारे रन में फॉर्म, CapSkip के लौटने के 16 से 21 मिलीसेकंड बाद पोस्ट हुआ। नीचे का पूरा उदाहरण इन तीनों functions को RunAsync से कॉल करता है।
पूरा console उदाहरण
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 और यह कि widget invisible है या नहीं, token की लंबाई, और उस पेज का टेक्स्ट जिस पर फॉर्म पोस्ट हुआ। बिना widget वाले पेज पर वह यही बताकर रुक जाता है। हमने इसे .NET 8 और .NET 10 पर नौ पेजों पर चलाया। इनमें अपने textarea वाला एक checkbox था, Google के असली api.js और उसकी public test key के साथ वही checkbox था, और callback वाले दो invisible widgets थे, एक Google की script के साथ और एक उसके बिना। बाकी पेजों में invisible size के साथ और उसके बिना explicit render थे, दो response फ़ील्ड वाला पेज था, खाली required फ़ील्ड वाला पेज था, और बिना widget वाला एक पेज था। ऐप ऐसे सॉल्वर की भी रिपोर्ट करता है जो चल नहीं रहा, उस तक पहुँचने की पहली कोशिश के करीब दो सेकंड बाद।
किसी रन का ज़्यादातर समय इंतज़ार में जाता है। पेज पर Google की script होने पर, हल की रिक्वेस्ट पेज माँगे जाने के 0.3 से 1.3 सेकंड बाद CapSkip तक पहुँची, और सबसे ज़्यादा समय cache साफ़ करने के बाद की पहली विज़िट में लगा। इसमें से ज़्यादातर समय api.js और उसके iframe के लोड होने में जाता है। हल के बाद का सब कुछ कुछ दर्जन मिलीसेकंड में हो गया।
दो इंतज़ार ऐसे हैं जिनका अपना कोई timeout नहीं है। WaitForInitialLoadAsync और LoadUrlAsync कभी हार नहीं मानते, और गलत बना पता कभी पूरी तरह लोड नहीं होता: बिना सुरक्षा वाला एक टेस्ट तब भी इंतज़ार कर रहा था जब हमने उसे ढाई मिनट बाद रोका। इसीलिए RunAsync पहले load को WaitAsync में लपेटता है। फेल होने वाला load भी पूरा होता है, बस Success, false पर सेट होता है। 404 पेज अपना status code बताता है, और बंद port करीब दो सेकंड बाद ConnectionRefused बताता है।
एक साथ कई पेज
एक Cef.Initialize कई ब्राउज़रों का काम चला सकता है, और हर हल एक ही समय पर चल सकता है। लेकिन हर ब्राउज़र को उसका अपना RequestContext दें। उसके बिना सभी ब्राउज़र एक ही cookie jar साझा करते हैं, इसलिए हर login पेज बाकी ब्राउज़रों की session cookie को overwrite कर देता है।
// 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));
}));साझा context पर तीन ब्राउज़रों ने एक साथ हल किया, और उनकी तीन में से दो पोस्ट CSRF mismatch की वजह से ठुकरा दी गईं, क्योंकि cookie अब फॉर्म से मेल नहीं खाती थी। हर ब्राउज़र का अपना RequestContext होने पर तीनों स्वीकार हुईं। top level से RunAsync की जगह RunManyAsync कॉल करें, उसी try और finally के अंदर। तब compiler चेतावनी देता है कि RunAsync और SiteUrl इस्तेमाल नहीं हुए, जो हानिरहित है। CapSkip का reCAPTCHA सेक्शन डिफ़ॉल्ट रूप से एक समय में 10 हल चलाता है, जो Max. Threads से तय होता है, और बाकी को क्यू में रखता है।
ऐप की दो कॉपी एक RootCachePath साझा नहीं कर सकतीं। दूसरी कॉपी करीब 0.1 सेकंड के अंदर शुरू होने में फेल हो गई, इसलिए ऊपर का कोड अपना InvalidOperationException फेंकता है, इस संदेश के साथ: CEF did not start: NormalExitProcessNotified। इससे भी बुरा, जो कॉपी पहले से चल रही थी, वह बाद में Cef.Shutdown पर crash हो गई, आठ में से आठ रन में। हर चलती कॉपी को उसका अपना फ़ोल्डर दें। अगर फिर भी दूसरी बार शुरू होना संभव है, तो यह handler जोड़ें, जिसने हर रन में crash रोक दिया।
// 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 में क्या बदलता है
डेस्कटॉप ऐप के अंदर CefSharp में reCAPTCHA हल करने के लिए, CefSharp.WinForms.NETCore या CefSharp.Wpf.NETCore पैकेज और यही चरण इस्तेमाल करें। कंट्रोल CEF को खुद शुरू और बंद करता है, इसलिए आप Cef.Shutdown कॉल नहीं करते। अगर आप अपनी सेटिंग्स चाहते हैं, तो पहला कंट्रोल बनने से पहले उनके साथ Cef.Initialize कॉल करें। scripts, frames और CapSkip कॉल बिल्कुल ऊपर की तरह काम करते हैं। बदलता यह है कि events कहाँ पहुँचते हैं।
// 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 को अपने थ्रेड पर उठाता है, उस थ्रेड पर नहीं जो आपके फॉर्म का मालिक है। script कॉल खुद किसी थ्रेड से बँधी नहीं हैं: EvaluateScriptAsync ने UI थ्रेड से, ConfigureAwait(false) के बाद, और Task.Run के अंदर, तीनों जगह काम किया। यह WebView2 का उल्टा है, जहाँ दूसरे थ्रेड से कोई भी कॉल exception फेंकती है। इसलिए CefSharp में नियम आपके अपने controls के बारे में है। WinForms में event से Label सेट करने पर Cross-thread operation not valid सिर्फ़ debugger जुड़ा होने पर आया, क्योंकि WinForms cross-thread कॉल की जाँच सिर्फ़ तभी करता है। debugger के बिना टेक्स्ट बदल गया और कुछ भी रिपोर्ट नहीं हुआ, इसलिए release build इस bug को छिपा देता है, और फॉर्म बाद में भी गड़बड़ कर सकता है। BeginInvoke अपडेट को फॉर्म के थ्रेड पर चलाता है। WPF में वही अपडेट हमेशा exception फेंकता है, इस संदेश के साथ: The calling thread cannot access this object because a different thread owns it। वहाँ कंट्रोल का Dispatcher.InvokeAsync इस्तेमाल करें।
CapSkip को किसी दूसरी मशीन पर चलाना
ऐप और सॉल्वर का एक ही PC पर होना ज़रूरी नहीं। कनेक्शन सेटिंग्स में 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 key validation चालू करें, क्लाइंट में भी वही key रखें। Server मोड शुरू होते ही CapSkip अपना पोर्ट हर पते के लिए खोल देता है, इसलिए उसे सिर्फ़ अपेक्षित मशीनों तक सीमित रखने के लिए बाकी पतों के लिए Windows Firewall में एक block नियम जोड़ें। उसी मशीन पर localhost के बजाय 127.0.0.1 लिखें, क्योंकि पिछले एक टेस्ट में localhost ने पहले IPv6 पता आज़माया और हर नए कनेक्शन पर करीब दो सेकंड गँवाए। सॉल्वर अब भी आपकी अपनी Windows मशीन है, और हल अब भी बिना मीटर वाले हैं।
CefSharp में reCAPTCHA हल करते समय आम errors
| आप जो देखते हैं | कारण | फिक्स |
|---|---|---|
| पैकेज जोड़ते समय NU1202 | प्रोजेक्ट, Windows suffix के बिना net10.0 को target करता है | target को net10.0-windows या net8.0-windows पर सेट करें |
| CefSharp.Core.Runtime.dll के लिए FileNotFoundException | RuntimeIdentifier नहीं है, इसलिए libcef.dll कभी bin तक नहीं पहुँची | RuntimeIdentifier win-x64 जोड़ें और दोबारा build करें |
| Cef.Shutdown must be called on the same thread | top level पर लगे await ने कोड को दूसरे थ्रेड पर भेज दिया | top level को synchronous रखें और RunAsync पर block करें |
| Unable to execute javascript at this time | नए पेज का JavaScript context बनने से पहले script ब्राउज़र पर चल गई | इसे browser.GetMainFrame() के ज़रिए चलाएँ |
| ऐप कभी पूरी तरह लोड नहीं होता | पता गलत बना है, और load के इंतज़ार का कोई timeout नहीं है | इंतज़ार को timeout के साथ WaitAsync में लपेटें |
| एक ब्राउज़र की पोस्ट स्वीकार होती है, लेकिन बाकी की ठुकरा दी जाती हैं | एक साथ चलते ब्राउज़र एक ही cookie jar साझा करते हैं | हर ब्राउज़र को उसका अपना RequestContext दें |
| पोस्ट के बाद पेज login फॉर्म फिर से लोड करता है | सिर्फ़ एक response फ़ील्ड भरा गया, callback कभी नहीं चला, या token एक्सपायर हो गया | हर फ़ील्ड भरें, callback कॉल करें, और हल के ठीक बाद inject करें |
| Is CapSkip running? No connection could be made | CapSkip चल नहीं रहा, या host या port गलत है | CapSkip शुरू करें और पते को उसकी कनेक्शन सेटिंग्स से मिलाकर जाँचें |
FAQ
क्या यह reCAPTCHA v3 और Enterprise के लिए काम करता है?
हाँ, किसी अलग method से नहीं, बल्कि ऑप्शंस से। Enterprise पेज api.js की जगह enterprise.js लोड करता है, और तब anchor के पते में /recaptcha/enterprise/ होता है, जिसे ऊपर वाली frame खोज पहले से पकड़ लेती है। RecaptchaAsync के ऑप्शंस में enterprise को 1 पर सेट करके जोड़ें। v3 पेज के फॉर्म में न कोई checkbox होता है, न कोई textarea: उसका sitekey, api.js के render parameter में होता है, और पेज token अपनी script से भेजता है। उसका anchor iframe size=invisible बताता है, इसलिए FindRecaptchaAsync उसे invisible widget के रूप में रिपोर्ट करता है। पहले api.js के पते में render= देखें, फिर version को v3 पर सेट करके पास करें, साथ में पेज का action भी, और invisible को छोड़ दें, जिसे SDK v3 के लिए मना कर देता है। invisible और enterprise ऑप्शंस यहाँ और विस्तार से कवर किए गए हैं: C# में reCAPTCHA v2 हल करने की गाइड.
क्या OffScreen ब्राउज़र साइट को headless दिखता है?
साफ़ दिखने वाले संकेतों से नहीं। इसका user agent एक सामान्य Chrome 152 string है जिसमें HeadlessChrome नहीं है, navigator.webdriver, false है, और डिफ़ॉल्ट view 1366 x 768 पिक्सल का है। दो फ़र्क दिखते हैं। यह जो browser brands भेजता है, उनमें Chromium का नाम है लेकिन Google Chrome का नहीं, और विंडो का बाहरी size उसके अंदरूनी size के बराबर है, क्योंकि पेज के चारों ओर कोई browser frame नहीं होता। reCAPTCHA token खुद CapSkip के हल से आता है, इस ब्राउज़र से नहीं, और हमारे टेस्ट में Google की test key इस्तेमाल हुई, इसलिए हमारे पास इसका कोई माप नहीं है कि कोई साइट CefSharp ब्राउज़र को कैसा score देती है।
क्या मैं Cef.Shutdown छोड़ सकता हूँ?
हमारे रन में, इसे छोड़ने वाला ऐप सामान्य रूप से exit हुआ, Main के लौटने के करीब 0.1 सेकंड बाद। फिर भी CefSharp इसे ज़रूरी बताता है, और हमें जो crash दिखे, उनकी वजहें दूसरी थीं: गलत थ्रेड पर Shutdown, या उसी cache फ़ोल्डर पर चल रही ऐप की दूसरी कॉपी। इसे finally block में, मुख्य थ्रेड पर, InitializeAsync के पूरा होने के बाद रखें।
क्या ऐप CapSkip से अलग किसी मशीन पर चल सकता है?
हाँ। कनेक्शन सेटिंग्स में CapSkip को Server मोड पर कर दें, ताकि वह loopback के बजाय किसी नेटवर्क पते पर सुने, और वही पता CapSkipClient के host के रूप में पास करें। तब ऐप उसे उसी HTTP API के ज़रिए किसी भी ऐसी मशीन से कॉल करता है जो पोर्ट तक पहुँच सकती है। जिन पतों को उस तक नहीं पहुँचना चाहिए, उनके लिए Windows Firewall में एक block नियम जोड़ें। सॉल्वर अब भी आपके अपने हार्डवेयर पर चलता है, और कोई भी हल किसी third-party सर्विस से होकर नहीं जाता।
संक्षेप में
CefSharp में reCAPTCHA हल करने के लिए, RuntimeIdentifier के साथ net10.0-windows या net8.0-windows को target करें, CEF को InitializeAsync से शुरू करें और top level को synchronous रखें, ताकि Cef.Shutdown उसी थ्रेड पर चले। हर script को GetMainFrame() के ज़रिए चलाएँ, sitekey को data-sitekey या anchor iframe से पढ़ें, और उसे RecaptchaAsync से हल करें। हर response फ़ील्ड भरें, callback या form.requestSubmit() कॉल करें, और सबमिट करने से पहले navigation का इंतज़ार शुरू कर दें।
- कैप्चा के वे सभी टाइप जिन्हें .NET पैकेज संभालता है: C# कैप्चा सॉल्वर पेज.
- checkbox वाला reCAPTCHA कैसे काम करता है और सॉल्वर क्या-क्या कवर करता है: reCAPTCHA v2 सॉल्वर पेज.
जो ब्राउज़र आपके अपने Windows PC पर चलता है, उसके लिए सॉल्वर भी वहीं चलना चाहिए। CapSkip उसी मशीन पर चलने वाला एक असीमित कैप्चा सॉल्वर है, इसलिए पेजों का पूरा बैच हल करने में एक अकेले पेज से ज़्यादा खर्च नहीं आता।
