Cara Memecahkan CAPTCHA di Worker Service .NET yang Tetap Berjalan

worker service captcha - How to Solve CAPTCHAs in a .NET Worker Service That Stays Up

Untuk memecahkan CAPTCHA di Worker Service .NET, daftarkan satu CapSkipClient sebagai singleton, panggil RecaptchaAsync dari ExecuteAsync dengan stopping token, dan tangkap exception milik CapSkip di dalam loop. Bagian terakhir itulah yang tidak disertakan template worker, dan dampaknya lebih besar daripada kelihatannya. Sejak .NET 6, exception yang tidak tertangani di BackgroundService menghentikan seluruh host. Proses lalu keluar dengan kode 0, sehingga opsi pemulihan (recovery options) Windows Service tidak pernah me-restart-nya. Satu NetworkException saat boot, yang dilempar karena CapSkip belum terbuka, sudah cukup. Penyiapan worker service CAPTCHA ini login ke sebuah situs di balik reCAPTCHA setiap 15 menit, menunggu pemecah CAPTCHA yang belum siap, berhenti dalam sepersekian detik, dan berjalan sebagai Windows Service di samping CapSkip.

Apa yang Anda butuhkan

  • CapSkip yang berjalan di komputer Windows, dengan solver reCAPTCHA diaktifkan.
  • SDK .NET 8 atau lebih baru. Kode di bawah sudah dijalankan di .NET 8 dan .NET 10.
  • Paket CapSkip dari NuGet, ditambah Microsoft.Extensions.Hosting.WindowsServices, yang dipakai Program.cs di bawah untuk berjalan sebagai Windows Service.
  • Hak administrator di mesin tempat Anda menginstal service, dan hanya untuk langkah itu.
  • Sebuah alamat untuk pemecah CAPTCHA. 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 worker di mesin lain dapat memanggilnya melalui API. Keduanya diatur di pengaturan koneksi.
dotnet new worker -n PortalWorker
cd PortalWorker
dotnet add package CapSkip
# Update Hosting before the next line: the template pins an older version,
# and the Windows Services package fails to restore against it (NU1605).
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Hosting.WindowsServices

Urutan dua baris terakhir itu penting. Template worker mengunci versi Microsoft.Extensions.Hosting yang dirilis bersama SDK Anda, sedangkan paket Windows Services saat ini membutuhkan versi yang lebih baru. Jika paket Windows Services ditambahkan lebih dulu, restore gagal dengan NU1605, sebuah error package downgrade. Memperbarui paket Hosting lebih dulu akan menghindarinya, dan tidak ada hal lain di template yang berubah.

Langkah 1: daftarkan satu klien di Program.cs

CapSkipClient tidak menyimpan state per permintaan dan mengirim semuanya melalui satu HttpClient statis di dalam paket, sehingga satu instance bisa melayani seluruh proses. Buat sekali dari konfigurasi dan daftarkan sebagai singleton.

// Program.cs
using CapSkip;
using PortalWorker;

var builder = Host.CreateApplicationBuilder(args);

// A Windows Service when the service manager starts it, an ordinary
// console app when you run it yourself.
builder.Services.AddWindowsService(options => options.ServiceName = "PortalWorker");

// One client for the whole process. The SDK reads no environment
// variables, but the host's configuration does, so CAPSKIP_HOST can come
// from the environment or from appsettings.json.
var config = builder.Configuration;
builder.Services.AddSingleton(new CapSkipClient(
    apiKey: config["CAPSKIP_API_KEY"] ?? "capskip",
    host: config["CAPSKIP_HOST"] ?? "127.0.0.1",
    port: int.Parse(config["CAPSKIP_PORT"] ?? "8080")));
builder.Services.AddSingleton<PortalLogin>();
builder.Services.AddHostedService<Worker>();

builder.Build().Run();

