Cara Membuat Endpoint Pemecah CAPTCHA Minimal API di C#

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

Untuk membuat pemecah CAPTCHA minimal API, daftarkan satu CapSkipClient sebagai singleton di ASP.NET Core. Sediakan endpoint POST yang menerima sitekey dan URL halaman. Bungkus setiap panggilan dengan tenggat dan SemaphoreSlim, lalu petakan keempat exception SDK ke kode status HTTP. Scraper, worker, dan skrip Anda dalam bahasa apa pun kemudian memanggil satu alamat internal. Mereka tidak pernah melihat port atau key CapSkip. CapSkip berjalan di mesin Windows Anda sendiri, jadi service ini juga tidak memiliki biaya per pemecahan. Panduan ini membangun service tersebut dalam sekitar 120 baris dan menunjukkan apa yang dilakukan setiap bagian saat CapSkip lambat, ditutup, atau diam.

Apa yang Anda butuhkan

  • CapSkip yang berjalan di PC Windows, dengan pemecah reCAPTCHA diaktifkan. Lihat panduan penyiapan yang menjelaskan cara menjalankan CapSkip untuk pertama kali.
  • SDK .NET 10. Semua yang ada di bawah dijalankan di Windows 11 dengan SDK 10.0.100, ASP.NET Core 10.0.0, dan paket NuGet CapSkip 1.3.0.
  • Prompt PowerShell. Perintahnya memakai sintaks PowerShell.
  • Halaman dengan kotak centang reCAPTCHA v2. Service membutuhkan sitekey dan alamat halaman itu, dan tidak membutuhkan hal lain tentang halamannya.
  • Sebuah alamat untuk CapSkip. Dalam mode Local, CapSkip menjawab di 127.0.0.1 hanya untuk perangkat itu. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga service di mesin lain dapat memanggilnya melalui API. Keduanya diatur di pengaturan koneksi.

Angka-angka dalam panduan ini berasal dari mock CapSkip (pengganti CapSkip yang dibuat khusus untuk pengujian) di PC yang sama. ASP.NET Core dan SDK yang dipakai adalah yang asli. Mock menahan setiap pemecahan selama waktu yang ditetapkan, dua detik kecuali sebuah test menyebutkan lain, dan waktu pemecahan Anda yang sesungguhnya akan berbeda. Tidak ada bagian di sini yang dijalankan terhadap CapSkip sungguhan atau situs sungguhan.

Mengapa menaruh service di depan CapSkip

Jika satu aplikasi C# memecahkan CAPTCHA, panggil SDK dari aplikasi itu. Halaman pemecah CAPTCHA C# membahas hal itu. Service baru berguna jika beberapa pemanggil berbagi satu CapSkip. Saat itu, ada empat tugas yang sebaiknya berada di satu tempat.

  • Satu tenggat. SDK .NET melakukan polling hingga 300 detik secara default, jadi setiap pemanggil akan membutuhkan timer sendiri.
  • Satu batas. CapSkip menjalankan sejumlah pemecahan tertentu sekaligus, dan job tambahan menunggu. Antrean yang Anda kendalikan lebih mudah dipahami daripada antrean di dalam CapSkip.
  • Satu key. Pemanggil mendapat service key. Alamat dan API key CapSkip tetap berada di pengaturan service.
  • Bahasa apa pun. Skrip Python, situs PHP, dan tugas PowerShell semuanya berbicara HTTP dan JSON.

Langkah 1: buat proyek

Template web adalah proyek ASP.NET Core kosong: satu Program.cs, tanpa controller. Simpan proyek di folder tersendiri. Penginstalan paket hanya menulis ke file proyek.

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

Langkah 2: taruh pengaturan di appsettings.json

Bagian CapSkip berisi alamat, API key pemecah CAPTCHA, jumlah thread CapSkip yang harus dicocokkan, dan tenggat dalam detik. Bagian Solver berisi key yang akan dikirim pemanggil Anda. Ganti kedua placeholder sebelum Anda menjalankan apa pun.

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

Nilai apa pun dapat ditimpa oleh variabel lingkungan. Garis bawah ganda mewakili titik dua, jadi CapSkip__Port menimpa CapSkip:Port. Bagian berjudul Jalankan service terpisah dari CapSkip memakai cara ini untuk mengarahkan service ke mesin lain tanpa mengedit file.

