Cara Membuat Detektor CAPTCHA di Node.js dan Memecahkannya

captcha detector in node.js - How to Build a CAPTCHA Detector in Node.js and Solve It

Detektor CAPTCHA di Node.js membaca HTML halaman untuk menemukan skrip dan container widget, mengambil sitekey dari keduanya, lalu memetakan keluarganya ke metode CapSkip yang memecahkannya. Kotak reCAPTCHA v2, skrip v3, dan widget Turnstile memerlukan tiga panggilan berbeda dengan opsi berbeda, dan tebakan yang salah menyia-nyiakan satu pemecahan dan mengembalikan hasil yang tidak bisa Anda pakai. Panduan ini membangun detektor sepanjang sekitar 165 baris tanpa paket selain capskip. Detektor mengembalikan jenis, metode, dan argumennya, atau menyatakan dengan jelas bahwa ia tidak bisa menentukan. CapSkip berjalan di PC Windows Anda sendiri tanpa biaya per pemecahan, jadi skrip yang memeriksa seratus halaman tidak menambah biaya apa pun. Detektor yang sama dalam Python ada di panduan deteksi jenis CAPTCHA.

Apa yang Anda butuhkan

  • CapSkip yang berjalan di PC Windows, dengan pemecah yang Anda perlukan diaktifkan. Lihat panduan penyiapan untuk menjalankannya pertama kali.
  • Node.js 20 atau lebih baru. Kode di bawah dijalankan pada Node.js 24.22.0, dan detektor serta langkah solve juga dijalankan pada 20.11.1 dan 25.2.1, dengan capskip 1.3.0. Paketnya sendiri memerlukan Node.js 18 atau lebih baru, tetapi Node.js 18 tidak dicoba di sini.
  • Prompt PowerShell. Perintahnya memakai sintaks PowerShell.
  • Halaman milik Anda atau yang boleh Anda otomatisasi, yang memiliki CAPTCHA di dalamnya.
  • 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 skrip di PC lain, VPS, atau platform hosting dapat menjangkaunya melalui API. Keduanya diatur di pengaturan koneksi.

Cara pengujiannya: detektor dijalankan pada 66 halaman tersimpan yang sama dengan versi Python dan mengembalikan hasil yang sama untuk setiap halaman. Dari jumlah itu, 59 dibuat dengan tangan (sekitar 40 mengikuti dokumentasi vendor, sisanya kasus tepi yang saya buat sendiri) dan 7 adalah snapshot halaman publik yang diambil pada 29 Oktober 2026. Pemecahan dijalankan melalui paket capskip yang asli terhadap mock CapSkip yang meniru pemeriksaan input aplikasi. Aplikasi yang asli tidak berjalan, jadi tidak ada angka di bawah ini yang merupakan waktu pemecahan.

Mengapa Anda mendeteksi jenis CAPTCHA sebelum memecahkannya

CapSkip punya metode berbeda untuk setiap keluarga: recaptcha, turnstile, geetest, altcha, capy, captchafox, friendlyCaptcha, dan normal untuk gambar di SDK, ditambah hcaptcha di HTTP API, yang belum dibungkus oleh SDK. Di dalam reCAPTCHA, opsinya juga penting. Invisible, Enterprise, dan v3 adalah flag pada panggilan yang sama. Kirim yang salah dan kemungkinan terbaiknya adalah job yang gagal, yang terburuk adalah token yang ditolak situs tanpa petunjuk apa pun mengapa. Saya tidak bisa memeriksa mana yang dilakukan aplikasi asli untuk versi reCAPTCHA yang salah. Bagaimanapun, pemecahan itu sudah terpakai.

Kesalahan lain gagal dengan cepat dan jelas. Membaca sitekey dari halaman yang merender reCAPTCHA secara eksplisit menghasilkan kata explicit, dan CapSkip menolaknya dengan ERROR_GOOGLEKEY. Panduan ERROR_GOOGLEKEY dan ERROR_PAGEURL membahas error itu dari sisi sebaliknya. Agar dapat mendeteksi jenis CAPTCHA dengan andal, detektor harus memeriksa sinyal dalam urutan tetap, dan urutan itu menghindari kedua jenis kesalahan. Detektor juga hal pertama yang perlu dibuat sebelum scraper yang menemui lebih dari satu situs. Jika Anda ingin tahu latar belakang tiap jenis, lihat apa itu CAPTCHA untuk penjelasan tiap keluarga tanpa kode.

Langkah 1: instal dan ambil halaman

Buat folder, inisialisasi proyek, dan instal capskip. Detektor hanya memakai regular expression, dan panggilan fetch memakai fetch bawaan Node.js.

npm init -y
npm install capskip

Detektor CAPTCHA di Node.js bekerja pada teks, jadi hanya memerlukan dua input: HTML halaman dan alamatnya. Pemanggilan fetch biasa menampilkan apa yang dikirim server sebelum JavaScript apa pun dijalankan. Halaman demo reCAPTCHA dari Google, demo Turnstile dari Cloudflare, dan demo hCaptcha semuanya memuat widget-nya dalam HTML itu. Halaman yang menambahkan widget dengan JavaScript tidak demikian, dan bagian “Saat HTML tidak menunjukkan apa-apa” membahas kasus itu. Fungsi fetch milik Node.js tidak menyimpan cookie, yang penting untuk langkah CAPTCHA gambar, jadi skrip di bawah meneruskannya secara manual.

