Node.js ile CAPTCHA Algılayıcı Nasıl Yazılır ve CAPTCHA Nasıl Çözülür

Node.js’te bir CAPTCHA algılayıcısı, bir sayfanın HTML’inde widget’ın script’ini ve kapsayıcısını okur, sitekey’i bunlardan çıkarır ve aileyi onu çözen CapSkip metoduna eşler. Bir reCAPTCHA v2 kutusu, bir v3 script’i ve bir Turnstile widget’ı farklı seçeneklerle üç ayrı çağrı gerektirir; yanlış bir tahmin bir çözümü boşa harcar ve kullanamayacağınız bir sonuç döndürür. Bu rehber, capskip dışında paket kullanmayan yaklaşık 165 satırlık bir algılayıcı oluşturur. Türü, metodu ve argümanları döndürür ya da ayırt edemediğini açıkça söyler. CapSkip kendi Windows bilgisayarınızda çalışır ve çözüm başına ücret yoktur; bu yüzden yüz sayfayı denetleyen bir script ekstra hiçbir şeye mal olmaz. Aynı algılayıcının Python sürümü şurada: CAPTCHA türü algılama kılavuzu.
Neye ihtiyacınız var
- Gerekli çözücüler açık hâlde bir Windows bilgisayarında çalışan CapSkip. İlk başlatmayı kurulum kılavuzu adım adım anlatır.
- Node.js 20 veya üzeri. Aşağıdaki kod Node.js 24.22.0 üzerinde çalıştırıldı; algılayıcı ve solve adımı ayrıca capskip 1.3.0 ile 20.11.1 ve 25.2.1 üzerinde de çalıştı. Paketin kendisi Node.js 18 veya üzerini gerektirir, ancak Node.js 18 burada denenmedi.
- Bir PowerShell istemi. Komutlar PowerShell sözdizimini kullanır.
- Sahibi olduğunuz ya da otomatikleştirmenize izin verilen, üzerinde CAPTCHA bulunan bir sayfa.
- Çözücü için bir adres. Local modunda CapSkip 127.0.0.1 üzerinden yalnızca o cihaza yanıt verir. Server modunda ağ adresinizi veya genel IP’nizi dinler, böylece başka bir bilgisayardaki, bir VPS’teki veya barındırılan bir platformdaki script API üzerinden ona erişebilir. İkisi de şurada ayarlanır: bağlantı ayarları.
Nasıl test edildi: algılayıcı, Python olanla aynı 66 kayıtlı sayfada çalıştırıldı ve her sayfa için aynı sonucu verdi. Bunların 59’u elle hazırlandı (yaklaşık 40’ı sağlayıcıların belgelerini izler, gerisi benim uydurduğum uç durumlardır) ve 7’si 29 Ekim 2026’da alınmış herkese açık sayfa anlık görüntüleridir. Çözümler, uygulamanın girdi denetimlerini kopyalayan bir mock CapSkip’e karşı gerçek capskip paketi üzerinden çalıştırıldı. Gerçek uygulama çalışmıyordu, bu yüzden aşağıdaki hiçbir sayı bir çözüm süresi değildir.
CAPTCHA türünü neden çözmeden önce algılarsınız
CapSkip’in her aile için farklı bir metodu vardır: SDK’da recaptcha, turnstile, geetest, altcha, capy, captchafox, friendlyCaptcha ve görseller için normal; ayrıca HTTP API’de, SDK’nın henüz sarmalamadığı hcaptcha. reCAPTCHA içinde seçenekler de önemlidir. Invisible, Enterprise ve v3 aynı çağrıdaki bayraklardır. Yanlış olanları gönderirseniz en iyi ihtimalle iş başarısız olur, en kötü ihtimalle site nedenini göstermeden bir token reddeder. Yanlış bir reCAPTCHA sürümünde gerçek uygulamanın bu ikisinden hangisini yaptığını kontrol edemedim. Her iki durumda da çözüm harcanmış olur.
Diğer hatalar hızlı ve gürültülü biçimde ortaya çıkar. reCAPTCHA’yı açıkça (explicit) render eden bir sayfadan sitekey okumak explicit kelimesini verir ve CapSkip bunu ERROR_GOOGLEKEY ile reddeder. ERROR_GOOGLEKEY ve ERROR_PAGEURL kılavuzu bu hatayı öbür taraftan ele alır. CAPTCHA türünü güvenilir biçimde algılamak için algılayıcının sinyalleri sabit bir sırayla denetlemesi gerekir ve bu sıra iki tür hatadan da kaçınır. Algılayıcı, birden fazla siteyle karşılaşan bir scraper’dan önce yazılacak ilk şeydir. Her türün ne olduğuna dair arka plan için CAPTCHA nedir yazısı aileleri kodsuz açıklar.
1. Adım: kurun ve sayfayı çekin
Bir klasör oluşturun, bir proje başlatın ve capskip’i kurun. Algılayıcı yalnızca düzenli ifadeler (regex) kullanır ve fetch çağrıları Node.js’in beraberinde gelen fetch’ini kullanır.
npm init -y npm install capskip
Node.js’te bir CAPTCHA algılayıcısı metin üzerinde çalışır, bu yüzden yalnızca iki girdiye ihtiyaç duyar: sayfanın HTML’i ve adresi. Düz bir fetch, sunucunun JavaScript çalışmadan önce gönderdiğini gösterir. Google’ın reCAPTCHA demo sayfaları, Cloudflare’in Turnstile demosu ve hCaptcha demosu widget’larını bu HTML’de taşır. Widget’ı JavaScript ile ekleyen sayfalar taşımaz ve “HTML hiçbir şey söylemediğinde” bölümü bu durumu ele alır. Node.js’in fetch’i çerez tutmaz; bu, görsel CAPTCHA adımı için önemlidir, bu yüzden aşağıdaki script çerezleri elle iletir.
2. Adım: yardımcıları yazın
Buradan 9. Adım’a kadar her şeyi, blok blok art arda, detect.js adlı tek bir dosyaya kaydedin. İlk blok küçük okuyucuları yazar. Her biri hiçbir şey eşleşmediğinde null döndürür, bu yüzden eksik bir öznitelik hiçbir zaman hata fırlatmaz.
'use strict';
const SITEKEY_JS = /sitekey['"]?\s*:\s*['"]([^'"]+)/; // sitekey: '...' inside a render() call
const NAMED = { '&': '&', '<': '<', '>': '>', '"': '"', ''': "'" };
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);grab fonksiyonu eşleşen ilk yakalama grubunu döndürür. tag fonksiyonu bir açılış etiketini CSS sınıfına göre bulur ve adı yalnızca daha uzun bir sınıfın içinde geçen sınıfı reddeder; bu yüzden cf-turnstile-response adlı bir sınıf bir Turnstile widget’ı değildir. attr fonksiyonu her iki tırnak biçimiyle yazılmış bir özniteliği okur ve HTML varlıklarını çözer; bu, bir özniteliğin içinde tutulan challenge için önemlidir. Tırnaksız öznitelikler okunmaz.
İkinci blok bir sayfayı denetimlerin paylaşabileceği bir şeye dönüştürür. Yorumları ve style bloklarını ayıklar, böylece bir widget sınıfından söz eden bir CSS kuralı eşleşemez; script adreslerini ve satır içi JavaScript’i ayrı ayrı toplar. parse’tan sonraki iki yardımcı yanıtları oluşturur: hit ve needsBrowser gerekçesini taşıyan browser. Üçüncü bir yanıt olan unsupported, bu bloğun sonundaki denetimler tarafından oluşturulur; bunu, hCaptcha denetimiyle birlikte sonraki adım açıklar.
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);
}3. Adım: hCaptcha ve CapSkip’in çözemedikleri
Bu üç denetim yukarıdaki ikinci bloğun sonunda yer alır, bu yüzden bu adımda yapıştıracak yeni bir şey yok. Doğru yanıtın hayır olduğu sayfalarla başlayın. CapSkip FunCaptcha veya Arkose’u çözmez; GeeTest v4 de v3 için olan GeeTest metodunun dışındadır. Algılayıcı bunları atlasaydı, Arkose kutusu olan bir sayfa sonraki denetime düşer ve olmadığı bir şey olarak dönebilirdi. Bu yüzden bu denetim ilk çalışır ve metodu olmayan bir sonuç döndürür.
Aynı blok, Cloudflare’in tam sayfa denetimini, yani “Just a moment” ara sayfasını da ele alır. Bu sayfa bir Turnstile widget’ı değildir. turnstile metodu ondan data ve pagedata olmak üzere iki değer ister ve düz bir fetch bunları içermez. Algılayıcı sayfanın satır içi challenge seçeneklerini arar ve needsBrowser yanıtını verir. cf_clearance kılavuzu bir challenge sayfası geçildikten sonra olanları ele alır.
Sırada, reCAPTCHA’dan önce hCaptcha var. CapSkip, widget ister onay kutusu, ister invisible, ister Enterprise olsun hCaptcha’yı çözer; ancak reCAPTCHA’dan hCaptcha’ya geçen bir site widget’ında çoğu zaman g-recaptcha sınıfını korur ve ikisini yalnızca hCaptcha script’i ayırt eder. Denetim sitekey’i widget’tan ya da bir hcaptcha.render çağrısından okur ve widget’ta data-size invisible olarak ayarlıysa invisible ekler. Widget’a hcaptcha.setData üzerinden bir rqdata değeri veren bir Enterprise sayfası ise bunun yerine needsBrowser alır, çünkü bu değer tek bir challenge için verilir ve kısa sürede geçerliliğini yitirir.
4. Adım: reCAPTCHA, doğru sırayla
reCAPTCHA en çok dikkat ister, çünkü birkaç CapSkip varyantı iki script adını paylaşır. Onları ayrı tutan şey soruların sırasıdır. Önce script adı: enterprise.js Enterprise bayrağı demektir, api.js değildir. Sonra script adresindeki render parametresi. render= sonrasındaki bir anahtar v3 demektir, ama render= sonrasındaki explicit kelimesi widget’ını koddan render eden bir v2 sayfası demektir ve asla anahtar olarak kullanılmamalıdır. Sonra işaretleme. g-recaptcha sınıfına ve data-action özniteliğine sahip bir düğme Google’ın v3 düğme bağlamasıdır. data-action olmayan bir düğme ya da data-size değeri invisible olan herhangi bir öğe, invisible v2 widget’ıdır. Sınıfa sahip diğer her şey onay kutusudur.
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);
}Her sonuç, zaten bildiğiniz bir çağrıya eşlenir. Bir onay kutusu; sitekey, sayfa URL’si ve boş bir seçenek nesnesiyle solver.recaptcha çağrısıdır. Invisible, invisible değerini 1 yapar, Enterprise enterprise değerini 1 yapar, v3 ise version değerini v3 yapar ve action değerini ekler. Action adı data-action özniteliğinden ya da sayfanın execute çağrısından gelir; ikisi de HTML’de yoksa seçenek dışarıda bırakılır ve CapSkip varsayılan verify değerini kullanır. reCAPTCHA çözücü sayfası ele aldığı her varyantı listeler.
5. Adım: Turnstile ve GeeTest
Bir Turnstile widget’ı sayfada iki şeye ihtiyaç duyar: Cloudflare’in api.js dosyası ve bir sitekey. Anahtar, cf-turnstile sınıfına sahip bir div üzerindeki data-sitekey özniteliğinde ya da bir turnstile.render çağrısında bulunur. Denetim script’i şart koşar, bu yüzden alakasız bir sayfadaki başıboş bir sınıf adı onu tetikleyemez. Ayrıca data-action ve data-cdata varsa onları da okur ve iletir. Turnstile çözücü sayfası seçenekleri listeler.
GeeTest v3 size iki değer verir ve biri tuzaktır. gt değeri site için sabittir ve sayfada bulunabilir. challenge değeri tek kullanımlıktır ve siteler onu çalışma zamanında bir register endpoint’inden alır. Sayfa ikisini de taşıyorsa algılayıcı ikisini döndürür. Yalnızca initGeetest çağrısını taşıyorsa needsBrowser yanıtını verir, çünkü eski HTML’de bulunan bir challenge zaten geçersiz olur.
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 });
}6. Adım: ALTCHA ve Capy
ALTCHA öznitelik adlarını sürümler arasında değiştirdi. 1. ve 2. sürümler challengeurl, satır içi bir challenge için ise challengejson kullanır. 3. sürüm ve sonrası, bir URL’yi ya da challenge verisini alabilen challenge adlı tek bir öznitelik kullanır. Denetim üçünü de okur ve göreli bir URL’yi sayfa adresine ekler. Node.js’te seçenekler challengeUrl ve challengeJson olarak adlandırılır.
Capy bu denetim için bir widget div’i gerektirmez. Anahtarı PUZZLE_ ile başlar ve widget script adresinin k parametresinde bulunur; varsayılan dışındaki bir host apiServer olarak iletilir.
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 });
}7. Adım: CaptchaFox ve Friendly Captcha
CaptchaFox iki derlemeyle gelir. Düz olan cdn.captchafox.com adresinden yüklenir. Diğeri, bir barındırma platformu tarafından ortak bir CDN yolunun altında paketlenir; bu yüzden algılayıcı yalnızca yola değil, bu yoldaki CaptchaFox paket adına bakar, çünkü yolu başka widget’lar da kullanır.
Friendly Captcha en sinsi olanıdır. 1. ve 2. sürüm aynı div’i ve aynı sitekey ad alanını kullanır ama protokolleri farklıdır; yanlış olanın token’ı site tarafından reddedilebilir. Script’ler ikisini ayırır: 2. sürüm @friendlycaptcha/sdk paketini, 1. sürüm friendly-challenge yükler. Sayfa hiçbir işaret vermediğinde algılayıcı tahmin yürütmeyi reddeder ve needsBrowser der.
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 }) });
}Bunların her birinin, metodun aldığı seçenekleri içeren kendi çözücü sayfası vardır:
- Capy Puzzle çözücü sayfası: bir Capy yanıtının döndürdüğü üç değer.
- CaptchaFox çözücü sayfası: CaptchaFox metodunun aldığı seçenekler.
- Friendly Captcha çözücü sayfası: Friendly Captcha metodu.
8. Adım: görsel CAPTCHA, bir tahmin ve denetimler
Görsel bir CAPTCHA’nın bulunacak bir sağlayıcı script’i yoktur, bu yüzden bu denetim bir tahmindir ve yalnızca başka hiçbir şey eşleşmediğinde çalışır. Denetim, işaretlemesinde captcha geçen bir görsel ve sayfanın başka bir yerinde işaretlemesinde captcha ya da security code geçen bir metin kutusu arar. İşaretlemenin bu sözcükleri kullanmadığı her durumda kaçırdı ve çizilmiş bir canvas ona görünmezdir. Bir eşleşmeyi kesin değil, olası sayın.
Aynı blok denetimleri sırayla listeler ve 9. Adım’ın sonunda dışa aktarılan detect fonksiyonunu tanımlar. Sıra mantığın kendisidir: önce desteklenmeyen sağlayıcılar, sonra Cloudflare sayfası, sonra hCaptcha, sonra diğer her aile ve görsel tahmini yalnızca liste hiçbir şey bulmadığında. detect fonksiyonu ilk sonucu döndürür ve sayfa birden fazla aile taşıdığında, örneğin bir v3 script’i artı bir Turnstile kutusu, bir also listesi ekler. Liste aileleri adlandırır, başka anahtarlı ikinci bir reCAPTCHA widget’ını değil.
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;
}9. Adım: çözün
Son blok sonucu doğru CapSkip metoduna yönlendirir. unsupported olan ya da needsBrowser işaretli her şeyi bir Error ile reddeder. CapSkip metotları ana değerlerini konuma göre, gerisini bir seçenek nesnesi olarak alır; bu yüzden küçük bir tablo hangi değerin nereye gittiğini söyler. Node.js seçenekleri camelCase’dir, tek bir istisnayla: çağrı başına bir yoklama aralığı eklerseniz adı polling_interval olur, çünkü çağrı başına pollingInterval bir ValidationException fırlatır. Görsel metodu çağrı başına hiçbir seçenek almaz, bu yüzden onun için pollingInterval değerini CapSkip constructor’ında ayarlayın. Bir görsel, ilettiğiniz çerezlerle indirilir, çünkü fetch’in çerez deposu yoktur ve SDK’nın bir URL’yi kendi indirmesi hiçbir çerez göndermez. Bu çerezler görselin bulunduğu hangi host ise oraya gider, bu yüzden CAPTCHA görseliniz farklı bir hosttaysa çerezi dışarıda bırakın.
SDK’da henüz hCaptcha çağrısı yok, bu yüzden bir hCaptcha sonucu bunun yerine solveHcaptcha’ya gider. Bu fonksiyon sitekey’i ve sayfa adresini method=hcaptcha ile in.php’ye gönderir, ardından token hazır olana kadar res.php’yi 5 saniyede bir yoklar. Bu akışı API belgelerinin hCaptcha bölümü anlatır. Çözücünün adresini solve’un son argümanında api olarak, API key doğrulaması açıksa API key’inizi de key olarak verin. Sonuç token’ı code içinde, token’ın alındığı User-Agent’ı ise userAgent içinde taşır.
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 };Node.js CAPTCHA algılayıcısını çalıştırın
Bunu detect.js dosyasının yanına use.js olarak kaydedin. Sayfanızı çeker, algılayıcının bulduğunu yazdırır ve CapSkip’i yalnızca çağrılacak bir metot varsa ve hiçbir şey eksik değilse çağırır. Tarayıcı benzeri bir User-Agent gönderir, çünkü birçok site Node.js’in varsayılanını reddeder. Sayfa 200 yanıtı vermediğinde HTTP durumunu yazdırır ve devam eder, çünkü bir Cloudflare challenge sayfası 403 ya da 503 olarak gelir. Sayfa adresini tırnak içinde verin, çünkü ve işareti içeren bir adres PowerShell’de ayrıştırma hatasıdır. 8080 portu varsayılandır; sizinki farklıysa, bu script’in kendisinin okuduğu CAPSKIP_PORT ortam değişkenini ayarlayın. Bu script’te host 127.0.0.1 olarak kalır, bu yüzden Server modu için host satırını düzenleyin.
$env:CAPSKIP_PORT = "9090" # only if your port is not 8080 node use.js "https://example.com/login"
Dikkat: fetch çerez tutmaz ve bu script Set-Cookie değerini yalnızca son yanıttan okur, bu yüzden bir yönlendirmede ayarlanan çerez kaybolur. Onu korumak için fetch’i redirect seçeneği manual olarak çağırın, Set-Cookie değerini yönlendirme yanıtından okuyun ve Location adresini kendiniz isteyin. Script’in kendisi aşağıdadır.
'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);
});reCAPTCHA v2 onay kutusu olan bir sayfada ilk çıktı türü, metodu ve argümanları gösterir, ikincisi ise sonucu gösterir. reCAPTCHA için token code alanındadır; diğer metotlar farklı alanlar kullanır, örneğin Turnstile ve hCaptcha token’larının yanında userAgent. Bir reCAPTCHA token’ı formun g-recaptcha-response alanına, bir hCaptcha token’ı ise h-captcha-response alanına gider. Mock çalıştırmasında algılayıcı şu sonuçları döndürdü:
| Sayfa | detect şunu döndürür |
|---|---|
| reCAPTCHA v2 checkbox | type recaptcha_v2, method recaptcha, sitekey ve url |
| hCaptcha widget’ı | type hcaptcha, method hcaptcha, sitekey ve url |
| Cloudflare “Just a moment” sayfası | type turnstile_challenge, needsBrowser ayarlı |
| v3 script’i artı bir Turnstile widget’ı | type recaptcha_v3, also listesinde turnstile var |
HTML hiçbir şey söylemediğinde: tarayıcı konsolu
Bazı sayfalar widget’larını yüklendikten sonra JavaScript ile ekler. O zaman düz bir fetch hiç CAPTCHA bulamaz ve algılayıcı, CAPTCHA’sı olan bir sayfa için null döndürür. İki durum bilinir ve beklenir: kodla eklenen bir Google ya da Cloudflare script’i ve kodla ayarlanan bir sitekey. Bunlar için sayfayı Chrome’da açın, F12’ye basın ve bunu Console’a yapıştırın. Chrome yapıştırmayı engellerse önce allow pasting yazıp Enter’a basın. Sitekey’leri sayfanın zaten yüklediği kaynaklardan okur, yani tarayıcının oluşturduğunu görür.
(() => {
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)';
})();Bir reCAPTCHA v2 sayfasında, bir invisible sayfada, bir v3 sayfasında, bir Turnstile sayfasında ve hCaptcha demosunda sitekey’i ve bayrakları buldu, CAPTCHA olmayan bir sayfada ise bir mesaj yazdırdı. Üç sınır var: tarayıcı varsayılan olarak yalnızca yaklaşık 250 kaynak girdisi tutar, bu yüzden uzun süre açık kalan tek sayfalı bir uygulama erken olanları kaybedebilir ve kod parçası Friendly Captcha 1. sürümü ile 2. sürümü ayırt edemez, GeeTest, Capy veya CaptchaFox script’lerini de aramaz. Cloudflare’in tam sayfa denetiminde sitekey’i bir kaynak adresinden gösterebilir ama render çağrısına bir hook gerektiren data ve pagedata değerlerini gösteremez. Bu hook burada test edilmedi.
Algılamanın başarısız olduğu yerler
| Durum | detect ne döndürür | Ne yapmalı |
|---|---|---|
| JavaScript ile eklenen widget script’i | null | Sayfayı bir tarayıcıda yükleyin ve konsol kod parçasını çalıştırın |
| JavaScript ile ayarlanan sitekey | needsBrowser | Anahtarı canlı sayfadan okuyun |
| Cloudflare tam sayfa denetimi | needsBrowser | Değerleri bir tarayıcıda okuyun, cf_clearance kılavuzuna bakın. Sayfa 403 veya 503 olarak gelir; use.js bunu bildirir ve yine de denetler |
| HTML’de widget olmadan CaptchaFox, Friendly Captcha veya Capy script’i | null | Konsol kod parçasını çalıştırın ya da sayfa kaynağını okuyun |
| Widget olmayan bir engel sayfası (403, 503) | null | use.js önce durumu yazdırır: bir engel sayfasında bulunacak CAPTCHA yoktur |
| Arkose, GeeTest v4 | unsupported | CapSkip bunları çözemez |
| rqdata ayarlayan hCaptcha Enterprise | needsBrowser | Her görev için canlı sayfadan taze bir rqdata okuyun ve bunu solveHcaptcha’ya data olarak verin |
| Script ipucu olmayan Friendly Captcha | needsBrowser | Sayfanın hangi script’i yüklediğini kontrol edin |
| captcha sözcüğünü içermeyen görsel CAPTCHA | null | O site için kendi kuralınızı ekleyin |
Yaygın hatalar
| Hata | Neden | Düzeltme |
|---|---|---|
| ERROR_GOOGLEKEY | Anahtar explicit kelimesiydi ya da bir reCAPTCHA anahtarı olmayan bir anahtardı | Anahtarı render parametresinden değil, widget’tan okuyun |
| ERROR_CAPTCHA_UNSOLVABLE | Başarısız bir iş böyle görünür ve yanıt nedeni belirtmez. Yanlış bir reCAPTCHA sürümü burada bitebilir ama bunu gerçek uygulamada doğrulamadım | Önce algılanan türü kontrol edin, sonra şuna bakın: API belgeleri |
| ValidationException | Metodun almadığı bir seçenek, örneğin bir v2 çağrısında action, çağrı başına pollingInterval ya da görsel metodunda çağrı başına herhangi bir seçenek | Algılayıcının döndürdüğü argümanları kullanın |
| NetworkException | CapSkip çalışmıyor ya da host veya port yanlış | CapSkip’i başlatın ve bağlantı ayarlarını kontrol edin |
| solve kaynaklı Error | Sonuç unsupported ya da bir tarayıcı gerektiriyor, veya CapSkip bir hCaptcha işini reddetti ve mesaj onun hata kodu | solve çağırmadan önce ilk iki durumu ele alın; üçüncüsü için sitekey’i ve sayfa adresini kontrol edin |
Çözücüyü bir sunucuda çalıştırmak
Algılayıcının CapSkip’ten yalnızca iki şeye ihtiyacı vardır: bir host ve bir port. Local modunda bu 127.0.0.1 ve kendi bilgisayarınızdır. Server modunda CapSkip ağ adresinizi ya da genel bir IP’yi dinler, böylece başka bir bilgisayarda, bir VPS’te ya da barındırılan bir platformda çalışan bir algılayıcı aynı API üzerinden onu çağırabilir. use.js içindeki host ve portu değiştirin, koddaki başka hiçbir şey değişmez. Kararlı uzaktan erişim için statik bir genel IP önerilir. Server modu yine kendi donanımınızdır ve yine kullanım başına ücretlendirilmez. Server modunun portu her adrese açtığını unutmayın; bu yüzden ulaşmaması gereken adresler için bir Windows Firewall engelleme kuralı ekleyin. Modları nasıl değiştireceğiniz için bağlantı ayarları bölümüne bakın.
FAQ
Kod olmadan bir sitenin hangi CAPTCHA’yı kullandığını söyleyebilir miyim?
Evet. DevTools’u açın, Network sekmesine gidin, sayfayı yeniden yükleyin ve script adreslerine bakın. google.com/recaptcha ya da recaptcha.net script’i reCAPTCHA, challenges.cloudflare.com Turnstile, js.hcaptcha.com ise hCaptcha’dır. Yukarıdaki Console kod parçası aynı aramayı sizin için yapar.
detect neden tahmin yerine needsBrowser döndürür?
Çünkü yanlış bir tahmin tam bir çözümü boşa harcar ve site nedenini göstermeden reddeden bir token döndürür. Statik HTML sitekey’i, bir GeeTest challenge’ını ya da bir Friendly Captcha sürümünü içermediğinde bunu söylemek, başarısız bir işten daha hızlıdır.
CapSkip hCaptcha çözer mi?
Evet. CapSkip, hCaptcha onay kutusu, invisible ve Enterprise widget’larını HTTP API’si üzerinden method=hcaptcha ile çözer. Ayrıntıları API belgeleri anlatır. Node.js SDK’sında henüz hCaptcha çağrısı yok, bu yüzden yukarıdaki solveHcaptcha işi in.php’ye kendisi gönderir. FunCaptcha, Arkose ve GeeTest v4 hâlâ desteklenmez; algılayıcı bunları unsupported olarak bildirir, böylece bir script sayfada başarısız olmak yerine onu atlayabilir.
Barındırılan bir platform algılayıcıyı benim CapSkip’imle kullanabilir mi?
Evet, Server modu ile. CapSkip’i ağ ya da genel IP’nizi dinleyecek şekilde ayarlayın, use.js içindeki host ve portu o adrese ayarlayın; algılayıcı ve solve çağrısı tek bir bilgisayardaki gibi çalışır. Çözücü sizin donanımınızda kalır.
Kısa özet
Node.js’te bir CAPTCHA algılayıcısı ilk çalışarak yerini hak eder: önce CAPTCHA türünü algılayın, sonra çözün. Script’i ve işaretlemeyi sabit bir sırayla okuyun, argümanları doğru metoda verin ve null, needsBrowser ile unsupported değerlerini hata değil, yanıt olarak ele alın. CapSkip kendi makinenizde olduğunda bir sayfayı denetlerken işleyen bir sayaç yoktur; bu yüzden yerel captcha çözücü yeni bir siteyle birkaç dakikada bir karşılaşan bir tarama için pratiktir. Node.js SDK’nın kendisi için şuraya bakın: Node.js CAPTCHA çözücü sayfasına.
