C#’ta Minimal API CAPTCHA Çözücü Endpoint’i Nasıl Oluşturulur

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

Minimal API CAPTCHA çözücü oluşturmak için ASP.NET Core içinde tek bir CapSkipClient’ı singleton olarak kaydedin. Bir sitekey ve bir sayfa URL’si alan bir POST endpoint’i sunun. Her çağrıyı bir son süre ve bir SemaphoreSlim içine alın ve SDK’nın dört istisnasını HTTP durum kodlarına eşleyin. Scraper’larınız, worker’larınız ve herhangi bir dildeki script’leriniz bu durumda tek bir iç adresi çağırır. CapSkip’in portunu ya da anahtarını hiç görmezler. CapSkip kendi Windows makinenizde çalışır, dolayısıyla servisin de çözüm başına bir faturası yoktur. Bu rehber servisi yaklaşık 120 satırda oluşturur ve CapSkip yavaş, kapalı ya da sessiz kaldığında her parçanın ne yaptığını gösterir.

Neye ihtiyacınız var

  • reCAPTCHA çözücüsü açık hâlde bir Windows bilgisayarında çalışan CapSkip. İlk başlatmayı kurulum kılavuzu adım adım anlatıyor.
  • SDK olarak .NET 10. Aşağıdaki her şey Windows 11 üzerinde SDK 10.0.100, ASP.NET Core 10.0.0 ve CapSkip NuGet paketi 1.3.0 ile çalıştırıldı.
  • Bir PowerShell istemi. Komutlar PowerShell sözdizimini kullanır.
  • reCAPTCHA v2 onay kutusu olan bir sayfa. Servisin sayfa hakkında sitekey’inden ve adresinden başka bir şeye ihtiyacı yoktur.
  • CapSkip için bir adres. Local modunda 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 servis onu API üzerinden çağırabilir. İkisi de şurada ayarlanır: bağlantı ayarları.

Bu rehberdeki sayılar, aynı bilgisayardaki sahte (mock) bir CapSkip’ten gelir. ASP.NET Core ve SDK gerçek olanlardır. Mock her çözümü belirli bir süre bekletir; bir test aksini söylemedikçe bu süre iki saniyedir. Gerçek çözüm süreleri farklı olacaktır. Burada hiçbir şey canlı bir CapSkip’e ya da canlı bir siteye karşı çalıştırılmadı.

CapSkip’in önüne neden bir servis koymalısınız

Tek bir C# uygulaması CAPTCHA çözüyorsa SDK’yı o uygulamadan çağırın. C# CAPTCHA çözücü sayfası bunu ele alıyor. Birkaç çağıran tek bir CapSkip’i paylaştığında bir servis karşılığını verir. O zaman dört iş tek bir yerde toplanır.

  • Tek bir son süre. .NET SDK varsayılan olarak 300 saniyeye kadar yoklama yapar, bu yüzden her çağıranın kendi zamanlayıcısı gerekir.
  • Tek bir sınır. CapSkip aynı anda sabit sayıda çözüm çalıştırır ve fazla işler bekler. Kontrol ettiğiniz bir kuyruğu anlamak, CapSkip içindeki bir kuyruğu anlamaktan daha kolaydır.
  • Tek bir anahtar. Çağıranlar bir servis anahtarı alır. CapSkip’in adresi ve API anahtarı servisin ayarlarında kalır.
  • Herhangi bir dil. Bir Python script’i, bir PHP sitesi ve bir PowerShell görevi HTTP ve JSON ile konuşur.

1. Adım: projeyi oluşturun

Web şablonu boş ASP.NET Core projesidir: tek bir Program.cs, hiç controller yok. Projeyi kendi klasöründe tutun. Paket kurulumu yalnızca proje dosyasına yazar.

# "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. Adım: ayarları appsettings.json dosyasına koyun

CapSkip bölümü adresi, çözücünün API anahtarını, eşleştirilecek CapSkip thread sayısını ve saniye cinsinden son süreyi tutar. Solver bölümü çağıranlarınızın göndereceği anahtarı tutar. Herhangi bir şey çalıştırmadan önce iki yer tutucuyu da değiştirin.

{
  "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": "*"
}

Her değer bir ortam değişkeniyle geçersiz kılınabilir. Çift alt çizgi iki nokta üst üste yerine geçer, bu yüzden CapSkip__Port, CapSkip:Port değerini geçersiz kılar. Servisi CapSkip’ten ayrı bir makinede çalıştırın başlıklı bölüm, dosyayı düzenlemeden servisi başka bir makineye yöneltmek için bunu kullanır.