AddWindowsService tidak melakukan apa pun saat Anda menjalankan program sendiri, sehingga build yang sama berjalan di konsol selama Anda mengujinya dan sebagai service nanti. SDK CapSkip tidak membaca variabel lingkungan dengan sendirinya, tetapi objek konfigurasi di balik Host.CreateApplicationBuilder membaca setiap variabel lingkungan, sehingga CAPSKIP_HOST bisa diambil dari variabel lingkungan. CAPSKIP_HOST juga bisa ditulis sebagai key di appsettings.json, tempat yang lebih mudah untuk service, karena variabel lingkungan milik service merepotkan untuk diubah. Saat Windows menjalankan service, folder kerjanya adalah System32. Host lalu memakai folder milik program itu sendiri sebagai content root, sehingga host membaca appsettings.json yang di-publish di samping file exe.

Langkah 2: job yang memecahkan CAPTCHA

Job-nya sendiri hanyalah kode biasa. Job ini mengambil halaman login, membaca sitekey dan field tersembunyi, meminta token ke CapSkip, lalu mengirim form. Simpan job di kelasnya sendiri, sehingga worker hanya memutuskan kapan job berjalan dan apa arti sebuah kegagalan.

// PortalLogin.cs
using System.Net;
using System.Text.RegularExpressions;
using CapSkip;

namespace PortalWorker;

public sealed class PortalLogin(CapSkipClient solver, ILogger<PortalLogin> logger)
{
    const string PageUrl = "https://example.com/login";

    public async Task RunAsync(CancellationToken ct)
    {
        // A new HttpClient per run keeps its own cookies, so one run's
        // session never leaks into the next.
        using var http = new HttpClient();
        var html = await http.GetStringAsync(PageUrl, ct);
        var sitekey = Regex.Match(html, "data-sitekey=\"([^\"]+)\"").Groups[1].Value;

        // The form's hidden fields, such as a CSRF token.
        var fields = new Dictionary<string, string>();
        foreach (Match input in Regex.Matches(html, "<input[^>]*type=\"hidden\"[^>]*>"))
        {
            var name = Regex.Match(input.Value, "name=\"([^\"]+)\"").Groups[1].Value;
            var value = Regex.Match(input.Value, "value=\"([^\"]*)\"").Groups[1].Value;
            fields[name] = WebUtility.HtmlDecode(value);
        }

        // Pass the token on, so stopping the service cancels the solve.
        var result = await solver.RecaptchaAsync(sitekey, PageUrl, cancellationToken: ct);

        fields["username"] = "YOUR_USERNAME";
        fields["password"] = "YOUR_PASSWORD";
        fields["g-recaptcha-response"] = result.Code;
        using var post = new HttpRequestMessage(HttpMethod.Post, PageUrl)
        {
            Content = new FormUrlEncodedContent(fields),
        };
        // HttpClient sends no Referer; strict servers refuse a post without one.
        post.Headers.Referrer = new Uri(PageUrl);
        using var response = await http.SendAsync(post, ct);
        if (response.IsSuccessStatusCode)
            logger.LogInformation("Login posted: {Status}", (int)response.StatusCode);
        else
            logger.LogWarning("Login rejected: {Status}", (int)response.StatusCode);
    }
}

Ada tiga detail yang mudah terlewat. HttpClient baru untuk setiap eksekusi menyimpan cookie hanya untuk eksekusi itu, sehingga cookie sesi dan token CSRF di field tersembunyi tetap cocok saat form dikirim kembali. HttpClient tidak mengirim header Referer, dan framework seperti Django menolak form post HTTPS tanpa header itu, jadi job ini menyetelnya. Lalu stopping token diteruskan ke RecaptchaAsync, dan itulah yang memungkinkan service berhenti di tengah pemecahan alih-alih menunggunya selesai.

Pecahkan tepat sebelum Anda mengirim form. Token reCAPTCHA kedaluwarsa dua menit setelah diterbitkan, jadi worker yang memecahkan terlalu awal dan mengirim belakangan akan mengirim token yang sudah mati. Parsing halaman di sini sengaja dibuat minimal. Widget invisible, key Enterprise, dan varian lainnya dibahas di panduan memecahkan reCAPTCHA v2 di C#.

Langkah 3: loop yang bertahan saat pemecahan gagal

