C# में Minimal API कैप्चा सॉल्वर endpoint कैसे बनाएँ

minimal api captcha solver - How to Build a Minimal API CAPTCHA Solver Endpoint in C#

Minimal API कैप्चा सॉल्वर बनाने के लिए ASP.NET Core में एक CapSkipClient को singleton के रूप में रजिस्टर करें। एक POST endpoint दें जो sitekey और पेज URL लेता है। हर कॉल को एक समय-सीमा और एक SemaphoreSlim में लपेटें, और SDK के चारों exceptions को HTTP status codes से जोड़ें। फिर किसी भी भाषा में लिखे आपके स्क्रैपर, वर्कर और script एक ही आंतरिक पते को कॉल करते हैं। उन्हें CapSkip का पोर्ट या key कभी दिखाई नहीं देती। CapSkip आपकी अपनी Windows मशीन पर चलता है, इसलिए सर्विस पर भी प्रति-हल कोई बिल नहीं आता। यह गाइड करीब 120 लाइनों में सर्विस बनाती है और दिखाती है कि CapSkip के धीमे, बंद या चुप होने पर हर हिस्सा क्या करता है।

आपको क्या चाहिए

  • किसी Windows PC पर चल रहा CapSkip, जिसमें reCAPTCHA सॉल्वर चालू हो। पहली बार शुरू करने के चरण सेटअप गाइड में समझाए गए हैं।
  • वर्ज़न 10 का .NET SDK। नीचे सब कुछ Windows 11 पर SDK 10.0.100, ASP.NET Core 10.0.0 और CapSkip NuGet पैकेज 1.3.0 के साथ चलाया गया।
  • एक PowerShell prompt। कमांड PowerShell syntax में लिखी गई हैं।
  • reCAPTCHA v2 checkbox वाला एक पेज। सर्विस को सिर्फ़ उसका sitekey और उसका पता चाहिए, पेज के बारे में और कुछ नहीं।
  • CapSkip के लिए एक पता। Local mode में वह केवल उसी डिवाइस के लिए 127.0.0.1 पर जवाब देता है। Server mode में वह आपके नेटवर्क पते या पब्लिक IP पर सुनता है, ताकि किसी दूसरी मशीन पर चल रही सर्विस API के ज़रिए उसे कॉल कर सके। दोनों यहाँ सेट किए जाते हैं: कनेक्शन सेटिंग्स.

इस गाइड की संख्याएँ उसी PC पर चल रहे एक mock CapSkip से ली गई हैं (यानी टेस्ट के लिए बनाया गया नकली CapSkip)। ASP.NET Core और SDK असली हैं। mock हर हल को एक तयशुदा समय तक रोके रखता है, जो दो सेकंड है जब तक कोई टेस्ट कुछ और न कहे, और असली हल का समय अलग होगा। यहाँ कुछ भी असली CapSkip या असली साइट के सामने नहीं चलाया गया।

CapSkip के आगे सर्विस क्यों रखें

अगर कोई एक C# ऐप कैप्चा हल करता है, तो SDK को उसी ऐप से कॉल करें। इसकी जानकारी C# कैप्चा सॉल्वर पेज में है। सर्विस तब काम आती है जब कई कॉलर एक ही CapSkip साझा करते हों। तब चार काम एक ही जगह होने चाहिए।

  • एक समय-सीमा। .NET SDK डिफ़ॉल्ट रूप से 300 सेकंड तक poll करता है, इसलिए हर कॉलर को अपना अलग timer चाहिए होता।
  • एक सीमा। CapSkip एक साथ तय संख्या में हल चलाता है, और बाकी जॉब इंतज़ार करती हैं। आपके नियंत्रण वाली क़तार को समझना CapSkip के अंदर की क़तार से आसान है।
  • एक key। कॉलर को सर्विस की key मिलती है। CapSkip का पता और API key सर्विस की सेटिंग्स में ही रहते हैं।
  • कोई भी भाषा। Python script, PHP साइट और PowerShell टास्क, सब HTTP और JSON बोलते हैं।

चरण 1: प्रोजेक्ट बनाएँ

web टेम्पलेट खाली ASP.NET Core प्रोजेक्ट है: एक Program.cs, कोई controller नहीं। प्रोजेक्ट को उसके अपने फ़ोल्डर में रखें। पैकेज इंस्टॉल सिर्फ़ प्रोजेक्ट फ़ाइल में लिखता है।

# "web" is the empty ASP.NET Core template. This guide uses .NET SDK 10.
dotnet new web -n CapSkipSolver -f net10.0
cd CapSkipSolver
dotnet add package CapSkip

चरण 2: सेटिंग्स को appsettings.json में रखें

CapSkip सेक्शन में पता, सॉल्वर की API key, मिलाने के लिए CapSkip thread की संख्या और सेकंड में समय-सीमा होती है। Solver सेक्शन में वह key होती है जो आपके कॉलर भेजेंगे। कुछ भी चलाने से पहले दोनों placeholder बदल दें।