3. Adım: ayarları okuyun ve anahtar yoksa başlamayı reddedin

Şablonun Program.cs dosyasındaki her şeyi aşağıdaki bloklarla değiştirin ve her yeni bloğu bir öncekinin altına ekleyin. Record’lar ve yardımcı fonksiyonlar en sona geldiği için proje yalnızca 10. adımdan sonra derlenir.

Program.cs dosyasının başı CapSkip bölümünü bir record’a bağlar ve servis anahtarını okur. Anahtar denetimi, throw içeren bir null denetimi değil, IsNullOrWhiteSpace kullanır. Mock üzerinde boş bir dize null denetiminden geçti ve servis başladı, sonra boş başlık gönderen her isteği kabul etti.

Bu blok, .NET’e özgü tek tuzağı da içerir. System ve CapSkip, ikisi de TimeoutException adlı bir sınıf tanımlar. Örtük using’ler ve using CapSkip ile yalın bir TimeoutException derlenmez. System.TimeoutException üzerinde bir switch kolu derlenir ama SDK’nın istisnasıyla hiçbir zaman eşleşmez. Serviste bu, bir SDK zaman aşımını her şeyi yakalayan kola gönderdi ve 504 yerine 500 döndürdü. Son using satırı SDK’nın sınıfına kendi adını verir.

// 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. Adım: tek bir istemci ve tek bir geçit kaydedin

CapSkipClient singleton olarak güvenlidir. SDK kaynak kodunda çağrı başına hiçbir durum tutmaz, seçenekler sözlüğünü her çağrıda kopyalar ve tüm örnekler tek bir statik HttpClient paylaşır. Dispose edilecek bir şey yoktur ve IHttpClientFactory’ye gerek yoktur. SDK özel, tek bir statik HttpClient kullanır ve sizinkini almaz. Uzun ömürlü bir bağlantı DNS değişikliğini fark etmez, bu yüzden CapSkip host’u için bir IP adresi kullanın.

SemaphoreSlim geçittir. Ona CapSkip thread’i başına bir slot verin; bu, CapSkip’teki Max. Threads ayarıdır (varsayılanı 10). AddProblemDetails, UseStatusCodePages ile birlikte, ASP.NET Core’un kendi yazdığı 400, 404, 405 ve 415 yanıtlarının bizimkilerle aynı biçimde gelmesini sağlar. UseStatusCodePages olmadan bu yanıtların gövdesi yoktu.

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. Adım: servis anahtarını denetleyin

Bir ağ adresindeki korumasız bir çözüm endpoint’i, porta ulaşabilen herkes için ücretsiz bir çözücüdür. Bu middleware /solve yolunu korur. FixedTimeEquals baytları, nerede farklı olurlarsa olsunlar aynı sürede karşılaştırır ve farklı uzunlukta olduklarında hata fırlatmak yerine false döndürür. Eksik bir anahtar, yanlış bir anahtar ve çok uzun bir anahtar mock üzerinde 401 aldı.

401 döndürür, 403 değil, çünkü tek bir kimlik bilgisi vardır ve o ya eksiktir ya yanlıştır. Middleware model bağlamadan önce çalışır, bu yüzden anahtarı olmayan ve JSON gövdesi bozuk bir istek 400 değil 401 alır.

// 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. Adım: bir sağlık denetimi ekleyin

SDK’da ping çağrısı yoktur, bu yüzden sağlık endpoint’i CapSkip’in portuna bir TCP bağlantısı açar ve üç saniye sonra vazgeçer. CapSkip kapalıyken yaklaşık iki saniye sonra 503 yanıtı verdi, çünkü Windows bir loopback bağlantısını bu kadar yavaş reddeder. Bir yük dengeleyiciye ya da bir izleme script’ine servisin ayakta olduğunu ama CapSkip’in olmadığını söyler. Bir çözümün çalıştığını denetlemez.

// 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. Adım: isteği doğrulayın ve son süreyi başlatın

Bir record parametresi, eksik bir alanı hata yapmaz. System.Text.Json özelliği null bırakır; bu yüzden işleyici sitekey’i ve URL’yi kendisi denetler ve bir doğrulama sorunuyla (validation problem) yanıt verir.

Son süre, bağlantılı tek bir CancellationTokenSource’tur. Çağıran bağlantıyı kapattığında (RequestAborted) ya da son süre geçtiğinde (CancelAfter) iptal edilir. Sonraki her adım onun token’ını alır, bu yüzden bakılacak tek bir saat vardır.

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. Adım: bir slot için bekleyin, çözün ve bırakın