Langkah 2: tulis fungsi bantu

Simpan semuanya dari sini sampai Langkah 9 dalam satu file bernama detect.js, blok demi blok. Blok pertama menulis pembaca kecil. Masing-masing mengembalikan null jika tidak ada yang cocok, sehingga atribut yang hilang tidak pernah melempar error.

'use strict';
const SITEKEY_JS = /sitekey['"]?\s*:\s*['"]([^'"]+)/; // sitekey: '...' inside a render() call
const NAMED = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&apos;': "'" };
const unescapeHtml = (s) => s.replace(/&(?:#x([0-9a-f]+)|#(\d+)|[a-z]+);/gi, (m, hex, dec) =>
  hex || dec ? String.fromCodePoint(parseInt(hex || dec, hex ? 16 : 10)) : NAMED[m.toLowerCase()] || m);
function grab(re, text) { // first capture group that matched, or null
  const m = new RegExp(re.source, 'is').exec(text || '');
  return m ? m.slice(1).find((g) => g !== undefined) ?? null : null;
}
const tag = (cls, h) => grab(new RegExp( // opening tag that has the CSS class cls
  `(<\\w+[^>]*\\sclass\\s*=\\s*["'][^"']*(?<![\\w-])${cls}(?![\\w-])[^"']*["'][^>]*>)`), h);
function attr(t, name) { // attribute value from a tag string, either quote style, unescaped
  const v = grab(new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`), t);
  return v === null ? null : unescapeHtml(v);
}
const renderOpts = (call, js) => grab(new RegExp(`${call}\\([^,]+,\\s*\\{(.{0,400})`), js);

Fungsi grab mengembalikan capture group pertama yang cocok. Fungsi tag menemukan tag pembuka berdasarkan class CSS dan menolak class yang hanya memuat namanya di dalam nama yang lebih panjang, sehingga class bernama cf-turnstile-response bukan widget Turnstile. Fungsi attr membaca atribut yang ditulis dengan kedua gaya tanda kutip dan melepas escape entitas HTML, yang penting untuk challenge yang disimpan di dalam atribut. Atribut tanpa tanda kutip tidak dibaca.

Blok kedua mengubah sebuah halaman menjadi sesuatu yang dapat dipakai bersama oleh pemeriksaan. Blok ini menghapus komentar dan blok style, sehingga aturan CSS yang menyebut class widget tidak bisa cocok, dan mengumpulkan alamat skrip serta JavaScript inline secara terpisah. Dua fungsi bantu setelah parse membangun jawabannya: hit, dan browser, yang membawa alasan needsBrowser. Jawaban ketiga, unsupported, dibangun oleh pemeriksaan di akhir blok ini, yang dijelaskan pada langkah berikutnya, bersama pemeriksaan hCaptcha.

function parse(html, url) {
  const h = html.replace(/<!--.*?-->|<style\b.*?<\/style>/gis, '');
  const srcs = [...h.matchAll(/<script\b[^>]*?\ssrc\s*=\s*["']([^"']+)/gi)].map((m) => m[1]).join(' ');
  const js = [...h.matchAll(/<script\b[^>]*>(.*?)<\/script>/gis)].map((m) => m[1]).join('\n');
  return { h, srcs: unescapeHtml(srcs), js, url };
}
const hit = (type, method, args) => ({ type, method, args });
const browser = (type, method, p, why) => ({ type, method, args: { url: p.url }, needsBrowser: why });

const UNSUPPORTED = { arkose: /arkoselabs\.com\/v2\/|funcaptcha\.com\/fc\//, geetest_v4: /gt4\.js|initGeetest4/ };
function unsupported(p) { // CapSkip cannot solve these: say so instead of misleading the reader
  const type = Object.keys(UNSUPPORTED).find((k) => UNSUPPORTED[k].test(p.srcs + ' ' + p.js));
  if (type) return { type, method: null, args: {}, unsupported: true };
}
const cfChallenge = (p) => /_cf_chl_opt\s*=\s*\{/.test(p.js) // the full-page "Just a moment..." page
  ? browser('turnstile_challenge', 'turnstile', p, 'data and pagedata (Cloudflare calls them cData and chlPageData) only exist at run time') : null;
function hcaptcha(p) { // before reCAPTCHA: a site that moved to hCaptcha may keep the g-recaptcha class
  if (!/hcaptcha\.com\/1\/api\.js/.test(p.srcs + ' ' + p.js)) return null;
  if (/\.setData\s*\(|rqdata/.test(p.js)) // Enterprise rqdata is issued per challenge
    return browser('hcaptcha_enterprise', 'hcaptcha', p, 'rqdata is issued for one challenge at run time');
  const w = tag('h-captcha', p.h) || tag('g-recaptcha', p.h);
  const key = attr(w, 'data-sitekey') || grab(SITEKEY_JS, renderOpts('hcaptcha\\.render', p.js));
  if (!key) return browser('hcaptcha', 'hcaptcha', p, 'no sitekey in the static HTML');
  const args = { sitekey: key, url: p.url };
  if (attr(w, 'data-size') === 'invisible') args.invisible = 1;
  return hit('hcaptcha', 'hcaptcha', args);
}

Langkah 3: hCaptcha, dan yang tidak bisa dipecahkan CapSkip

Ketiga pemeriksaan ini berada di akhir blok kedua di atas, jadi tidak ada yang baru untuk ditempel pada langkah ini. Mulailah dengan halaman yang jawaban benarnya adalah tidak. CapSkip tidak memecahkan FunCaptcha atau Arkose, dan GeeTest v4 berada di luar metode GeeTest, yang ditujukan untuk v3. Jika detektor melewati semua itu, halaman dengan kotak Arkose akan jatuh ke pemeriksaan berikutnya dan bisa dikembalikan sebagai sesuatu yang bukan dirinya. Karena itu pemeriksaan ini dijalankan pertama dan mengembalikan hasil tanpa metode.

Blok yang sama menangani pemeriksaan halaman penuh Cloudflare, interstisial “Just a moment”. Halaman itu bukan widget Turnstile. Metode turnstile membutuhkan dua nilai darinya, data dan pagedata, dan pengambilan biasa tidak memilikinya. Detektor mencari opsi challenge inline pada halaman dan menjawab needsBrowser. Panduan cf_clearance membahas apa yang terjadi setelah halaman challenge berhasil dilewati.

hCaptcha diperiksa berikutnya, sebelum reCAPTCHA. CapSkip memecahkannya, baik widget-nya berupa checkbox, invisible, maupun Enterprise, tetapi situs yang pindah dari reCAPTCHA ke hCaptcha sering mempertahankan class g-recaptcha pada widget-nya, dan hanya skrip hCaptcha yang membedakan keduanya. Pemeriksaan ini membaca sitekey dari widget atau dari panggilan hcaptcha.render, dan menambahkan invisible bila widget memiliki data-size yang diatur ke invisible. Halaman Enterprise yang memberikan nilai rqdata ke widget lewat hcaptcha.setData justru mendapat needsBrowser, karena nilai itu diterbitkan untuk satu challenge dan cepat kedaluwarsa.

Langkah 4: reCAPTCHA, dalam urutan yang benar

reCAPTCHA memerlukan kehati-hatian paling besar karena beberapa varian CapSkip berbagi dua nama skrip. Urutan pertanyaan itulah yang memisahkannya. Pertama nama skripnya: enterprise.js berarti flag Enterprise, api.js tidak. Lalu parameter render pada alamat skrip. Kunci setelah render= berarti v3, tetapi kata explicit setelah render= berarti halaman v2 yang merender widget-nya dari kode, dan kata itu tidak boleh dipakai sebagai kunci. Lalu markup. Tombol dengan class g-recaptcha dan atribut data-action adalah binding tombol v3 dari Google. Tombol tanpa data-action, atau elemen apa pun dengan data-size diatur ke invisible, adalah widget v2 invisible. Selain itu, apa pun dengan class tersebut adalah checkbox.

function recaptcha(p) { // Enterprise flag first, then v3 (render=KEY), then invisible, then v2
  const m = /\/recaptcha\/(api|enterprise)\.js(\?\S*)?/.exec(p.srcs);
  if (!m) return null;
  const ent = m[1] === 'enterprise' ? { enterprise: 1 } : {};
  const render = grab(/[?&]render=([^&\s]+)/, m[2]); // a v3 sitekey, or "explicit" (a v2 page)
  const t = tag('g-recaptcha', p.h), o = renderOpts('\\.render', p.js);
  const button = (t || '').toLowerCase().startsWith('<button'), dataAction = attr(t, 'data-action');
  const v3 = Boolean(render && render !== 'explicit') || Boolean(button && dataAction);
  const key = (render !== 'explicit' ? render : null) || attr(t, 'data-sitekey') || grab(SITEKEY_JS, o);
  if (!key) return browser('recaptcha', 'recaptcha', p, 'no sitekey in the static HTML (set by JavaScript)');
  const small = attr(t, 'data-size') === 'invisible' || grab(/size['"]?\s*:\s*['"](invisible)/, o);
  const invisible = !v3 && Boolean(button || small);
  const action = dataAction || grab(/execute\(\s*[^,)]+,\s*\{\s*['"]?action['"]?\s*:\s*['"]([^'"]+)/, p.js);
  const args = { sitekey: key, url: p.url, ...ent };
  if (v3) Object.assign(args, { version: 'v3' }, action ? { action } : {});
  if (invisible) args.invisible = 1;
  const kind = 'recaptcha_' + (v3 ? 'v3' : invisible ? 'v2_invisible' : 'v2');
  return hit(kind + (ent.enterprise ? '_enterprise' : ''), 'recaptcha', args);
}

Setiap hasil dipetakan ke panggilan yang sudah Anda kenal. Checkbox adalah solver.recaptcha dengan sitekey, URL halaman, dan objek opsi kosong. Invisible menambahkan invisible yang diatur ke 1, Enterprise menambahkan enterprise yang diatur ke 1, dan v3 menambahkan version yang diatur ke v3 serta action. Nama action berasal dari atribut data-action atau dari panggilan execute pada halaman; jika keduanya tidak ada di HTML, opsi itu dihilangkan dan CapSkip memakai nilai defaultnya, yaitu verify. Lihat halaman pemecah reCAPTCHA untuk setiap varian yang ditanganinya.

Langkah 5: Turnstile dan GeeTest

Widget Turnstile memerlukan dua hal pada halaman: api.js dari Cloudflare dan sebuah sitekey. Kuncinya berada di atribut data-sitekey pada sebuah div dengan class cf-turnstile, atau di panggilan turnstile.render. Pemeriksaan ini mensyaratkan skripnya, sehingga nama class nyasar di halaman yang tidak terkait tidak bisa memicunya. Pemeriksaan ini juga membaca data-action dan data-cdata bila ada, lalu meneruskannya. Halaman pemecah Turnstile mencantumkan opsinya.

GeeTest v3 memberi Anda dua nilai, dan salah satunya adalah jebakan. Nilai gt tetap untuk situs dan bisa berada di halaman. Nilai challenge hanya berlaku sekali, dan situs mengambilnya dari endpoint register saat dijalankan. Jika halaman memuat keduanya, detektor mengembalikannya. Jika halaman hanya memuat panggilan initGeetest, detektor menjawab needsBrowser, karena challenge yang ditemukan di HTML lama sudah mati.

function turnstile(p) { // widget only: implicit (div.cf-turnstile) or explicit (turnstile.render)
  if (!p.srcs.includes('challenges.cloudflare.com/turnstile/v0/api.js')) return null;
  const t = tag('cf-turnstile', p.h), o = renderOpts('turnstile\\.render', p.js);
  const sitekey = attr(t, 'data-sitekey') || grab(SITEKEY_JS, o);
  if (!sitekey) return browser('turnstile', 'turnstile', p, 'no sitekey in the static HTML');
  const action = attr(t, 'data-action') || grab(/\baction['"]?\s*:\s*['"]([^'"]+)/, o);
  const data = attr(t, 'data-cdata') || grab(/\bcData['"]?\s*:\s*['"]([^'"]+)/, o);
  return hit('turnstile', 'turnstile', { sitekey, url: p.url, ...(action && { action }), ...(data && { data }) });
}
function geetest(p) { // v3 only: gt is static, challenge comes from the register endpoint
  if (!(/initGeetest\s*\(/.test(p.js) || /\/gt(\.[\d.]+)?\.js/.test(p.srcs))) return null;
  const gt = grab(/\bgt['"]?\s*:\s*['"]([0-9a-f]{32})/, p.js);
  const challenge = grab(/\bchallenge['"]?\s*:\s*['"]([0-9a-f]{32,34})/, p.js);
  if (!(gt && challenge)) return browser('geetest', 'geetest', p, 'gt and challenge come from the register endpoint');
  return hit('geetest', 'geetest', { gt, challenge, url: p.url });
}

Langkah 6: ALTCHA dan Capy

ALTCHA mengubah nama atributnya antargenerasi. Versi 1 dan 2 memakai challengeurl, atau challengejson untuk challenge inline. Versi 3 dan seterusnya memakai satu atribut bernama challenge, yang menerima URL atau data challenge. Pemeriksaan ini membaca ketiganya dan menggabungkan URL relatif dengan alamat halaman. Di Node.js opsinya bernama challengeUrl dan challengeJson.

Capy tidak memerlukan div widget untuk pemeriksaan ini. Kuncinya diawali PUZZLE_ dan berada di parameter k pada alamat skrip widget, dan host selain default diteruskan sebagai apiServer.

function altcha(p) { // challengeurl / challengejson (v1, v2) or challenge (v3+)
  const t = grab(/(<altcha-widget\b[^>]*>)/, p.h);
  if (!t) return null;
  const c = attr(t, 'challenge');
  const asJson = attr(t, 'challengejson') || ((c || '').trimStart().startsWith('{') ? c : null);
  const asUrl = attr(t, 'challengeurl') || (asJson ? null : c);
  if (asJson) return hit('altcha', 'altcha', { url: p.url, challengeJson: asJson });
  if (asUrl) return hit('altcha', 'altcha', { url: p.url, challengeUrl: new URL(asUrl, p.url).href });
  return browser('altcha', 'altcha', p, 'no challenge attribute');
}
function capy(p) { // the widget script is .../puzzle/get_js/?k=PUZZLE_...
  const m = /((?:https?:)?\/\/[^\s"']+?)\/puzzle\/get_js\/\S*?(PUZZLE_[\w-]+)/.exec(p.srcs);
  if (!m) return null;
  const host = m[1].startsWith('http') ? m[1] : 'https:' + m[1];
  const more = host === 'https://jp.api.capy.me' ? {} : { apiServer: host };
  return hit('capy', 'capy', { sitekey: m[2], url: p.url, ...more });
}

Langkah 7: CaptchaFox dan Friendly Captcha

CaptchaFox hadir dalam dua build. Yang biasa dimuat dari cdn.captchafox.com. Yang lain dibundel oleh platform hosting di bawah path CDN bersama, sehingga detektor mencari nama paket CaptchaFox di path itu dan bukan path-nya saja, yang juga dipakai widget lain.

Friendly Captcha adalah yang paling rumit. Versi 1 dan versi 2 memakai div yang sama dan namespace sitekey yang sama, tetapi protokolnya berbeda, dan token untuk yang salah bisa ditolak situs. Skripnya membedakan keduanya: versi 2 memuat paket @friendlycaptcha/sdk, versi 1 memuat friendly-challenge. Bila halaman tidak memberi petunjuk, detektor menolak menebak dan menjawab needsBrowser.

const MAM = 'mampkg/@mamdev/core.frontend.libs.captchafox';
function captchafox(p) { // plain build from cdn.captchafox.com, or the bundled build from s.uicdn.com
  const t = tag('captchafox', p.h), key = grab(/captchafox[^"']*\/api\.js\?key=([^"'&]+)/, p.srcs);
  if (!t && !key) return null;
  const more = p.srcs.includes(MAM) ? { apiServer: 'https://s.uicdn.com/' + MAM + '/' } : {};
  const sitekey = attr(t, 'data-sitekey') || key;
  if (!sitekey) return browser('captchafox', 'captchafox', p, 'no sitekey in the static HTML');
  return hit('captchafox', 'captchafox', { sitekey, url: p.url, ...more });
}
function friendly(p) { // v2 loads @friendlycaptcha/sdk (site.min.js), v1 loads friendly-challenge
  const t = tag('frc-captcha', p.h);
  if (!t) return null;
  const v2End = attr(t, 'data-api-endpoint'), v1End = attr(t, 'data-puzzle-endpoint');
  const v2 = v2End !== null || /friendlycaptcha\/sdk|site(\.compat)?\.min\.js/.test(p.srcs);
  const v1 = v1End !== null || /friendly-challenge|widget(\.module)?\.min\.js/.test(p.srcs);
  if (v2 === v1) return browser('friendly_captcha', 'friendlyCaptcha', p, 'cannot tell v1 from v2 in the HTML');
  const server = v2End || v1End;
  if (!attr(t, 'data-sitekey')) return browser('friendly_captcha', 'friendlyCaptcha', p, 'no sitekey in the static HTML');
  return hit('friendly_captcha_' + (v2 ? 'v2' : 'v1'), 'friendlyCaptcha',
    { sitekey: attr(t, 'data-sitekey'), url: p.url, version: v2 ? 'v2' : 'v1', ...(server && { apiServer: server }) });
}

Masing-masing punya halaman pemecahnya sendiri dengan opsi yang diterima metodenya:

Langkah 8: CAPTCHA gambar, sebuah tebakan, dan pemeriksaan

CAPTCHA gambar tidak memiliki skrip vendor untuk ditemukan, jadi pemeriksaan ini hanya tebakan dan dijalankan hanya bila tidak ada yang lain cocok. Pemeriksaan ini mencari gambar yang tag-nya menyebut captcha, dan kotak teks di tempat lain pada halaman yang tag-nya menyebut captcha atau security code. Pemeriksaan ini meleset di setiap kasus ketika markup tidak memakai kata-kata itu, dan canvas yang digambar tidak terlihat olehnya. Anggap sebuah hit sebagai kemungkinan, bukan kepastian.

Blok yang sama mendaftar pemeriksaan secara berurutan dan mendefinisikan detect, yang diekspor di akhir Langkah 9. Urutan itulah logikanya: vendor unsupported dulu, lalu halaman Cloudflare, lalu hCaptcha, lalu setiap keluarga lainnya, dan tebakan gambar hanya bila daftar tidak menemukan apa-apa. Fungsi detect mengembalikan hasil pertama dan menambahkan daftar also bila halaman memuat lebih dari satu keluarga, misalnya skrip v3 ditambah kotak Turnstile. Daftar itu menyebut keluarga, bukan widget reCAPTCHA kedua dengan kunci lain.

function image(p) { // a guess: an <img> that says captcha, next to a text box that says captcha
  const box = /<input\b(?![^>]*type\s*=\s*["']?(?:hidden|checkbox|submit))[^>]*(?:captcha|security.?code)/i;
  const img = (p.h.match(/<img\b[^>]*>/gi) || []).find((i) => box.test(p.h) && /captcha/i.test(i) && attr(i, 'src'));
  if (img) return hit('image', 'normal', { image: new URL(attr(img, 'src'), p.url).href });
}
const CHECKS = [unsupported, cfChallenge, hcaptcha, recaptcha, turnstile, geetest, altcha, capy, captchafox, friendly];

function detect(html, pageUrl) {
  const p = parse(html, pageUrl);
  const found = CHECKS.map((check) => check(p)).filter(Boolean);
  const best = found[0] || image(p) || null; // the image guess comes last
  if (found.length > 1) best.also = found.slice(1).map((r) => r.type);
  return best;
}

Langkah 9: solve

Blok terakhir meneruskan hasil ke metode CapSkip yang tepat. Blok ini menolak apa pun yang unsupported atau ditandai needsBrowser dengan sebuah Error. Metode CapSkip menerima nilai utamanya berdasarkan posisi dan sisanya sebagai objek opsi, jadi sebuah tabel kecil menyatakan nilai mana ditaruh di mana. Opsi Node.js berbentuk camelCase, dengan satu pengecualian: bila Anda menambahkan polling interval per panggilan, namanya polling_interval, karena pollingInterval per panggilan melempar ValidationException. Metode gambar tidak menerima opsi per panggilan sama sekali, jadi atur pollingInterval pada konstruktor CapSkip untuk yang satu itu. Gambar diunduh dengan cookie yang Anda berikan, karena fetch tidak punya cookie jar dan unduhan URL oleh SDK sendiri tidak mengirim cookie. Cookie itu dikirim ke host mana pun tempat gambar berada, jadi bila gambar CAPTCHA Anda berada di host yang berbeda, jangan sertakan cookie.

SDK belum punya panggilan hCaptcha, jadi hasil hCaptcha diteruskan ke solveHcaptcha. Fungsi ini mengirim sitekey dan alamat halaman ke in.php dengan method=hcaptcha, lalu memeriksa res.php setiap 5 detik sampai token siap, sebagaimana bagian hCaptcha dalam dokumentasi API menjelaskannya. Berikan alamat solver sebagai api di argumen terakhir solve, dan API key Anda sebagai key bila validasi API key diaktifkan. Hasilnya membawa token di code dan User-Agent yang dipakai untuk memperolehnya di userAgent.

const POSITIONAL = { normal: ['image'], recaptcha: ['sitekey', 'url'], turnstile: ['sitekey', 'url'],
  geetest: ['gt', 'challenge', 'url'], altcha: ['url'], capy: ['sitekey', 'url'], captchafox: ['sitekey', 'url'],
  friendlyCaptcha: ['sitekey', 'url'] };

async function solveHcaptcha(args, api, key) { // no SDK call yet: post to in.php, poll res.php
  const form = { method: 'hcaptcha', sitekey: args.sitekey, pageurl: args.url, invisible: args.invisible || 0, json: 1 };
  if (args.data) form.data = args.data;
  if (key) form.key = key;
  const job = await (await fetch(`${api}/in.php`, { method: 'POST', body: new URLSearchParams(form) })).json();
  if (job.status !== 1) throw new Error(job.request);
  for (let i = 0; i < 36; i++) { // a solve takes 10 to 30 seconds, so give up after 3 minutes
    await new Promise((done) => setTimeout(done, 5000));
    const q = new URLSearchParams({ action: 'get', id: job.request, json: 1, ...(key ? { key } : {}) });
    const r = await (await fetch(`${api}/res.php?${q}`)).json();
    if (r.request !== 'CAPCHA_NOT_READY') {
      if (r.status !== 1) throw new Error(r.request);
      return { code: r.request, userAgent: r.useragent };
    }
  }
  throw new Error('hCaptcha was not solved within 3 minutes');
}

async function solve(info, solver, fetchOpts = {}, { api = 'http://127.0.0.1:8080', key } = {}) {
  if (info.unsupported || info.needsBrowser) throw new Error(info.needsBrowser || `${info.type} is not supported`);
  const args = { ...info.args };
  if (info.method === 'hcaptcha') return solveHcaptcha(args, api, key);
  if (info.method === 'normal' && !args.image.startsWith('data:')) { // fetch has no cookie jar: pass a Cookie header
    const res = await fetch(args.image, fetchOpts);
    if (!res.ok) throw new Error(`image download failed: HTTP ${res.status}`);
    args.image = 'data:image/png;base64,' + Buffer.from(await res.arrayBuffer()).toString('base64');
  }
  const names = POSITIONAL[info.method];
  const options = Object.fromEntries(Object.entries(args).filter(([k]) => !names.includes(k)));
  return solver[info.method](...names.map((k) => args[k]), options);
}
module.exports = { detect, solve };

Jalankan detektor CAPTCHA di Node.js

Simpan ini sebagai use.js di samping detect.js. Skrip mengambil halaman Anda, mencetak apa yang ditemukan detektor, dan hanya memanggil CapSkip bila ada metode untuk dipanggil dan tidak ada yang kurang. Skrip mengirim User-Agent yang menyerupai browser, karena banyak situs menolak yang default dari Node.js. Skrip mencetak status HTTP bila halaman tidak menjawab 200 dan melanjutkan, karena halaman challenge Cloudflare datang sebagai 403 atau 503. Jalankan dengan alamat halaman dalam tanda kutip, karena alamat yang berisi ampersand adalah error parse di PowerShell. Port 8080 adalah default; atur variabel lingkungan CAPSKIP_PORT, yang dibaca skrip ini sendiri, bila port Anda berbeda. Host tetap 127.0.0.1 di skrip ini, jadi ubah baris host untuk mode Server.

$env:CAPSKIP_PORT = "9090"   # only if your port is not 8080
node use.js "https://example.com/login"

Perhatikan bahwa fetch tidak menyimpan cookie, dan skrip ini membaca Set-Cookie hanya dari respons akhir, jadi cookie yang diatur pada redirect hilang. Untuk menyimpannya, panggil fetch dengan opsi redirect diatur ke manual, baca Set-Cookie dari jawaban redirect, dan minta sendiri alamat Location. Skripnya sendiri ada di bawah.

'use strict';
const { CapSkip } = require('capskip');
const { detect, solve } = require('./detect');

async function main(pageUrl) {
  const res = await fetch(pageUrl, { headers: { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/130.0 Safari/537.36' } });
  if (!res.ok) console.error(`page answered HTTP ${res.status}`); // a challenge page is a 403 or 503, so keep going
  const html = await res.text();
  const info = detect(html, res.url); // res.url is the final address after any redirect
  console.log(JSON.stringify(info, null, 2));
  if (!info || info.unsupported || info.needsBrowser) return;
  const cookie = res.headers.getSetCookie().map((c) => c.split(';')[0]).join('; ');
  const host = '127.0.0.1', port = Number(process.env.CAPSKIP_PORT || 8080);
  const solver = new CapSkip({ host, port });
  console.log(await solve(info, solver, cookie ? { headers: { cookie } } : {}, { api: `http://${host}:${port}` }));
}

main(process.argv[2]).catch((err) => {
  console.error(`${err.constructor.name}: ${err.message}`);
  process.exit(1);
});

Pada halaman dengan checkbox reCAPTCHA v2, print pertama menampilkan jenis, metode, dan argumennya, dan print kedua menampilkan hasilnya. Untuk reCAPTCHA, token ada di field code; metode lain memakai field berbeda, misalnya userAgent di samping token Turnstile dan hCaptcha. Token reCAPTCHA dimasukkan ke field g-recaptcha-response pada formulir, dan token hCaptcha ke h-captcha-response. Pada eksekusi mock, detektor mengembalikan hasil berikut:

Halamandetect mengembalikan
reCAPTCHA v2 checkboxtype recaptcha_v2, method recaptcha, sitekey dan url
Widget hCaptchatype hcaptcha, method hcaptcha, sitekey dan url
Halaman Cloudflare “Just a moment”type turnstile_challenge, needsBrowser diatur
Skrip v3 ditambah widget Turnstiletype recaptcha_v3, also mencantumkan turnstile

Saat HTML tidak menunjukkan apa-apa: konsol browser

Beberapa halaman menambahkan widget-nya dengan JavaScript setelah dimuat. Pengambilan biasa lalu tidak menemukan CAPTCHA sama sekali, dan detektor mengembalikan null untuk halaman yang memilikinya. Dua kasus diketahui dan memang diharapkan: skrip Google atau Cloudflare yang ditambahkan lewat kode, dan sitekey yang ditetapkan lewat kode. Untuk kasus itu, buka halaman di Chrome, tekan F12, dan tempel ini ke Console. Jika Chrome memblokir penempelan, ketik allow pasting lalu tekan Enter lebih dulu. Snippet ini membaca sitekey dari resource yang sudah dimuat halaman, sehingga ia melihat apa yang dibangun browser.

(() => {
  const iframes = [...document.querySelectorAll('iframe')].map((f) => f.src);
  const urls = performance.getEntriesByType('resource').map((r) => r.name).concat(iframes);
  const found = new Map(), add = (o) => found.set(JSON.stringify(o), o);
  for (const u of urls) {
    let m;
    if ((m = u.match(/recaptcha\/(?:api2|(enterprise))\/anchor\?.*?\bk=([\w-]+)/)))
      add({ type: 'recaptcha', sitekey: m[2], enterprise: !!m[1], invisible: /size=invisible/.test(u) });
    else if ((m = u.match(/recaptcha\/(?:api|enterprise)\.js\?.*?\brender=([\w-]+)/)) && m[1] !== 'explicit')
      add({ type: 'recaptcha v3', sitekey: m[1], enterprise: /enterprise\.js/.test(u) });
    else if ((m = u.match(/challenges\.cloudflare\.com\/.*turnstile\/.*\/(0x4[\w-]+|[123]x0+[A-Za-z]+)\//)))
      add({ type: 'turnstile', sitekey: m[1] });
    else if ((m = u.match(/hcaptcha\.com\/captcha\/.*[#&?]sitekey=([\w-]+)/))) add({ type: 'hcaptcha', sitekey: m[1] });
    else if (/arkoselabs\.com\/fc\//.test(u)) add({ type: 'arkose: CapSkip cannot solve' });
  }
  if (window._cf_chl_opt) add({ type: 'cloudflare challenge page' });
  document.querySelectorAll('altcha-widget').forEach((e) => add({ type: 'altcha', challenge: e.getAttribute('challenge') || e.getAttribute('challengeurl') }));
  document.querySelectorAll('.h-captcha[data-sitekey]').forEach((e) => add({ type: 'hcaptcha', sitekey: e.dataset.sitekey }));
  document.querySelectorAll('.frc-captcha, .captchafox').forEach((e) => add({ type: e.classList.contains('captchafox') ? 'captchafox' : 'frc-captcha', sitekey: e.dataset.sitekey }));
  const v3 = [...found.values()].filter((o) => o.type === 'recaptcha v3').map((o) => o.sitekey);  // a v3 badge also adds an invisible anchor frame
  const res = [...found.values()].filter((o) => !(o.type === 'recaptcha' && v3.includes(o.sitekey)));
  return res.length ? res : 'no known CAPTCHA found (GeeTest, Capy and CaptchaFox scripts are not checked)';
})();

Snippet itu menemukan sitekey dan flag pada halaman reCAPTCHA v2, halaman invisible, halaman v3, halaman Turnstile, dan demo hCaptcha, dan mencetak pesan pada halaman tanpa CAPTCHA. Tiga batasan: browser hanya menyimpan sekitar 250 entri resource secara default, jadi single page app yang berjalan lama bisa kehilangan entri awal, dan snippet tidak bisa membedakan Friendly Captcha versi 1 dari versi 2, serta tidak mencari skrip GeeTest, Capy, atau CaptchaFox. Pada pemeriksaan halaman penuh Cloudflare, snippet bisa menampilkan sitekey dari alamat resource, tetapi tidak nilai data dan pagedata, yang memerlukan hook pada panggilan render. Hook itu tidak diuji di sini.

Tempat deteksi gagal

SituasiYang dikembalikan detectApa yang harus dilakukan
Skrip widget ditambahkan oleh JavaScriptnullMuat halaman di browser dan jalankan snippet console
Sitekey ditetapkan oleh JavaScriptneedsBrowserBaca kunci dari halaman yang sedang berjalan
Pemeriksaan halaman penuh CloudflareneedsBrowserBaca nilainya di browser, lihat panduan cf_clearance. Halaman datang sebagai 403 atau 503, yang dilaporkan oleh use.js lalu tetap diperiksa
Skrip CaptchaFox, Friendly Captcha, atau Capy tanpa widget di HTMLnullJalankan snippet console atau baca source halaman
Halaman blokir (403, 503) tanpa widgetnulluse.js mencetak status lebih dulu: halaman blokir tidak memuat CAPTCHA untuk ditemukan
Arkose, GeeTest v4unsupportedCapSkip tidak bisa memecahkan ini
hCaptcha Enterprise yang mengatur rqdataneedsBrowserBaca rqdata baru dari halaman live untuk setiap tugas dan berikan ke solveHcaptcha sebagai data
Friendly Captcha tanpa petunjuk skripneedsBrowserPeriksa skrip mana yang dimuat halaman
CAPTCHA gambar tanpa kata captchanullTambahkan aturan Anda sendiri untuk situs itu

Kesalahan umum

KesalahanPenyebabPerbaiki
ERROR_GOOGLEKEYKuncinya adalah kata explicit, atau kunci yang bukan kunci reCAPTCHABaca kunci dari widget, bukan dari parameter render
ERROR_CAPTCHA_UNSOLVABLEJob yang gagal terbaca seperti ini dan jawabannya tidak menyebut penyebabnya. Versi reCAPTCHA yang salah mungkin berakhir di sini, tetapi saya tidak mengonfirmasinya pada aplikasi asliPeriksa dulu jenis yang terdeteksi, lalu lihat Dokumentasi API
ValidationExceptionOpsi yang tidak diterima metode, seperti action pada panggilan v2, pollingInterval per panggilan, atau opsi per panggilan apa pun pada metode gambarGunakan argumen yang dikembalikan detektor
NetworkExceptionCapSkip tidak berjalan, atau host atau port-nya salahJalankan CapSkip dan periksa pengaturan koneksi
Error dari solveHasilnya unsupported atau butuh browser, atau CapSkip menolak job hCaptcha dan pesannya adalah kode error-nyaTangani dua kasus pertama sebelum memanggil solve, dan periksa sitekey serta alamat halaman untuk kasus ketiga

Menjalankan solver di sebuah server

Detektor hanya memerlukan dua hal dari CapSkip: host dan port. Dalam mode Local itu adalah 127.0.0.1 dan PC Anda sendiri. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga detektor yang berjalan di PC lain, VPS, atau platform hosting dapat memanggilnya melalui API yang sama. Ubah host dan port di use.js, dan tidak ada hal lain dalam kode yang berubah. IP publik statis disarankan untuk akses jarak jauh yang stabil. Mode Server tetap berjalan di hardware Anda sendiri dan tetap tanpa meteran. Perhatikan bahwa mode Server membuka port untuk semua alamat, jadi tambahkan aturan blokir Windows Firewall untuk alamat yang tidak boleh menjangkaunya. Lihat pengaturan koneksi untuk cara beralih mode.

FAQ

Bisakah saya mengetahui CAPTCHA apa yang dipakai sebuah situs tanpa kode?

Bisa. Buka DevTools, masuk ke tab Network, muat ulang halaman, dan lihat alamat skripnya. Skrip google.com/recaptcha atau recaptcha.net adalah reCAPTCHA, challenges.cloudflare.com adalah Turnstile, dan js.hcaptcha.com adalah hCaptcha. Snippet Console di atas melakukan pencarian yang sama untuk Anda.

Mengapa detect mengembalikan needsBrowser alih-alih menebak?

Karena tebakan yang salah menyia-nyiakan satu pemecahan penuh dan mengembalikan token yang ditolak situs tanpa petunjuk mengapa. Bila HTML statis tidak memuat sitekey, challenge GeeTest, atau versi Friendly Captcha, menyatakannya lebih cepat daripada job yang gagal.

Apakah CapSkip memecahkan hCaptcha?

Ya. CapSkip memecahkan widget hCaptcha checkbox, invisible, dan Enterprise melalui HTTP API-nya dengan method=hcaptcha, sebagaimana Dokumentasi API menjelaskannya. SDK Node.js belum punya panggilan hCaptcha, jadi solveHcaptcha di atas mengirim job ke in.php sendiri. FunCaptcha, Arkose, dan GeeTest v4 masih belum didukung, dan detektor melaporkannya sebagai unsupported agar skrip dapat melewati halaman itu alih-alih gagal di sana.

Bisakah platform hosting memakai detektor dengan CapSkip saya?

Bisa, lewat mode Server. Atur CapSkip untuk mendengarkan di jaringan atau IP publik Anda, atur host dan port di use.js ke alamat itu, dan detektor serta panggilan solve bekerja sama seperti pada satu PC. Pemecah tetap berjalan di hardware Anda.

Versi singkatnya

Detektor CAPTCHA di Node.js membuktikan gunanya dengan berjalan lebih dulu. Deteksi jenis CAPTCHA, lalu pecahkan: baca skrip dan markup dalam urutan tetap, serahkan argumen ke metode yang tepat, dan perlakukan null, needsBrowser, dan unsupported sebagai jawaban, bukan error. Dengan CapSkip di komputer Anda sendiri, tidak ada meteran yang berjalan saat Anda memeriksa halaman, dan itulah yang membuat pemecah captcha lokal praktis untuk crawl yang menemui situs baru setiap beberapa menit. Untuk SDK Node.js itu sendiri, lihat halaman pemecah CAPTCHA Node.js.