{
  "CapSkip": {
    "Host": "127.0.0.1",
    "Port": 8080,
    "ApiKey": "YOUR_API_KEY",
    "MaxThreads": 10,
    "DeadlineSeconds": 120
  },
  "Solver": {
    "ApiKey": "YOUR_SERVICE_KEY"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

कोई भी वैल्यू एनवायरनमेंट वेरिएबल से बदली जा सकती है। दोहरा underscore कोलन की जगह लेता है, इसलिए CapSkip__Port, CapSkip:Port को बदल देता है। सर्विस को CapSkip से दूर चलाएँ नाम का सेक्शन फ़ाइल बदले बिना सर्विस को दूसरी मशीन की ओर मोड़ने के लिए इसी का इस्तेमाल करता है।

चरण 3: सेटिंग्स पढ़ें और key के बिना शुरू होने से मना करें

टेम्पलेट के Program.cs की पूरी सामग्री को आगे आने वाले ब्लॉकों से बदल दें, और हर नया ब्लॉक पिछले के नीचे जोड़ें। प्रोजेक्ट चरण 10 के बाद ही build होता है, क्योंकि records और helper functions सबसे आख़िर में आते हैं।

Program.cs का ऊपरी हिस्सा CapSkip सेक्शन को एक record में भरता है और सर्विस की key पढ़ता है। key की जाँच IsNullOrWhiteSpace से होती है, throw वाली null जाँच से नहीं। mock पर खाली string null जाँच से निकल गई और सर्विस शुरू हो गई, फिर उसने हर वह रिक्वेस्ट स्वीकार कर ली जिसने खाली header भेजा।

इस ब्लॉक में .NET की अपनी एक अकेली गड़बड़ी भी है। System और CapSkip, दोनों TimeoutException नाम की class बनाते हैं। implicit usings और using CapSkip के साथ, सादा TimeoutException compile नहीं होता। System.TimeoutException पर लगा switch का case compile तो होता है, पर SDK के exception से कभी मेल नहीं खाता। सर्विस में इसने SDK के timeout को catch-all case में भेज दिया और 504 की जगह 500 लौटाया। आख़िरी using लाइन SDK की class को उसका अपना नाम देती है।

// Program.cs
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using CapSkip;
// System and CapSkip both define TimeoutException, so give the SDK's
// one its own name.
using SolverTimeout = CapSkip.TimeoutException;

var builder = WebApplication.CreateBuilder(args);

// Reads the "CapSkip" section of appsettings.json. The environment
// variable CapSkip__Port overrides CapSkip:Port (__ stands for a colon).
var cfg = builder.Configuration.GetSection("CapSkip").Get<CapSkipSettings>()
    ?? new CapSkipSettings();
var serviceKey = builder.Configuration["Solver:ApiKey"];
if (string.IsNullOrWhiteSpace(serviceKey))
    throw new InvalidOperationException("Set Solver:ApiKey before starting.");

चरण 4: एक क्लाइंट और एक गेट रजिस्टर करें

CapSkipClient को singleton के रूप में रखना सुरक्षित है। SDK के source में वह कॉल-वार कोई state नहीं रखता, हर कॉल पर options dictionary की कॉपी बनाता है, और सारे instances एक ही static HttpClient साझा करते हैं। dispose करने को कुछ नहीं है और IHttpClientFactory की ज़रूरत भी नहीं। SDK एक निजी static HttpClient इस्तेमाल करता है और आपका वाला नहीं लेता। लंबे समय तक खुला रहने वाला कनेक्शन DNS बदलाव को नहीं पहचानता, इसलिए CapSkip host के लिए IP पता इस्तेमाल करें।

SemaphoreSlim ही गेट है। उसे CapSkip के हर thread के लिए एक स्लॉट दें, जो CapSkip की Max. Threads सेटिंग है (उसका डिफ़ॉल्ट 10 है)। AddProblemDetails और UseStatusCodePages साथ मिलकर ASP.NET Core के अपने लिखे 400, 404, 405 और 415 जवाबों को हमारे जवाबों वाले ही फ़ॉर्मैट में लौटाते हैं। UseStatusCodePages के बिना उन जवाबों में कोई body नहीं थी।

builder.Services.AddSingleton(cfg);
// One client for the whole process. It keeps no per-call state.
builder.Services.AddSingleton(new CapSkipClient(
    cfg.ApiKey, cfg.Host, cfg.Port, recaptchaTimeout: cfg.SdkTimeoutSeconds));
// One slot per CapSkip thread (Max. Threads in the CapSkip settings).
builder.Services.AddSingleton(new SemaphoreSlim(cfg.MaxThreads));
// With these two calls, 400, 404, 405 and 415 also come back as
// ProblemDetails.
builder.Services.AddProblemDetails();

var app = builder.Build();
app.UseStatusCodePages();

चरण 5: सर्विस की key जाँचें

नेटवर्क पते पर खुला, बिना सुरक्षा वाला solve endpoint उन सबके लिए मुफ़्त सॉल्वर है जो उस पोर्ट तक पहुँच सकते हैं। यह middleware /solve पाथ की रखवाली करता है। FixedTimeEquals bytes को हर जगह बराबर समय में मिलाता है, चाहे वे जहाँ भी अलग हों, और अलग लंबाई होने पर exception फेंकने के बजाय false लौटाता है। mock पर गायब key, गलत key और बहुत लंबी key, तीनों को 401 मिला।

यह 403 नहीं, 401 लौटाता है, क्योंकि यहाँ एक ही credential है और वह गायब है या गलत। middleware, मॉडल binding से पहले चलता है, इसलिए बिना key और टूटी JSON body वाली रिक्वेस्ट को 400 नहीं, 401 मिलता है।

// Only callers that know the service key may use /solve. FixedTimeEquals
// takes the same time wherever the first wrong byte is.
var expected = Encoding.UTF8.GetBytes(serviceKey);
app.Use(async (ctx, next) =>
{
    var sent = Encoding.UTF8.GetBytes(ctx.Request.Headers["X-API-Key"].ToString());
    if (ctx.Request.Path.StartsWithSegments("/solve")
        && !CryptographicOperations.FixedTimeEquals(sent, expected))
    {
        await Fail(401, "Missing or wrong X-API-Key").ExecuteAsync(ctx);
        return;
    }
    await next(ctx);
});

चरण 6: health check जोड़ें

SDK में कोई ping कॉल नहीं है, इसलिए health endpoint CapSkip के पोर्ट पर एक TCP कनेक्शन खोलता है और तीन सेकंड बाद हार मान लेता है। CapSkip बंद होने पर उसने करीब दो सेकंड बाद 503 दिया, क्योंकि Windows loopback कनेक्शन को इतनी धीमी गति से ठुकराता है। यह load balancer या monitoring script को बताता है कि सर्विस चालू है और CapSkip नहीं। यह नहीं जाँचता कि हल काम करता है।

// CapSkip has no ping call, so check that its port accepts a connection.
app.MapGet("/health", async (CapSkipSettings s, CancellationToken ct) =>
{
    using var probe = CancellationTokenSource.CreateLinkedTokenSource(ct);
    probe.CancelAfter(TimeSpan.FromSeconds(3));
    try
    {
        using var tcp = new TcpClient();
        await tcp.ConnectAsync(s.Host, s.Port, probe.Token);
        return Results.Ok(new { status = "ok" });
    }
    catch (Exception ex) when (ex is SocketException or OperationCanceledException)
    {
        return Fail(503, "CapSkip is not reachable");
    }
});

चरण 7: रिक्वेस्ट जाँचें और समय-सीमा शुरू करें

record parameter किसी गायब फ़ील्ड को error नहीं बनाता। System.Text.Json उस property को null छोड़ देता है, इसलिए handler खुद sitekey और URL जाँचता है और validation problem के साथ जवाब देता है।

समय-सीमा एक linked CancellationTokenSource है। वह तब cancel होता है जब कॉलर कनेक्शन बंद कर देता है (RequestAborted) या जब समय-सीमा बीत जाती है (CancelAfter)। बाद का हर चरण उसका टोकन लेता है, इसलिए देखने के लिए सिर्फ़ एक घड़ी है।

app.MapPost("/solve", async (SolveRequest req, CapSkipClient solver,
    SemaphoreSlim gate, CapSkipSettings s, HttpContext http) =>
{
    if (string.IsNullOrWhiteSpace(req.Sitekey)
        || !Uri.IsWellFormedUriString(req.Url, UriKind.Absolute))
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            ["request"] = ["sitekey and an absolute url are required"]
        });

    // One token for the whole request. It is cancelled when the caller
    // hangs up (RequestAborted) or when the deadline passes.
    using var deadline = CancellationTokenSource.CreateLinkedTokenSource(
        http.RequestAborted);
    deadline.CancelAfter(TimeSpan.FromSeconds(s.DeadlineSeconds));

चरण 8: स्लॉट का इंतज़ार करें, हल करें और छोड़ें

स्लॉट का इंतज़ार उसी टोकन का इस्तेमाल करता है, साथ में QueueSeconds की अपनी सीमा भी (settings record में 20)। अगर हर स्लॉट इतनी देर तक व्यस्त रहे, तो कॉलर को 503 मिलता है और वह दोबारा कोशिश कर सकता है। सीमा के बिना रिक्वेस्ट क़तार में तब तक बैठी रहती जब तक समय-सीमा उसे उठा न लेती।

CapSkipError, SDK के चारों exceptions की base class है, और catch उसे cancellation exception के साथ नाम से लेता है। RecaptchaAsync भी टोकन लेता है। SDK उसे हर poll रिक्वेस्ट को पास करता है, इसलिए समय-सीमा चल रही HTTP कॉल का इंतज़ार करने के बजाय उसे cancel कर देती है। finally block स्लॉट तभी छोड़ता है जब वह लिया गया हो। जो स्लॉट आपके पास कभी था ही नहीं, उसे छोड़ने से CapSkip के चला सकने वाले हलों से एक हल ज़्यादा निकल जाता।

    var held = false;
    try
    {
        // Wait for a free slot, but not longer than QueueSeconds.
        if (!await gate.WaitAsync(TimeSpan.FromSeconds(s.QueueSeconds), deadline.Token))
            return Fail(503, "All solver slots are busy");
        held = true;
        var result = await solver.RecaptchaAsync(
            req.Sitekey, req.Url, req.Options(), deadline.Token);
        return Results.Ok(new SolveResponse(result.Code));
    }
    catch (Exception ex) when (ex is CapSkipError or OperationCanceledException)
    {
        return ToProblem(ex, http.RequestAborted.IsCancellationRequested,
            deadline.IsCancellationRequested);
    }
    finally
    {
        if (held) gate.Release();
    }
});