Sebagian besar bug worker service CAPTCHA ada di file ini. Loop bawaan template sama sekali tidak memiliki blok try, dan itu tidak masalah untuk mencatat waktu ke log, tetapi fatal untuk apa pun yang berkomunikasi dengan program lain.

// Worker.cs
using CapSkip;

namespace PortalWorker;

public sealed class Worker(PortalLogin job, ILogger<Worker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var wait = TimeSpan.FromMinutes(15);
                try
                {
                    await job.RunAsync(stoppingToken);
                }
                catch (NetworkException ex) when (!stoppingToken.IsCancellationRequested)
                {
                    // CapSkip is not running yet, as after a reboot. Try again soon.
                    logger.LogWarning("CapSkip unreachable: {Message}", ex.Message);
                    wait = TimeSpan.FromSeconds(30);
                }
                catch (CapSkipError ex) when (!stoppingToken.IsCancellationRequested)
                {
                    logger.LogWarning("Solve failed: {Type}: {Message}", ex.GetType().Name, ex.Message);
                }
                catch (Exception ex) when ((ex is HttpRequestException or TaskCanceledException)
                                           && !stoppingToken.IsCancellationRequested)
                {
                    // The site failed, or HttpClient gave up on it after 100 s.
                    logger.LogWarning("The site did not answer: {Message}", ex.Message);
                }

                await Task.Delay(wait, stoppingToken);
            }
        }
        catch (Exception) when (stoppingToken.IsCancellationRequested)
        {
            // Stopping. A cancelled poll ends in TaskCanceledException and a
            // cancelled submit in NetworkException; neither is a failure.
        }
        catch (Exception ex)
        {
            logger.LogCritical(ex, "Worker failed");
            // Exit non-zero, so the service's recovery options restart it.
            // A host that stops on its own exits with code 0.
            Environment.Exit(1);
        }
    }
}

Setiap catch menjawab satu pertanyaan. NetworkException berarti CapSkip tidak menjawab, biasanya karena belum berjalan, jadi worker mencoba lagi dalam 30 detik, bukan 15 menit. CapSkipError jenis lain adalah pemecahan yang gagal, misalnya ApiException dengan kode error dari CapSkip, jadi worker mencatatnya ke log dan menunggu eksekusi berikutnya. HttpRequestException atau TaskCanceledException yang muncul saat tidak ada penghentian yang sedang berlangsung berasal dari situs. TaskCanceledException di sini berarti HttpClient menyerah setelah batas default-nya, 100 detik. Setiap kegagalan dicatat di level Warning, termasuk login yang ditolak, dengan alasan yang dijelaskan di bagian tentang service.

Filter pada stopping token mencegah penghentian terlihat seperti kegagalan. Catch luar lalu menangani penghentian itu sendiri, dan catch terakhir menangani semua hal yang tidak terduga, misalnya bug pada parsing. Catch itu mencatat exception ke log dan mengakhiri proses dengan exit code 1, satu-satunya detail yang memungkinkan Windows me-restart service.

Mengapa satu exception menghentikan seluruh host

Host memulai ExecuteAsync dan memantau task yang dikembalikannya. Sejak .NET 6, jika task itu gagal, host menerapkan HostOptions.BackgroundServiceExceptionBehavior, yang default-nya StopHost. Ambil loop bawaan template lalu tutup CapSkip. Eksekusi di .NET 10 lalu mencatat BackgroundService failed dengan NetworkException di bawahnya, diikuti baris critical yang menyatakan host berhenti karena sebuah BackgroundService melempar exception. Setelah itu host melakukan shutdown. Satu koneksi yang ditolak, dan worker pun hilang.

Yang membuat hal ini merepotkan bagi sebuah service adalah exit code-nya. Host berhenti dengan bersih, sehingga proses keluar dengan kode 0, hasil yang kami ukur baik di .NET 8 maupun .NET 10. Windows menerapkan opsi pemulihan sebuah service hanya saat service itu gagal, dan keluar dengan bersih bukanlah kegagalan, sehingga service tetap saja dalam keadaan berhenti. Panduan Microsoft sendiri tentang menjalankan BackgroundService sebagai Windows Service menyarankan pemanggilan Environment.Exit dengan kode bukan nol karena alasan itu, dan worker di atas melakukannya di catch terakhirnya. Pengaturan lainnya, Ignore, lebih buruk: host tetap berjalan dengan worker yang mati di dalamnya, sebuah service yang terlihat sehat tetapi tidak melakukan apa pun.