Slot beklemesi aynı token’ı kullanır; ayrıca kendi sınırı QueueSeconds’tır (ayarlar record’unda 20). Her slot bu kadar süre meşgul kalırsa çağıran 503 alır ve yeniden deneyebilir. Sınır olmasa istek, son süre onu alana kadar kuyrukta beklerdi.

CapSkipError, SDK’nın dört istisnasının temel sınıfıdır ve catch bloğu onu iptal istisnasıyla birlikte adlandırır. RecaptchaAsync de token’ı alır. SDK onu her yoklama isteğine iletir; böylece son süre, devam eden HTTP çağrısını beklemek yerine iptal eder. finally bloğu slot’u yalnızca alındıysa bırakır. Hiç tutmadığınız bir slot’u bırakmak, CapSkip’in çalıştırabileceğinden bir çözüm daha fazlasının geçmesine izin verirdi.

    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. Adım: istisnaları durum kodlarına çevirin

Çağıranların fark ettiği kısım budur. Switch kollarının sırası önemlidir, çünkü SDK iptal edilmiş bir çağrıyı iki farklı biçimde bildirir. İptal edilmiş bir gönderim NetworkException olarak çıkar (“The operation was canceled”). İptal edilmiş bir yoklama TaskCanceledException olarak çıkar. İstisna türüne göre eşleme yapmak, aslında son süre geçmişken bir çağırana CapSkip’in kapalı olduğunu söylerdi. Bu yüzden önce iki bayrak denetlenir.

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’nın ApiException sınıfı metnini mesajda taşır ve bir kod özelliği yoktur. Çözülemez durumu, bu mesajdaki UNSOLVABLE sözcüğüne göre eşleştirilir. Aşağıdaki durum kodlarının her biri mock’a karşı bilerek tetiklendi.

DurumNe olduNe kadar sürdü
503CapSkip kapalı, bu yüzden gönderim reddedildi2.1 s
422CapSkip ERROR_CAPTCHA_UNSOLVABLE bildirdi3.8 s
502CapSkip servisin kendi API anahtarını reddetti0.1 s altında
400Bir v2 isteği, yalnızca v3’ün kabul ettiği bir action taşıdı0.1 s altında
504Son süre, sessiz kalan bir CapSkip ile geçti (6 s test son süresi)6.0 s
504SDK’nın kendi 5 s zaman aşımı önce tetiklendi7.8 s

502 bilerek seçildi. Reddedilen bir CapSkip anahtarı çağıranın değil servisin hatasıdır, bu yüzden 401 ya da 403 değildir. Son satır 5 saniyeden uzundur, çünkü SDK saatini yalnızca her beklemeden sonra denetler. 0.25 saniye bekler, sonra beklemeyi 5 saniyeye kadar ikiye katlar. 3.75 saniyede başlayan bekleme 4 saniye sürer, bu yüzden zaman aşımı 7.75 saniyede tetiklenir. Aynı program 3.8 s satırını da açıklar: sonuç 2. saniyede hazırdır ve 3.75 saniyedeki denetimde bulunur.

Bir hata durumu hiçbir zaman erken bir yanıt almaz. CapSkip işi kabul ettikten sonra kapanırsa SDK reddedilen her yoklamayı henüz hazır değil olarak değerlendirir. Çağıran bu durumda son sürenin tamamını bekler (mock üzerinde 8 s son süreyle yaklaşık 8.0 s) ve 504 alır. Yalnızca reddedilen bir gönderim hızlı bir 503 verir.

10. Adım: record’ları ekleyin

Ayarlar record’u varsayılanları taşır. İstek record’u seçenekler sözlüğünü yalnızca çağıranın gönderdiği alanlardan oluşturur. Bir v3 çağıranı bir version ve bir action gönderebilir, bir v2 çağıranı ikisini de göndermez. SdkTimeoutSeconds SDK’nın kendi sınırıdır, QueueSeconds ise bir isteğin bir slot için ne kadar bekleyebileceğidir. İkisi de appsettings.json içinde değildir, bu yüzden varsayılanlar geçerlidir. Son sürenin önce tetiklenmesi için SDK’nın sınırını son sürenin üzerinde tutun.

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’ta çalıştırın

dotnet run komutunun urls seçeneğini kullanın ve ASPNETCORE_URLS değişkenine güvenmeyin. Web şablonu rastgele bir port içeren bir launchSettings.json dosyasıyla gelir ve dotnet run onu okur. Test bilgisayarında, ASPNETCORE_URLS ayarlıyken düz bir dotnet run değişkeni yok saydı, başlatma profilinin portunu dinledi ve Development ortamına geçti. urls seçeneği profili yendi; no-launch-profile bayrağı ise değişkenin yeniden çalışmasını sağladı ve Production ortamını verdi. urls seçeneği tek başına profilin Development ortamını açık bırakır, bu yüzden Production istiyorsanız bayrağı da ekleyin. Ne profil ne de URL ayarlıyken Kestrel localhost:5000 adresine döner; bu burada test edilmedi.