Langkah 3: baca pengaturan dan tolak mulai tanpa key

Ganti seluruh isi Program.cs bawaan template dengan blok-blok berikut, dan tambahkan setiap blok baru di bawah blok sebelumnya. Proyek baru berhasil di-build setelah Langkah 10, karena record dan fungsi helper ada di bagian akhir.

Bagian atas Program.cs mem-bind bagian CapSkip ke sebuah record dan membaca service key. Pemeriksaan key memakai IsNullOrWhiteSpace, bukan pemeriksaan null dengan throw. Pada mock, string kosong lolos pemeriksaan null dan service pun menyala, lalu menerima setiap permintaan yang mengirim header kosong.

Blok ini juga memuat satu jebakan khusus .NET. System dan CapSkip sama-sama mendefinisikan kelas bernama TimeoutException. Dengan implicit usings dan using CapSkip, TimeoutException tanpa kualifikasi tidak bisa dikompilasi. Cabang switch pada System.TimeoutException bisa dikompilasi tetapi tidak pernah cocok dengan exception SDK. Di service ini, hal itu membuat timeout SDK masuk ke cabang catch-all dan mengembalikan 500, bukan 504. Baris using terakhir memberi kelas SDK nama tersendiri.

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

Langkah 4: daftarkan satu klien dan satu gerbang

CapSkipClient aman dipakai sebagai singleton. Di source SDK, klien ini tidak menyimpan state per pemanggilan, menyalin dictionary opsi pada setiap pemanggilan, dan semua instance berbagi satu HttpClient statis. Tidak ada yang perlu di-dispose, dan IHttpClientFactory tidak diperlukan. SDK memakai satu HttpClient statis privat dan tidak menerima milik Anda. Koneksi yang hidup lama tidak akan menyadari perubahan DNS, jadi pakai alamat IP untuk host CapSkip.

SemaphoreSlim adalah gerbangnya. Beri satu slot per thread CapSkip, yaitu pengaturan Max. Threads di CapSkip (default-nya 10). AddProblemDetails bersama UseStatusCodePages membuat jawaban 400, 404, 405, dan 415 yang ditulis sendiri oleh ASP.NET Core muncul dalam format yang sama dengan jawaban kita. Tanpa UseStatusCodePages, jawaban-jawaban itu tidak memiliki 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();

Langkah 5: periksa service key

Endpoint pemecahan tanpa perlindungan di alamat jaringan adalah pemecah gratis bagi siapa pun yang bisa menjangkau port-nya. Middleware ini menjaga path /solve. FixedTimeEquals membandingkan byte dalam waktu yang sama di mana pun byte itu berbeda, dan mengembalikan false untuk panjang yang berbeda, bukan melempar exception. Key yang hilang, key yang salah, dan key yang terlalu panjang semuanya mendapat 401 pada mock.

Yang dikembalikan adalah 401, bukan 403, karena hanya ada satu kredensial dan kredensial itu hilang atau salah. Middleware berjalan sebelum model binding, jadi permintaan tanpa key dan dengan body JSON yang rusak mendapat 401, bukan 400.

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

Langkah 6: tambahkan health check

SDK tidak memiliki panggilan ping, jadi endpoint health membuka koneksi TCP ke port CapSkip dan menyerah setelah tiga detik. Dengan CapSkip ditutup, endpoint ini menjawab 503 setelah sekitar dua detik, karena Windows menolak koneksi loopback selambat itu. Endpoint ini memberi tahu load balancer atau skrip pemantau bahwa service berjalan sedangkan CapSkip tidak. Endpoint ini tidak memeriksa apakah pemecahan berfungsi.

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

Langkah 7: validasi permintaan dan mulai tenggat

Parameter record tidak membuat field yang hilang menjadi error. System.Text.Json membiarkan propertinya null, jadi handler memeriksa sitekey dan URL sendiri lalu menjawab dengan validation problem.

Tenggat adalah satu linked CancellationTokenSource. Token ini dibatalkan saat pemanggil memutus koneksi (RequestAborted) atau saat tenggat lewat (CancelAfter). Setiap langkah berikutnya memakai token-nya, jadi hanya ada satu jam yang perlu diperhatikan.

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

Langkah 8: tunggu slot, pecahkan, lalu lepaskan