Berhenti dengan bersih di tengah pemecahan

Saat Windows menghentikan service, atau Anda menekan Ctrl+C di konsol, host membatalkan stopping token dan menunggu ExecuteAsync selesai. Host menunggu hingga HostOptions.ShutdownTimeout, 30 detik secara default, lalu tetap berhenti. Pemecahan reCAPTCHA bisa memakan waktu jauh lebih lama dari itu, karena SDK melakukan polling hingga 300 detik. Tanpa token itu, penghentian yang diminta 5 detik setelah pemecahan 60 detik dimulai memakan waktu 30 detik dalam pengujian kami: host menunggu ShutdownTimeout sampai habis lalu meninggalkan pemecahan itu. Dengan token yang diteruskan, penghentian yang sama hanya memakan waktu sekitar 20 milidetik.

Pembatalan sampai ke kode Anda dalam dua bentuk, tergantung kapan terjadinya. Selama SDK melakukan polling, pembatalan berakhir sebagai TaskCanceledException dari jeda antar-polling. Jika pembatalan terjadi saat SDK sedang mengirim tugas, yaitu permintaan pertamanya ke CapSkip, SDK membungkusnya dalam NetworkException, yang merupakan CapSkipError dan bukan OperationCanceledException. Tanpa filter, kasus kedua itu terlihat seperti crash. Kami menghentikan worker di tengah pengiriman tugas, dan host mencatat BackgroundService failed dengan The operation was canceled di bawahnya, pada shutdown biasa. Itulah sebabnya setiap catch di atas memeriksa stopping token lebih dulu.

Langkah 4: jalankan sebagai Windows Service

Publish worker ke sebuah folder lalu daftarkan file exe-nya ke service manager. Setiap perintah di bawah kecuali dotnet publish membutuhkan PowerShell dengan hak administrator (elevated).

# In an elevated PowerShell. Paths are examples.
dotnet publish -c Release -r win-x64 -o C:\Services\PortalWorker
sc.exe create PortalWorker binpath= "C:\Services\PortalWorker\PortalWorker.exe" start= delayed-auto
sc.exe failure PortalWorker reset= 86400 actions= restart/60000/restart/60000/restart/60000
sc.exe start PortalWorker

Baris failure adalah pasangan dari Environment.Exit(1). Baris itu memberi tahu Windows untuk me-restart service satu menit setelah terjadi kegagalan. Windows mengulang aksi terakhir untuk setiap kegagalan berikutnya, sehingga service terus dijalankan kembali, dan hitungan kegagalan di-reset setelah satu hari tanpa kegagalan. Mulai otomatis tertunda (delayed automatic start) menjalankan service sesaat setelah service otomatis lainnya, bukan di awal proses boot. CapSkip sendiri adalah aplikasi desktop, jadi setelah reboot, CapSkip baru berjalan begitu seseorang masuk ke Windows, dan service bisa saja sudah dimulai jauh sebelum itu. Worker menangani kasus itu dengan sendirinya: worker mencatat CapSkip unreachable setiap 30 detik sampai pemecah CAPTCHA menjawab. Setiap percobaan ulang juga memuat halaman login lagi, sekitar 120 kali per jam selama Windows menunggu di layar masuk, jadi perpanjang jeda 30 detik itu jika situs bisa menyadarinya. Service dan aplikasi desktop berjalan di sesi Windows yang berbeda, tetapi 127.0.0.1 tetap terhubung lintas sesi, sehingga mode Local berfungsi di antara keduanya.

Sebagai service, host mengirim log-nya ke Windows Event Log, di Application log, dan secara default hanya entri di level Warning atau lebih tinggi yang masuk ke sana. Itulah sebabnya worker mencatat kegagalannya sebagai warning: baris Information tidak akan pernah muncul di Event Viewer.

Beberapa akun dalam satu eksekusi