Aynı bilgisayar için 127.0.0.1’e bağlayın. Bu durumda ağdaki başka hiçbir şey servise ulaşamaz.

# 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

Başka makinelerdeki çağıranlar için, her ağ adaptörü anlamına gelen 0.0.0.0’a bağlayın. Test bilgisayarında bu adres, kendi LAN IP’sinde doğru anahtarla yanıt verdi ve yanlış olanı 401 ile reddetti. 127.0.0.1’e bağlı bir servis LAN adresini reddetti. Önce gerçek bir servis anahtarı ayarlayın. Servis anahtarı da düz HTTP üzerinden ağda açık metin olarak taşınır, bu yüzden bunu yalnızca güvendiğiniz bir ağda kullanın. Windows Firewall programa izin vermenizi isteyebilir, bu yüzden port için hangi kuralı oluşturduğunu kontrol edin.

# 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

Servisi CapSkip’ten ayrı bir makinede çalıştırın

Servisin aynı bilgisayarda durması gerekmez. CapSkip’i, kontrol ettiğiniz bir Windows makinesinde, tercihen statik bir genel IP ile Server moduna alın. Servisi iki ortam değişkeniyle ona yöneltin. Test bu düzeni bir LAN adresine bağlı bir mock’a karşı çalıştırdı ve bir çözüm 1.85 s sürdü.

# 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

Üç uyarı. CapSkip’te API key validation’ı açın ve ayarların CapSkip bölümünde aynı anahtarı kullanın. SDK düz bir http adresi oluşturur, bu yüzden bu anahtar ağda açık metin olarak taşınır. CapSkip ayrıca portu için her adrese izin veren bir kural ekler, bu yüzden ona ulaşmaması gereken her adres için bir engelleme kuralı ekleyin. Kurulum kılavuzunun bağlantı ayarları bölümü iki modu da listeler.

PowerShell veya Python’dan çağırın

Çağıran sitekey’i, sayfa URL’sini ve servis anahtarını gönderir. Yanıt, token adlı tek alanı olan bir JSON nesnesidir. Token’ı, bir reCAPTCHA token’ının süresi dolduğu için yaklaşık iki dakika içinde kullanın ve yalnızca bir kez kullanın. Sürenin nasıl işlediğini token süre sonu rehberi anlatıyor.

# 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

Başka her dil de aynı şekilde çalışır. Aşağıdaki Python çağıranı, servisin son süresinden daha uzun bir zaman aşımı ayarlar. Böylece servisten gelen 504, çağıranın kendi zamanlayıcısı pes etmeden önce ulaşır.

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

Geçidin yaptığı ve yapmadığı şeyler

CapSkip’in thread sınırı mock’ta 3 olarak ayarlıyken ve aynı anda on istek gelirken geçit, mock’ta en fazla üç işi tuttu. Geçit olmadan on isteğin hepsi ona ulaştı ve yedisi bir thread için bekledi. Geçit hız katmaz, çünkü her iki durumda da hızı üç thread belirler. Bir saniyelik bekletmeyle on istek geçitle yaklaşık 7.2 s, geçitsiz 7.9 s sürdü. Kazanç bir saniyenin altındadır. Değeri, çağıranlar vazgeçtiğinde ortaya çıkar.

Bu çalıştırmada her çağıran 4 saniye sonra vazgeçti ve her çözüm 6 saniye gerektirdi. Geçitle CapSkip’e üç ila beş iş ulaştı, çünkü bekleyen bir istek kendi son süresinden hemen önce boşalan bir slot’u alabilir. Sekiz saniye sonra hiçbiri beklemede değildi. Geçit olmadan on işin hepsi CapSkip’e ulaştı ve çağıranlar vazgeçtikten altı saniye sonra yedisi hâlâ beklemedeydi. Kimse o çözümleri beklemiyordu, ama yine de thread’leri tutuyorlardı.

Bir boşluk kalır. Bir çağıran bağlantıyı kapattığında servis yoklamayı bırakır ve slot’unu serbest bırakır. SDK’da iptal çağrısı yoktur, bu yüzden CapSkip’in işi bitirdiğini ve o zamana kadar bir thread tuttuğunu varsayın. Mock böyle davranır. Bu nedenle ardı ardına gelen iptallerden sonra, o işler bitene kadar slot sayısı CapSkip’in gerçek thread’lerinin önüne geçebilir. Gerçek bir CapSkip test edilmedi ve onun Row Timeout ayarı bir işin ne kadar çalışabileceğini sınırlar.