Penantian slot memakai token yang sama, ditambah batasnya sendiri, yaitu QueueSeconds (20 pada record pengaturan). Jika setiap slot tetap sibuk selama itu, pemanggil mendapat 503 dan bisa mencoba lagi. Tanpa batas, permintaan akan duduk di antrean sampai tenggat mengambilnya.

CapSkipError adalah kelas dasar dari keempat exception SDK, dan catch menyebutnya bersama exception pembatalan. RecaptchaAsync juga menerima token. SDK meneruskannya ke setiap permintaan poll, sehingga tenggat membatalkan panggilan HTTP yang sedang berjalan, bukan menunggunya selesai. Blok finally melepaskan slot hanya jika slot itu sudah diambil. Melepaskan slot yang tidak pernah Anda pegang akan meloloskan satu pemecahan lebih banyak daripada yang bisa dijalankan 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();
    }
});

Langkah 9: ubah exception menjadi kode status

Inilah bagian yang dirasakan pemanggil. Urutan case pada switch itu penting, karena SDK melaporkan panggilan yang dibatalkan dengan dua cara berbeda. Pengiriman yang dibatalkan muncul sebagai NetworkException (“The operation was canceled”). Poll yang dibatalkan muncul sebagai TaskCanceledException. Memetakan berdasarkan tipe exception akan memberi tahu pemanggil bahwa CapSkip mati, padahal sebenarnya tenggat yang lewat. Karena itu kedua flag diperiksa lebih dulu.

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

ApiException milik SDK membawa teksnya di message dan tidak memiliki properti code. Kasus tak terpecahkan dicocokkan dari kata UNSOLVABLE di message itu. Setiap kode status di bawah ini dipicu dengan sengaja terhadap mock.

StatusApa yang terjadiBerapa lama waktu yang dibutuhkan
503CapSkip ditutup, jadi pengiriman ditolak2.1 s
422CapSkip melaporkan ERROR_CAPTCHA_UNSOLVABLE3.8 s
502CapSkip menolak API key milik service sendirikurang dari 0.1 detik
400Permintaan v2 membawa action, yang hanya diterima v3kurang dari 0.1 detik
504Tenggat lewat saat CapSkip diam (tenggat uji 6 detik)6.0 s
504Timeout 5 detik milik SDK terpicu lebih dulu7.8 s

Status 502 itu disengaja. API key CapSkip yang ditolak adalah kesalahan service, bukan kesalahan pemanggil, jadi bukan 401 atau 403. Baris terakhir lebih lama dari 5 detik karena SDK memeriksa jamnya hanya setelah setiap penantian. SDK menunggu 0.25 detik, lalu menggandakan penantian hingga 5 detik. Penantian yang dimulai pada 3.75 detik berlangsung 4 detik, jadi timeout terpicu pada 7.75 detik. Jadwal yang sama menjelaskan baris 3.8 detik: hasilnya siap pada 2 detik dan ditemukan pada pemeriksaan 3.75 detik.

Satu kegagalan tidak pernah mendapat jawaban awal. Jika CapSkip ditutup setelah menerima job, SDK menganggap setiap poll yang ditolak sebagai belum siap. Pemanggil lalu menunggu sepanjang tenggat (sekitar 8.0 detik dengan tenggat 8 detik pada mock) dan mendapat 504. Hanya pengiriman yang ditolak yang memberi 503 dengan cepat.

Langkah 10: tambahkan record

Record pengaturan membawa nilai default. Record permintaan membangun dictionary opsi hanya dari field yang dikirim pemanggil. Pemanggil v3 bisa mengirim version dan action, sedangkan pemanggil v2 tidak mengirim keduanya. SdkTimeoutSeconds adalah batas milik SDK sendiri, dan QueueSeconds adalah berapa lama sebuah permintaan boleh menunggu slot. Keduanya tidak ada di appsettings.json, jadi nilai default berlaku. Jaga batas SDK di atas tenggat agar tenggat terpicu lebih dulu.

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

Jalankan di Windows