Untuk login dengan beberapa akun di setiap putaran jadwal, buat loop di dalam job dengan Parallel.ForEachAsync alih-alih menjalankan lebih banyak worker. Setel MaxDegreeOfParallelism ke 10, karena CapSkip memecahkan 10 reCAPTCHA sekaligus secara default (Max. Threads di pengaturan reCAPTCHA-nya) dan memasukkan sisanya ke antrean. Berikan stopping token di CancellationToken milik ParallelOptions, lalu teruskan token yang diterima setiap iterasi ke RecaptchaAsync, sehingga penghentian membatalkan semua pemecahan sekaligus. Tangkap kegagalan setiap akun di dalam badan loop, dengan filter yang sama seperti di worker. Exception yang lolos dari satu akun membatalkan pemecahan semua akun lain dalam eksekusi itu. Singleton CapSkipClient aman dipakai bersama di semua pemanggilan itu. Beri setiap akun HttpClient-nya sendiri, seperti yang dilakukan job di atas, sehingga tidak ada dua sesi yang berbagi cookie.

Menjalankan solver di mesin lain

127.0.0.1 sudah benar selama worker dan CapSkip berada di PC Windows yang sama. Worker bisa saja berjalan di tempat lain, di server Windows lain, VM, atau host container. Dalam kasus itu, alihkan CapSkip ke mode Server agar ia mendengarkan di alamat jaringan atau IP publik Anda, lalu setel CAPSKIP_HOST ke alamat itu di appsettings.json atau di variabel lingkungan. Gunakan IP publik statis jika rutenya melewati internet dan aktifkan validasi API key. CapSkip membuka port-nya untuk semua alamat saat mode Server dimulai, jadi untuk membatasinya hanya pada mesin yang Anda harapkan, tambahkan aturan blokir Windows Firewall untuk alamat lainnya. Pemecah CAPTCHA tetap berupa mesin Windows milik Anda sendiri, dan pemecahannya tetap tanpa kuota.

Kesalahan umum worker service CAPTCHA

Apa yang Anda lihatPenyebabPerbaiki
NU1605, package downgrade terdeteksi, saat menambahkan paket Windows ServicesTemplate mengunci Microsoft.Extensions.Hosting versi yang lebih lama daripada yang dibutuhkan paket Windows ServicesTambahkan Microsoft.Extensions.Hosting lebih dulu, lalu paket Windows Services
BackgroundService failed, lalu baris critical tentang StopHost, dan worker pun hilangSebuah exception lolos dari ExecuteAsync; perilaku default StopHost menghentikan hostTangkap CapSkipError dan exception dari situs di dalam loop
Service berhenti setelah error dan Windows tidak pernah me-restart-nyaHost yang dihentikan oleh StopHost keluar dengan kode 0, yang bagi Windows bukan kegagalanAkhiri kegagalan tak terduga dengan Environment.Exit(1) dan atur pemulihan dengan sc.exe failure
BackgroundService failed dengan The operation was canceled, pada penghentian normalPenghentian terjadi saat tugas sedang dikirim ke CapSkip, dan SDK membungkusnya dalam NetworkExceptionFilter setiap catch berdasarkan stopping token
Menghentikan service memakan waktu 30 detikPemecahan mengabaikan pembatalan, sehingga host menunggu ShutdownTimeout sampai habisTeruskan stopping token ke RecaptchaAsync dan ke setiap panggilan HTTP
CapSkip unreachable di log setelah setiap rebootService dimulai sebelum ada yang masuk ke Windows dan membuka CapSkipTidak perlu diperbaiki: worker mencoba lagi setiap 30 detik sampai CapSkip menjawab
NetworkException yang menyatakan mesin tujuan secara aktif menolak koneksiCapSkip tidak berjalan, atau host atau port-nya salahJalankan CapSkip lalu cocokkan CAPSKIP_HOST dan CAPSKIP_PORT dengan pengaturan koneksinya
Situs menolak token, atau menjawab dengan halaman login-nya lagiToken kedaluwarsa sebelum form dikirim, atau kiriman form tidak menyertakan cookie, field tersembunyi, atau RefererPecahkan tepat sebelum mengirim, dari HttpClient yang mengambil halaman tersebut
Tidak ada apa pun dari worker di Event ViewerSecara default, Event Log hanya menerima Warning ke atasCatat kegagalan di level Warning, atau setel LogLevel EventLog ke Information di appsettings.json
ApiException dengan ERROR_KEY_DOES_NOT_EXIST atau ERROR_WRONG_USER_KEYValidasi API key aktif di CapSkip, dan worker mengirim key default atau tidak mengirim key sama sekaliSetel CAPSKIP_API_KEY ke key yang dikonfigurasi di CapSkip