चरण 9: exceptions को status codes में बदलें

यही वह हिस्सा है जो कॉलर को दिखता है। switch के cases का क्रम मायने रखता है, क्योंकि SDK cancel हुई कॉल को दो अलग तरीकों से बताता है। cancel हुआ submit NetworkException के रूप में निकलता है (“The operation was canceled”)। cancel हुआ poll TaskCanceledException के रूप में निकलता है। exception के प्रकार से जोड़ने पर कॉलर को बताया जाता कि CapSkip बंद है, जबकि असल में समय-सीमा बीती होती। इसलिए पहले दोनों flags जाँचे जाते हैं।

app.Run();

static IResult Fail(int status, string title, string? detail = null) =>
    Results.Problem(statusCode: status, title: title, detail: detail);

// Check the two flags first: a cancelled submit surfaces as
// NetworkException, a cancelled poll as TaskCanceledException.
static IResult ToProblem(Exception ex, bool callerLeft, bool deadlinePassed) => ex switch
{
    _ when callerLeft => Results.StatusCode(499),
    _ when deadlinePassed => Fail(504, "Solve deadline exceeded"),
    SolverTimeout => Fail(504, "CapSkip timed out"),
    ValidationException => Fail(400, "Bad solve parameters", ex.Message),
    ApiException when ex.Message.Contains("UNSOLVABLE") => Fail(422, "CAPTCHA could not be solved"),
    ApiException => Fail(502, "CapSkip refused the request"),
    NetworkException => Fail(503, "CapSkip is not reachable"),
    _ => Fail(500, "Unexpected error"),
};