Pakai opsi urls pada dotnet run, dan jangan mengandalkan variabel ASPNETCORE_URLS. Template web menyertakan file launchSettings.json dengan port acak, dan dotnet run membacanya. Di PC uji, dotnet run biasa dengan ASPNETCORE_URLS yang disetel mengabaikan variabel itu, mendengarkan di port milik profil, dan beralih ke environment Development. Opsi urls mengalahkan profil, dan switch no-launch-profile membuat variabel berfungsi lagi serta memberi environment Production. Opsi urls saja masih membiarkan environment Development dari profil tetap aktif, jadi tambahkan juga switch itu jika Anda ingin Production. Tanpa profil dan tanpa URL yang disetel, Kestrel kembali ke localhost:5000, yang tidak diuji di sini.

Untuk PC yang sama, bind ke 127.0.0.1. Dengan begitu tidak ada yang lain di jaringan yang bisa menjangkau service.

# 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

Untuk pemanggil di mesin lain, bind ke 0.0.0.0, yang berarti setiap adapter jaringan. Di PC uji, alamat itu menjawab di IP LAN-nya sendiri dengan key yang benar dan menolak key yang salah dengan 401. Service yang di-bind ke 127.0.0.1 menolak alamat LAN. Setel service key yang sungguhan lebih dulu. Service key juga melintasi jaringan sebagai teks polos lewat HTTP biasa, jadi pakai ini hanya di jaringan yang Anda percaya. Windows Firewall mungkin meminta Anda mengizinkan program, jadi periksa aturan apa yang dibuatnya untuk port itu.

# 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

Jalankan service terpisah dari CapSkip

Service tidak harus berada di PC yang sama. Taruh CapSkip dalam mode Server di mesin Windows yang Anda kendalikan, sebaiknya dengan IP publik statis. Arahkan service ke sana dengan dua variabel lingkungan. Pengujian menjalankan bentuk ini terhadap mock yang di-bind ke alamat LAN, dan satu pemecahan memakan 1.85 detik.

# 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

Ada tiga peringatan. Aktifkan validasi API key di CapSkip dan pakai key yang sama di bagian CapSkip pada pengaturan. SDK membuat alamat http biasa, jadi key itu melintasi jaringan sebagai teks polos. CapSkip juga menambahkan aturan allow-any untuk port-nya, jadi tambahkan aturan blokir untuk setiap alamat yang tidak boleh menjangkaunya. Bagian pengaturan koneksi di panduan penyiapan mencantumkan kedua mode.

Panggil dari PowerShell atau Python

Pemanggil mengirim sitekey, URL halaman, dan service key. Jawabannya adalah objek JSON dengan satu field, token. Pakai token dalam sekitar dua menit, karena token reCAPTCHA kedaluwarsa, dan pakai sekali saja. Lihat panduan masa berlaku token yang menjelaskan hitungan waktunya.

# 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

Bahasa lain apa pun bekerja dengan cara yang sama. Pemanggil Python di bawah menyetel timeout yang lebih panjang daripada tenggat service. Dengan begitu 504 dari service tiba sebelum timer milik pemanggil menyerah.

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

Apa yang dilakukan dan tidak dilakukan gerbang

Dengan batas thread CapSkip diset ke 3 pada mock dan sepuluh permintaan serentak, gerbang menjaga paling banyak tiga job di mock. Tanpa gerbang, kesepuluhnya sampai ke mock dan tujuh menunggu thread. Gerbang tidak menambah kecepatan, karena tiga thread menentukan lajunya dalam kedua kasus. Dengan penahanan satu detik, sepuluh permintaan memakan sekitar 7.2 detik dengan gerbang dan 7.9 detik tanpanya. Selisihnya kurang dari satu detik. Nilai gerbang terlihat saat pemanggil menyerah.

Pada percobaan itu setiap pemanggil menyerah setelah 4 detik dan setiap pemecahan membutuhkan 6 detik. Dengan gerbang, tiga sampai lima job sampai ke CapSkip, karena penunggu bisa mengambil slot yang bebas sesaat sebelum tenggatnya sendiri. Tidak ada yang masih menunggu delapan detik kemudian. Tanpa gerbang, kesepuluhnya sampai ke CapSkip, dan tujuh masih menunggu enam detik setelah pemanggil menyerah. Tidak ada yang menunggu pemecahan itu, tetapi pemecahan itu tetap memegang thread.