FAQ

Apakah sebaiknya saya menyetel BackgroundServiceExceptionBehavior ke Ignore saja?

Tidak. Dengan Ignore, host mencatat exception sekali sebagai error lalu terus berjalan, tetapi ExecuteAsync tidak pernah dipanggil lagi, sehingga proses tetap berjalan tanpa ada apa pun yang bekerja di dalamnya. Windows melaporkan service sebagai berjalan, dan tidak ada login yang terjadi sampai seseorang menyadarinya. Tangkap kegagalan yang sudah diperkirakan di dalam loop, dan biarkan sisanya mengakhiri proses dengan exit code bukan nol.

Apakah hal yang sama berlaku untuk hosted service di ASP.NET Core?

Ya. Aplikasi ASP.NET Core berjalan di Generic Host yang sama. Jadi BackgroundService di aplikasi web memiliki default StopHost yang sama dan shutdown timeout 30 detik yang sama, dan exception CapSkip yang tidak tertangani ikut menjatuhkan seluruh situs web. Loop di atas bisa dipakai di sana tanpa perubahan, tetapi perhatikan bahwa Environment.Exit(1) di dalamnya juga mengakhiri seluruh aplikasi web.

Kapan Hangfire lebih cocok daripada worker?

Saat pemecahan berupa job yang dimasukkan ke antrean oleh kode lain, bukan satu tugas yang berjalan dengan timer. Hangfire menyimpan setiap job, mencoba ulang job sesuai jadwalnya sendiri, dan menampilkan riwayatnya di dashboard, hal-hal yang pada worker biasa harus Anda tangani sendiri. Filter retry-nya butuh penanganan khusus terhadap exception CapSkip, seperti yang dijelaskan di panduan memecahkan CAPTCHA di job Hangfire.

Bisakah worker berjalan di mesin yang berbeda dari CapSkip?

Bisa. Alihkan CapSkip ke mode Server di pengaturan koneksi, agar ia mendengarkan di alamat jaringan, bukan di loopback, lalu setel CAPSKIP_HOST ke alamat itu. Worker kemudian memanggilnya melalui API HTTP yang sama dari mesin mana pun yang bisa menjangkau port tersebut, dengan aturan blokir Windows Firewall untuk alamat yang seharusnya tidak bisa menjangkaunya. Pemecah CAPTCHA tetap berada di perangkat milik Anda, jadi tidak ada yang berubah soal cara pemecahan dihitung.

Versi singkatnya

Job worker service CAPTCHA membutuhkan tiga hal yang tidak diberikan template. Tangkap CapSkipError dan exception dari situs di dalam loop, karena satu exception yang lolos menghentikan host dan keluar dengan kode 0. Teruskan stopping token ke RecaptchaAsync dan filter setiap catch berdasarkan token itu, sehingga penghentian hanya butuh sesaat, bukan 30 detik, dan tidak pernah tercatat sebagai crash. Lalu akhiri kegagalan yang benar-benar tak terduga dengan Environment.Exit(1), agar opsi pemulihan service bisa me-restart-nya. Coba lagi setiap 30 detik selama CapSkip belum siap, dan beralihlah ke mode Server begitu worker tidak lagi berada di mesin pemecah CAPTCHA.

Worker yang berjalan setiap 15 menit selama berbulan-bulan menghasilkan ribuan pemecahan. Di situlah bypass captcha di mesin Windows Anda sendiri sangat menguntungkan: jumlah pemecahan tidak pernah muncul di tagihan mana pun.