SDK का ApiException अपना टेक्स्ट message में रखता है और उसमें कोई code property नहीं है। हल न हो सकने वाला मामला उस message में UNSOLVABLE शब्द से पहचाना जाता है। नीचे के हर status code को mock पर जानबूझकर पैदा किया गया था।

स्थितिक्या हुआकितना समय लगा
503CapSkip बंद है, इसलिए submit ठुकरा दिया गया2.1 s
422CapSkip ने ERROR_CAPTCHA_UNSOLVABLE बताया3.8 s
502CapSkip ने सर्विस की अपनी API key ठुकरा दी0.1 s से कम
400v2 रिक्वेस्ट में एक action था, जिसे सिर्फ़ v3 स्वीकार करता है0.1 s से कम
504चुप पड़े CapSkip के साथ समय-सीमा बीत गई (6 s की टेस्ट समय-सीमा)6.0 s
504SDK का अपना 5 s का timeout पहले चल गया7.8 s

502 जानबूझकर है। CapSkip की ठुकराई गई key सर्विस की गलती है, कॉलर की नहीं, इसलिए यह 401 या 403 नहीं है। आख़िरी पंक्ति 5 सेकंड से लंबी है क्योंकि SDK अपनी घड़ी हर इंतज़ार के बाद ही जाँचता है। वह 0.25 s इंतज़ार करता है, फिर इंतज़ार को दोगुना करता जाता है, 5 s तक। 3.75 s पर शुरू होने वाला इंतज़ार 4 s चलता है, इसलिए timeout 7.75 s पर चलता है। यही शेड्यूल 3.8 s वाली पंक्ति को समझाता है: नतीजा 2 s पर तैयार है और 3.75 s की जाँच पर मिलता है।

एक नाकामी को कभी जल्दी जवाब नहीं मिलता। अगर CapSkip जॉब स्वीकार करने के बाद बंद हो जाए, तो SDK हर ठुकराए गए poll को अभी तैयार नहीं मानता। कॉलर फिर पूरी समय-सीमा तक इंतज़ार करता है (mock पर 8 s की समय-सीमा के साथ करीब 8.0 s) और 504 पाता है। सिर्फ़ ठुकराया गया submit तुरंत 503 देता है।

चरण 10: records जोड़ें