Masih ada satu celah. Saat pemanggil memutus koneksi, service berhenti melakukan polling dan membebaskan slotnya. SDK tidak memiliki panggilan cancel, jadi anggap CapSkip menyelesaikan job dan memegang satu thread sampai selesai. Mock berperilaku seperti itu. Jumlah slot karena itu bisa mendahului thread CapSkip yang sebenarnya setelah rentetan pembatalan, sampai job-job itu selesai. CapSkip sungguhan tidak diuji, dan pengaturan Row Timeout-nya membatasi berapa lama sebuah job boleh berjalan.

Program.cs lengkap

Ini adalah Langkah 3 sampai 10 yang digabung, berurutan. Tempel di atas Program.cs bawaan template.

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

Kesalahan umum pada pemecah CAPTCHA minimal API

Apa yang Anda lihatPenyebabPerbaiki
CS0104 tentang TimeoutExceptionSystem dan CapSkip sama-sama mendefinisikannyaPakai alias SolverTimeout dari Langkah 3
500 untuk timeout SDKSebuah cabang switch menyebut System.TimeoutExceptionTangkap kelas SDK yang sudah di-alias
Service mengabaikan ASPNETCORE_URLSdotnet run menerapkan launchSettings.jsonBerikan opsi urls sebagai gantinya
Setiap panggilan mengembalikan 401Header X-API-Key tidak sama dengan Solver:ApiKeyKirim service key, bukan key milik CapSkip
503 dari health checkTidak ada yang menerima koneksi di port CapSkipJalankan CapSkip dan periksa mode Local atau Server
502 pada setiap pemecahanCapSkip menolak key di bagian CapSkipCocokkan dengan key yang disetel di CapSkip
504 setelah tenggat penuhCapSkip diam atau pemecahan lambatLihat pemecahan di tab Monitor CapSkip, lalu naikkan tenggat

Dari Microsoft, baca ikhtisar minimal API yang membahas sisi framework. Setiap kode error CapSkip tercantum di Dokumentasi API.

FAQ

Apakah saya membutuhkan service jika hanya satu aplikasi yang memecahkan CAPTCHA?

Tidak. Panggil SDK dari aplikasi itu. Service membantu jika beberapa pemanggil, atau pemanggil dalam bahasa lain, berbagi satu CapSkip dan Anda menginginkan satu tenggat, satu batas, dan satu key. Lihat panduan Worker Service yang menunjukkan jalur in-process untuk job latar belakang.

Bisakah saya meng-host service di Azure atau penyedia lain?

Bisa, tetapi service yang di-host berjalan di mesin milik pihak lain, jadi tidak bisa menjangkau 127.0.0.1 di PC Anda. Jalankan CapSkip dalam mode Server di mesin Windows yang Anda kendalikan, sebaiknya dengan IP publik statis, dan setel CapSkip__Host ke alamat itu. Aktifkan validasi API key, tambahkan aturan blokir untuk setiap alamat lain, dan ingat bahwa key dikirim sebagai teks polos lewat HTTP biasa. Tidak ada penyedia hosting yang diuji untuk panduan ini.

Apakah permintaan yang dibatalkan menghentikan pemecahan di CapSkip?

Tidak. Service berhenti melakukan polling dan membebaskan slotnya. SDK tidak memiliki panggilan cancel, jadi anggap CapSkip terus mengerjakan job dan memegang satu thread sampai selesai. Buat tenggat cukup panjang untuk pemecahan normal, agar pembatalan jarang terjadi.

Bisakah endpoint yang sama memecahkan reCAPTCHA v3?

Bisa. Kirim version v3 dan sebuah action di body permintaan, dan opsi itu sampai ke SDK apa adanya. Service tidak mengirim nilai score, karena CapSkip tidak memiliki pengaturan score. Halaman pemecah reCAPTCHA v2 membahas tipe kotak centang yang dipakai panduan ini.

Versi singkatnya

Untuk membuat pemecah CAPTCHA minimal API, pertahankan satu CapSkipClient, satu SemaphoreSlim, dan satu linked cancellation token per permintaan. Periksa service key lebih dulu. Periksa kedua flag pembatalan sebelum tipe exception. Jalankan service dengan opsi urls, bukan variabel lingkungan. Pasang aturan blokir di depan CapSkip jika berjalan dalam mode Server.

Service bersama bisa melayani seluruh tim tanpa ada yang menghitung jumlah pemecahan. Jika dijalankan di depan sebuah pemecah captcha lokal di mesin Windows Anda sendiri, setiap pemecahan bebas dari tagihan per panggilan.