Tam Program.cs

Bu, 3. ile 10. adımların sırayla birleştirilmiş hâlidir. Şablonun Program.cs dosyasının üzerine yapıştırın.

// 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 CAPTCHA çözücüde sık görülen hatalar

GördüğünüzNedenDüzeltme
TimeoutException hakkında CS0104System ve CapSkip ikisi de onu tanımlıyor3. adımdaki SolverTimeout takma adını kullanın
Bir SDK zaman aşımı için 500Bir switch kolu System.TimeoutException adını veriyorTakma adı verilmiş SDK sınıfını yakalayın
Servis ASPNETCORE_URLS değişkenini yok sayıyordotnet run, launchSettings.json dosyasını uygularBunun yerine urls seçeneğini verin
Her çağrı 401 döndürüyorX-API-Key başlığı Solver:ApiKey değerine eşit değilCapSkip’in değil, servis anahtarını gönderin
Sağlık denetiminden 503CapSkip’in portunda hiçbir şey bağlantı kabul etmiyorCapSkip’i başlatın ve Local veya Server modunu kontrol edin
Her çözümde 502CapSkip, CapSkip bölümündeki anahtarı reddettiOnu CapSkip’te ayarlanan anahtarla eşleştirin
Son sürenin tamamı geçtikten sonra 504CapSkip sessiz ya da çözüm yavaşÇözüme CapSkip’in Monitor sekmesinden bakın, sonra son süreyi uzatın

Microsoft’un minimal API’lere genel bakış sayfası çerçeve tarafını ele alır, her CapSkip hata kodu ise şurada listelenir: API belgeleri.

FAQ

Yalnızca tek bir uygulama CAPTCHA çözüyorsa bir servise ihtiyacım var mı?

Hayır. SDK’yı o uygulamadan çağırın. Servis, birkaç çağıran ya da başka dillerdeki çağıranlar tek bir CapSkip’i paylaştığında ve tek bir son süre, tek bir sınır ve tek bir anahtar istediğinizde işe yarar. Bir arka plan işi için süreç içi yolu Worker Service rehberi gösteriyor.

Servisi Azure’da ya da başka bir sağlayıcıda barındırabilir miyim?

Evet, ama barındırılan bir servis başkasının makinesinde çalışır, bu yüzden bilgisayarınızdaki 127.0.0.1’e ulaşamaz. CapSkip’i kontrol ettiğiniz bir Windows makinesinde, tercihen statik bir genel IP ile Server modunda çalıştırın ve CapSkip__Host değerini o adrese ayarlayın. API key validation’ı açın, diğer her adres için bir engelleme kuralı ekleyin ve anahtarın düz HTTP üzerinden açık metin olarak gittiğini unutmayın. Bu rehber için hiçbir barındırma sağlayıcısı test edilmedi.

İptal edilen bir istek CapSkip’teki çözümü durdurur mu?

Hayır. Servis yoklamayı bırakır ve slot’unu serbest bırakır. SDK’da iptal çağrısı yoktur, bu yüzden CapSkip’in işle çalışmaya devam ettiğini ve bitene kadar bir thread tuttuğunu varsayın. İptaller seyrek kalsın diye son süreyi normal bir çözüme yetecek kadar uzun tutun.

Aynı endpoint reCAPTCHA v3’ü çözebilir mi?

Evet. İstek gövdesinde version olarak v3 ve bir action gönderin; seçenekler SDK’ya olduğu gibi ulaşır. CapSkip’te score ayarı olmadığı için servis hiçbir score değeri göndermez. Bu rehberin kullandığı onay kutusu türünü reCAPTCHA v2 çözücü sayfası ele alıyor.

Kısa özet

Minimal API CAPTCHA çözücü oluşturmak için tek bir CapSkipClient, tek bir SemaphoreSlim ve istek başına bağlantılı tek bir iptal token’ı tutun. Önce servis anahtarını denetleyin. İstisna türünden önce iki iptal bayrağını denetleyin. Servisi ortam değişkeniyle değil, urls seçeneğiyle başlatın. CapSkip Server modunda çalışıyorsa onun için bir engelleme kuralı ekleyin.

Paylaşılan bir servis, kimse çözümleri saymadan bütün bir ekibe hizmet verebilir. Servisi kendi Windows makinenizde çalışan bir yerel captcha çözücü ile birlikte kullanmak, her çözümü çağrı başına ücretten uzak tutar.