settings record में डिफ़ॉल्ट वैल्यू होती हैं। request record options dictionary सिर्फ़ उन फ़ील्ड से बनाता है जो कॉलर ने भेजे। v3 कॉलर version और action भेज सकता है, और v2 कॉलर दोनों में से कुछ नहीं भेजता। SdkTimeoutSeconds SDK की अपनी सीमा है, और QueueSeconds यह है कि एक रिक्वेस्ट स्लॉट के लिए कितनी देर इंतज़ार कर सकती है। इनमें से कोई appsettings.json में नहीं है, इसलिए डिफ़ॉल्ट लागू होते हैं। SDK की सीमा समय-सीमा से ऊपर रखें, ताकि समय-सीमा पहले चले।

record CapSkipSettings(string Host = "127.0.0.1", int Port = 8080,
    string ApiKey = "capskip", int MaxThreads = 10, int DeadlineSeconds = 120,
    int QueueSeconds = 20, int SdkTimeoutSeconds = 300);

record SolveRequest(string Sitekey, string Url, string? Version = null, string? Action = null)
{
    // Extra reCAPTCHA options, only when the caller sent them.
    public Dictionary<string, object?> Options()
    {
        var o = new Dictionary<string, object?>();
        if (Version is not null) o["version"] = Version;
        if (Action is not null) o["action"] = Action;
        return o;
    }
}

record SolveResponse(string Token);

Windows पर चलाएँ

dotnet run का urls option इस्तेमाल करें, और ASPNETCORE_URLS वेरिएबल के भरोसे न रहें। web टेम्पलेट के साथ launchSettings.json फ़ाइल आती है जिसमें एक random पोर्ट होता है, और dotnet run उसे पढ़ता है। टेस्ट PC पर, ASPNETCORE_URLS सेट करके चलाए गए सादे dotnet run ने वेरिएबल को अनदेखा किया, launch profile के पोर्ट पर सुना और Development environment पर चला गया। urls option ने profile को हरा दिया, और no-launch-profile switch ने वेरिएबल को फिर से काम करने लायक बना दिया और Production environment दिया। सिर्फ़ urls option के साथ profile का Development environment फिर भी चालू रहता है, इसलिए Production चाहिए तो switch भी जोड़ें। profile और URL, दोनों न होने पर Kestrel localhost:5000 पर लौट आता है, जिसे यहाँ टेस्ट नहीं किया गया।

उसी PC के लिए 127.0.0.1 पर bind करें (यानी उसी पते पर सुनें)। तब नेटवर्क पर कोई और सर्विस तक नहीं पहुँच सकता।

# Local mode: CapSkip and this service run on the same PC. The service
# listens on 127.0.0.1 only, so no other machine can reach it.
dotnet run --urls http://127.0.0.1:5080

दूसरी मशीनों के कॉलर के लिए 0.0.0.0 पर bind करें, जिसका मतलब है हर नेटवर्क adapter। टेस्ट PC पर वह पता अपने ही LAN IP पर सही key के साथ जवाब देता था और गलत key को 401 से ठुकराता था। 127.0.0.1 पर bind की गई सर्विस ने LAN पते को ठुकरा दिया। पहले असली सर्विस key सेट करें। सादे HTTP पर सर्विस की key भी clear text में नेटवर्क पार करती है, इसलिए इसे सिर्फ़ ऐसे नेटवर्क पर इस्तेमाल करें जिस पर आपको भरोसा हो। Windows Firewall आपसे प्रोग्राम को अनुमति देने को कह सकता है, इसलिए जाँचें कि वह पोर्ट के लिए कौन-सा नियम बनाता है।

# Callers on other machines: 0.0.0.0 means every network adapter.
# Put the service key in place first, because anyone on the network can
# reach this port.
dotnet run --urls http://0.0.0.0:5080

सर्विस को CapSkip से दूर चलाएँ

सर्विस का उसी PC पर होना ज़रूरी नहीं। CapSkip को अपने नियंत्रण वाली किसी Windows मशीन पर Server mode में रखें, बेहतर होगा कि स्टैटिक पब्लिक IP के साथ। सर्विस को दो एनवायरनमेंट वेरिएबल से उसकी ओर मोड़ें। टेस्ट ने यह ढाँचा एक LAN पते पर bind किए गए mock के सामने चलाया, और एक हल में 1.85 s लगे।

# The service runs on another PC and CapSkip runs in Server mode on
# 203.0.113.10. Environment variables override appsettings.json.
$env:CapSkip__Host = '203.0.113.10'
$env:CapSkip__Port = '8080'
dotnet run --urls http://0.0.0.0:5080

तीन सावधानियाँ। CapSkip में API key validation चालू करें और सेटिंग्स के CapSkip सेक्शन में वही key इस्तेमाल करें। SDK सादा http पता बनाता है, इसलिए वह key clear text में नेटवर्क पार करती है। CapSkip अपने पोर्ट के लिए एक allow-any नियम भी जोड़ देता है, इसलिए हर उस पते के लिए एक block नियम जोड़ें जिसे वहाँ तक नहीं पहुँचना चाहिए। सेटअप गाइड के कनेक्शन सेटिंग्स सेक्शन में दोनों mode की सूची दी गई है।

PowerShell या Python से कॉल करें

कॉलर sitekey, पेज URL और सर्विस की key भेजता है। जवाब एक JSON object है जिसमें एक ही फ़ील्ड है, token। टोकन का इस्तेमाल करीब दो मिनट के भीतर करें, क्योंकि reCAPTCHA टोकन एक्सपायर हो जाता है, और उसे एक ही बार इस्तेमाल करें। यह घड़ी कैसे चलती है, यह टोकन एक्सपायरी गाइड में समझाया गया है।

# Ask the service for a token. It answers when the solve finishes.
$body = @{
    sitekey = 'YOUR_SITEKEY'
    url     = 'https://example.com/page-with-recaptcha'
} | ConvertTo-Json
$r = Invoke-RestMethod -Method Post -Uri http://127.0.0.1:5080/solve `
    -Headers @{ 'X-API-Key' = 'YOUR_SERVICE_KEY' } `
    -ContentType 'application/json' -Body $body
$r.token

कोई भी दूसरी भाषा इसी तरह काम करती है। नीचे का Python कॉलर सर्विस की समय-सीमा से लंबा timeout सेट करता है। इस तरह सर्विस का 504 कॉलर के अपने timer के हार मानने से पहले पहुँच जाता है।

import requests

# The caller's timeout must be longer than DeadlineSeconds (120 here),
# so the 504 from the service arrives before your own timeout fires.
r = requests.post(
    "http://127.0.0.1:5080/solve",
    headers={"X-API-Key": "YOUR_SERVICE_KEY"},
    json={"sitekey": "YOUR_SITEKEY", "url": "https://example.com/page-with-recaptcha"},
    timeout=130,
)
print(r.status_code, r.json())

गेट क्या करता है और क्या नहीं

mock में CapSkip की thread सीमा 3 रखकर और दस रिक्वेस्ट एक साथ भेजकर, गेट ने mock पर अधिकतम तीन जॉब रखीं। गेट के बिना, दसों वहाँ पहुँचीं और सात को thread के लिए इंतज़ार करना पड़ा। गेट गति नहीं बढ़ाता, क्योंकि तीन thread दोनों हाल में रफ़्तार तय करते हैं। एक सेकंड के hold के साथ, दस रिक्वेस्ट में गेट के साथ करीब 7.2 s और उसके बिना 7.9 s लगे। फ़ायदा एक सेकंड से कम है। इसकी असली कीमत तब दिखती है जब कॉलर हार मान लेते हैं।

उस रन में हर कॉलर ने 4 सेकंड बाद हार मान ली और हर हल को 6 चाहिए थे। गेट के साथ तीन से पाँच जॉब CapSkip तक पहुँचीं, क्योंकि इंतज़ार कर रही रिक्वेस्ट अपनी समय-सीमा से ज़रा पहले खाली हुआ स्लॉट ले सकती है। आठ सेकंड बाद कोई भी जॉब लंबित नहीं थी। उसके बिना दसों CapSkip तक पहुँचीं, और कॉलरों के हार मानने के छह सेकंड बाद भी सात लंबित थीं। उन हलों का इंतज़ार कोई नहीं कर रहा था, फिर भी वे thread रोके हुए थीं।

एक कमी बाकी है। जब कोई कॉलर कनेक्शन बंद कर देता है, तो सर्विस poll करना बंद कर देती है और अपना स्लॉट खाली कर देती है। SDK में कोई cancel कॉल नहीं है, इसलिए मान लें कि CapSkip जॉब पूरी करता है और तब तक एक thread रोके रखता है। mock ऐसा ही करता है। इसलिए cancellation की बौछार के बाद स्लॉट की गिनती CapSkip के असली threads से आगे निकल सकती है, जब तक वे जॉब खत्म न हों। असली CapSkip को टेस्ट नहीं किया गया, और उसकी Row Timeout सेटिंग तय करती है कि कोई जॉब ज़्यादा से ज़्यादा कितनी देर चल सकती है।

पूरी Program.cs

यह चरण 3 से 10 को क्रम से जोड़ने पर बनी फ़ाइल है। इसे टेम्पलेट की Program.cs के ऊपर चिपका दें।

// Program.cs
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using CapSkip;
// System and CapSkip both define TimeoutException, so give the SDK's
// one its own name.
using SolverTimeout = CapSkip.TimeoutException;

var builder = WebApplication.CreateBuilder(args);

// Reads the "CapSkip" section of appsettings.json. The environment
// variable CapSkip__Port overrides CapSkip:Port (__ stands for a colon).
var cfg = builder.Configuration.GetSection("CapSkip").Get<CapSkipSettings>()
    ?? new CapSkipSettings();
var serviceKey = builder.Configuration["Solver:ApiKey"];
if (string.IsNullOrWhiteSpace(serviceKey))
    throw new InvalidOperationException("Set Solver:ApiKey before starting.");

builder.Services.AddSingleton(cfg);
// One client for the whole process. It keeps no per-call state.
builder.Services.AddSingleton(new CapSkipClient(
    cfg.ApiKey, cfg.Host, cfg.Port, recaptchaTimeout: cfg.SdkTimeoutSeconds));
// One slot per CapSkip thread (Max. Threads in the CapSkip settings).
builder.Services.AddSingleton(new SemaphoreSlim(cfg.MaxThreads));
// With these two calls, 400, 404, 405 and 415 also come back as
// ProblemDetails.
builder.Services.AddProblemDetails();

var app = builder.Build();
app.UseStatusCodePages();

// Only callers that know the service key may use /solve. FixedTimeEquals
// takes the same time wherever the first wrong byte is.
var expected = Encoding.UTF8.GetBytes(serviceKey);
app.Use(async (ctx, next) =>
{
    var sent = Encoding.UTF8.GetBytes(ctx.Request.Headers["X-API-Key"].ToString());
    if (ctx.Request.Path.StartsWithSegments("/solve")
        && !CryptographicOperations.FixedTimeEquals(sent, expected))
    {
        await Fail(401, "Missing or wrong X-API-Key").ExecuteAsync(ctx);
        return;
    }
    await next(ctx);
});

// CapSkip has no ping call, so check that its port accepts a connection.
app.MapGet("/health", async (CapSkipSettings s, CancellationToken ct) =>
{
    using var probe = CancellationTokenSource.CreateLinkedTokenSource(ct);
    probe.CancelAfter(TimeSpan.FromSeconds(3));
    try
    {
        using var tcp = new TcpClient();
        await tcp.ConnectAsync(s.Host, s.Port, probe.Token);
        return Results.Ok(new { status = "ok" });
    }
    catch (Exception ex) when (ex is SocketException or OperationCanceledException)
    {
        return Fail(503, "CapSkip is not reachable");
    }
});

app.MapPost("/solve", async (SolveRequest req, CapSkipClient solver,
    SemaphoreSlim gate, CapSkipSettings s, HttpContext http) =>
{
    if (string.IsNullOrWhiteSpace(req.Sitekey)
        || !Uri.IsWellFormedUriString(req.Url, UriKind.Absolute))
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            ["request"] = ["sitekey and an absolute url are required"]
        });

    // One token for the whole request. It is cancelled when the caller
    // hangs up (RequestAborted) or when the deadline passes.
    using var deadline = CancellationTokenSource.CreateLinkedTokenSource(
        http.RequestAborted);
    deadline.CancelAfter(TimeSpan.FromSeconds(s.DeadlineSeconds));

    var held = false;
    try
    {
        // Wait for a free slot, but not longer than QueueSeconds.
        if (!await gate.WaitAsync(TimeSpan.FromSeconds(s.QueueSeconds), deadline.Token))
            return Fail(503, "All solver slots are busy");
        held = true;
        var result = await solver.RecaptchaAsync(
            req.Sitekey, req.Url, req.Options(), deadline.Token);
        return Results.Ok(new SolveResponse(result.Code));
    }
    catch (Exception ex) when (ex is CapSkipError or OperationCanceledException)
    {
        return ToProblem(ex, http.RequestAborted.IsCancellationRequested,
            deadline.IsCancellationRequested);
    }
    finally
    {
        if (held) gate.Release();
    }
});

app.Run();

static IResult Fail(int status, string title, string? detail = null) =>
    Results.Problem(statusCode: status, title: title, detail: detail);

// Check the two flags first: a cancelled submit surfaces as
// NetworkException, a cancelled poll as TaskCanceledException.
static IResult ToProblem(Exception ex, bool callerLeft, bool deadlinePassed) => ex switch
{
    _ when callerLeft => Results.StatusCode(499),
    _ when deadlinePassed => Fail(504, "Solve deadline exceeded"),
    SolverTimeout => Fail(504, "CapSkip timed out"),
    ValidationException => Fail(400, "Bad solve parameters", ex.Message),
    ApiException when ex.Message.Contains("UNSOLVABLE") => Fail(422, "CAPTCHA could not be solved"),
    ApiException => Fail(502, "CapSkip refused the request"),
    NetworkException => Fail(503, "CapSkip is not reachable"),
    _ => Fail(500, "Unexpected error"),
};

record CapSkipSettings(string Host = "127.0.0.1", int Port = 8080,
    string ApiKey = "capskip", int MaxThreads = 10, int DeadlineSeconds = 120,
    int QueueSeconds = 20, int SdkTimeoutSeconds = 300);

record SolveRequest(string Sitekey, string Url, string? Version = null, string? Action = null)
{
    // Extra reCAPTCHA options, only when the caller sent them.
    public Dictionary<string, object?> Options()
    {
        var o = new Dictionary<string, object?>();
        if (Version is not null) o["version"] = Version;
        if (Action is not null) o["action"] = Action;
        return o;
    }
}

record SolveResponse(string Token);

Minimal API कैप्चा सॉल्वर की आम errors

आप जो देखते हैंकारणफिक्स
TimeoutException को लेकर CS0104System और CapSkip, दोनों इसे परिभाषित करते हैंचरण 3 का SolverTimeout alias इस्तेमाल करें
SDK timeout पर 500एक switch case System.TimeoutException का नाम लेता हैalias वाली SDK class को पकड़ें
सर्विस ASPNETCORE_URLS को अनदेखा करती हैdotnet run, launchSettings.json लागू करता हैइसके बजाय urls option दें
हर कॉल 401 लौटाती हैX-API-Key header, Solver:ApiKey के बराबर नहीं हैसर्विस की key भेजें, CapSkip वाली नहीं
health check से 503CapSkip के पोर्ट पर कुछ भी कनेक्शन स्वीकार नहीं कर रहाCapSkip शुरू करें और Local या Server mode जाँचें
हर हल पर 502CapSkip ने CapSkip सेक्शन वाली key ठुकरा दीउसे CapSkip में सेट की गई key से मिलाएँ
पूरी समय-सीमा के बाद 504CapSkip चुप है या हल धीमा हैCapSkip के Monitor टैब में हल को देखें, फिर समय-सीमा बढ़ाएँ

Microsoft का minimal APIs ओवरव्यू framework वाले पक्ष को कवर करता है, और CapSkip के हर error code की सूची यहाँ मिलती है: API दस्तावेज़.

FAQ

अगर सिर्फ़ एक ऐप कैप्चा हल करता है, तो क्या मुझे सर्विस चाहिए?

नहीं। SDK को उसी ऐप से कॉल करें। सर्विस तब काम आती है जब कई कॉलर, या दूसरी भाषाओं के कॉलर, एक ही CapSkip साझा करते हों और आप एक समय-सीमा, एक सीमा और एक key चाहते हों। बैकग्राउंड जॉब के लिए in-process रास्ता Worker Service गाइड में दिखाया गया है।

क्या मैं सर्विस को Azure या किसी दूसरे प्रदाता पर होस्ट कर सकता हूँ?

हाँ, पर होस्टेड सर्विस किसी और की मशीन पर चलती है, इसलिए वह आपके PC के 127.0.0.1 तक नहीं पहुँच सकती। CapSkip को अपने नियंत्रण वाली किसी Windows मशीन पर Server mode में चलाएँ, बेहतर होगा कि स्टैटिक पब्लिक IP के साथ, और CapSkip__Host को उसी पते पर सेट करें। API key validation चालू करें, हर दूसरे पते के लिए block नियम जोड़ें, और याद रखें कि सादे HTTP पर key clear text में जाती है। इस गाइड के लिए किसी होस्टिंग प्रदाता को टेस्ट नहीं किया गया।

क्या cancel की गई रिक्वेस्ट CapSkip में हल को रोक देती है?

नहीं। सर्विस poll करना बंद कर देती है और अपना स्लॉट खाली कर देती है। SDK में कोई cancel कॉल नहीं है, इसलिए मान लें कि CapSkip जॉब पर काम करता रहता है और खत्म होने तक एक thread रोके रखता है। समय-सीमा इतनी लंबी रखें कि सामान्य हल पूरा हो सके, ताकि cancellation कम ही हों।

क्या यही endpoint reCAPTCHA v3 हल कर सकता है?

हाँ। request body में version v3 और एक action भेजें, और options जैसे हैं वैसे SDK तक पहुँचते हैं। सर्विस कोई score वैल्यू नहीं भेजती, क्योंकि CapSkip में कोई score सेटिंग नहीं है। इस गाइड में इस्तेमाल हुए checkbox टाइप की जानकारी reCAPTCHA v2 सॉल्वर पेज पर दी गई है।

संक्षेप में

Minimal API कैप्चा सॉल्वर बनाने के लिए, एक CapSkipClient, एक SemaphoreSlim और हर रिक्वेस्ट के लिए एक linked cancellation token रखें। सबसे पहले सर्विस की key जाँचें। exception के प्रकार से पहले दोनों cancellation flags जाँचें। सर्विस को एनवायरनमेंट वेरिएबल से नहीं, urls option से शुरू करें। अगर CapSkip Server mode में चलता है, तो उसके आगे block नियम लगाएँ।

एक साझा सर्विस पूरी टीम के काम आ सकती है, और किसी को हल गिनने की ज़रूरत नहीं पड़ती। इसे आपकी अपनी Windows मशीन पर चल रहे लोकल captcha सॉल्वर के आगे चलाने से हर हल प्रति-कॉल बिल से मुक्त रहता है।