|
10 | 10 | } |
11 | 11 | return elements; |
12 | 12 | } |
| 13 | + const CAPTCHA_TEXT_KEYWORDS = [ "verify you are human", "captcha", "human verification", "unusual traffic", "are you a robot", "security check", "prove you are human", "bot detection", "automated access" ], CAPTCHA_URL_HINTS = [ "captcha", "challenge", "verify" ], CAPTCHA_IFRAME_HINTS = { |
| 14 | + recaptcha: [ "recaptcha", "google.com/recaptcha" ], |
| 15 | + hcaptcha: [ "hcaptcha.com" ], |
| 16 | + turnstile: [ "challenges.cloudflare.com", "turnstile" ], |
| 17 | + arkose: [ "arkoselabs.com", "funcaptcha.com", "client-api.arkoselabs.com" ], |
| 18 | + awswaf: [ "amazonaws.com/captcha", "awswaf.com" ] |
| 19 | + }, CAPTCHA_SCRIPT_HINTS = { |
| 20 | + recaptcha: [ "recaptcha" ], |
| 21 | + hcaptcha: [ "hcaptcha" ], |
| 22 | + turnstile: [ "turnstile", "challenges.cloudflare.com" ], |
| 23 | + arkose: [ "arkoselabs", "funcaptcha" ], |
| 24 | + awswaf: [ "captcha.awswaf", "awswaf-captcha" ] |
| 25 | + }, CAPTCHA_CONTAINER_SELECTORS = [ { |
| 26 | + selector: ".g-recaptcha", |
| 27 | + provider: "recaptcha" |
| 28 | + }, { |
| 29 | + selector: "#g-recaptcha", |
| 30 | + provider: "recaptcha" |
| 31 | + }, { |
| 32 | + selector: "[data-sitekey]", |
| 33 | + provider: "unknown" |
| 34 | + }, { |
| 35 | + selector: 'iframe[title*="recaptcha" i]', |
| 36 | + provider: "recaptcha" |
| 37 | + }, { |
| 38 | + selector: ".h-captcha", |
| 39 | + provider: "hcaptcha" |
| 40 | + }, { |
| 41 | + selector: "#h-captcha", |
| 42 | + provider: "hcaptcha" |
| 43 | + }, { |
| 44 | + selector: 'iframe[title*="hcaptcha" i]', |
| 45 | + provider: "hcaptcha" |
| 46 | + }, { |
| 47 | + selector: ".cf-turnstile", |
| 48 | + provider: "turnstile" |
| 49 | + }, { |
| 50 | + selector: "[data-cf-turnstile-sitekey]", |
| 51 | + provider: "turnstile" |
| 52 | + }, { |
| 53 | + selector: 'iframe[src*="challenges.cloudflare.com"]', |
| 54 | + provider: "turnstile" |
| 55 | + }, { |
| 56 | + selector: "#FunCaptcha", |
| 57 | + provider: "arkose" |
| 58 | + }, { |
| 59 | + selector: ".funcaptcha", |
| 60 | + provider: "arkose" |
| 61 | + }, { |
| 62 | + selector: "[data-arkose-public-key]", |
| 63 | + provider: "arkose" |
| 64 | + }, { |
| 65 | + selector: 'iframe[src*="arkoselabs"]', |
| 66 | + provider: "arkose" |
| 67 | + }, { |
| 68 | + selector: "#captcha-container", |
| 69 | + provider: "awswaf" |
| 70 | + }, { |
| 71 | + selector: "[data-awswaf-captcha]", |
| 72 | + provider: "awswaf" |
| 73 | + }, { |
| 74 | + selector: 'iframe[title*="captcha" i]', |
| 75 | + provider: "unknown" |
| 76 | + } ]; |
| 77 | + function addEvidence(list, value) { |
| 78 | + value && (list.length >= 5 || list.push(value)); |
| 79 | + } |
| 80 | + function truncateText(text, maxLen) { |
| 81 | + return text ? text.length <= maxLen ? text : text.slice(0, maxLen) : ""; |
| 82 | + } |
| 83 | + function matchHints(value, hints) { |
| 84 | + const lower = String(value || "").toLowerCase(); |
| 85 | + return !!lower && hints.some(hint => lower.includes(hint)); |
| 86 | + } |
| 87 | + function detectCaptcha() { |
| 88 | + const evidence = { |
| 89 | + text_hits: [], |
| 90 | + selector_hits: [], |
| 91 | + iframe_src_hits: [], |
| 92 | + url_hits: [] |
| 93 | + }; |
| 94 | + let hasIframeHit = !1, hasContainerHit = !1, hasScriptHit = !1, hasKeywordHit = !1, hasUrlHit = !1; |
| 95 | + const providerSignals = { |
| 96 | + recaptcha: 0, |
| 97 | + hcaptcha: 0, |
| 98 | + turnstile: 0, |
| 99 | + arkose: 0, |
| 100 | + awswaf: 0 |
| 101 | + }; |
| 102 | + try { |
| 103 | + const iframes = document.querySelectorAll("iframe"); |
| 104 | + for (const iframe of iframes) { |
| 105 | + const src = iframe.getAttribute("src") || "", title = iframe.getAttribute("title") || ""; |
| 106 | + if (src) for (const [provider, hints] of Object.entries(CAPTCHA_IFRAME_HINTS)) matchHints(src, hints) && (hasIframeHit = !0, |
| 107 | + providerSignals[provider] += 1, addEvidence(evidence.iframe_src_hits, truncateText(src, 120))); |
| 108 | + if (title && matchHints(title, [ "captcha", "recaptcha" ]) && (hasContainerHit = !0, |
| 109 | + addEvidence(evidence.selector_hits, 'iframe[title*="captcha"]')), evidence.iframe_src_hits.length >= 5) break; |
| 110 | + } |
| 111 | + } catch (e) {} |
| 112 | + try { |
| 113 | + const scripts = document.querySelectorAll("script[src]"); |
| 114 | + for (const script of scripts) { |
| 115 | + const src = script.getAttribute("src") || ""; |
| 116 | + if (src) { |
| 117 | + for (const [provider, hints] of Object.entries(CAPTCHA_SCRIPT_HINTS)) matchHints(src, hints) && (hasScriptHit = !0, |
| 118 | + providerSignals[provider] += 1, addEvidence(evidence.selector_hits, `script[src*="${hints[0]}"]`)); |
| 119 | + if (evidence.selector_hits.length >= 5) break; |
| 120 | + } |
| 121 | + } |
| 122 | + } catch (e) {} |
| 123 | + for (const {selector: selector, provider: provider} of CAPTCHA_CONTAINER_SELECTORS) try { |
| 124 | + document.querySelector(selector) && (hasContainerHit = !0, addEvidence(evidence.selector_hits, selector), |
| 125 | + "unknown" !== provider && (providerSignals[provider] += 1)); |
| 126 | + } catch (e) {} |
| 127 | + const textSnippet = function() { |
| 128 | + try { |
| 129 | + const candidates = document.querySelectorAll("h1, h2, h3, h4, p, label, button, form, div, span"); |
| 130 | + let combined = "", count = 0; |
| 131 | + for (const node of candidates) { |
| 132 | + if (count >= 30 || combined.length >= 2e3) break; |
| 133 | + if (!node || "string" != typeof node.innerText) continue; |
| 134 | + if (!node.offsetWidth && !node.offsetHeight && !node.getClientRects().length) continue; |
| 135 | + const text = node.innerText.replace(/\s+/g, " ").trim(); |
| 136 | + text && (combined += `${text} `, count += 1); |
| 137 | + } |
| 138 | + if (combined = combined.trim(), combined) return truncateText(combined, 2e3); |
| 139 | + } catch (e) {} |
| 140 | + try { |
| 141 | + let bodyText = document.body?.innerText || ""; |
| 142 | + return !bodyText && document.body?.textContent && (bodyText = document.body.textContent), |
| 143 | + truncateText(bodyText.replace(/\s+/g, " ").trim(), 2e3); |
| 144 | + } catch (e) { |
| 145 | + return ""; |
| 146 | + } |
| 147 | + }(); |
| 148 | + if (textSnippet) { |
| 149 | + const lowerText = textSnippet.toLowerCase(); |
| 150 | + for (const keyword of CAPTCHA_TEXT_KEYWORDS) lowerText.includes(keyword) && (hasKeywordHit = !0, |
| 151 | + addEvidence(evidence.text_hits, keyword)); |
| 152 | + } |
| 153 | + try { |
| 154 | + const lowerUrl = (window.location?.href || "").toLowerCase(); |
| 155 | + for (const hint of CAPTCHA_URL_HINTS) lowerUrl.includes(hint) && (hasUrlHit = !0, |
| 156 | + addEvidence(evidence.url_hits, hint)); |
| 157 | + } catch (e) {} |
| 158 | + let confidence = 0; |
| 159 | + hasIframeHit && (confidence += .7), hasContainerHit && (confidence += .5), hasScriptHit && (confidence += .5), |
| 160 | + hasKeywordHit && (confidence += .3), hasUrlHit && (confidence += .2), confidence = Math.min(1, confidence), |
| 161 | + hasIframeHit && (confidence = Math.max(confidence, .8)), !hasKeywordHit || hasIframeHit || hasContainerHit || hasScriptHit || hasUrlHit || (confidence = Math.min(confidence, .4)); |
| 162 | + const detected = confidence >= .7; |
| 163 | + let providerHint = null; |
| 164 | + return providerSignals.recaptcha > 0 ? providerHint = "recaptcha" : providerSignals.hcaptcha > 0 ? providerHint = "hcaptcha" : providerSignals.turnstile > 0 ? providerHint = "turnstile" : providerSignals.arkose > 0 ? providerHint = "arkose" : providerSignals.awswaf > 0 ? providerHint = "awswaf" : detected && (providerHint = "unknown"), |
| 165 | + { |
| 166 | + detected: detected, |
| 167 | + provider_hint: providerHint, |
| 168 | + confidence: confidence, |
| 169 | + evidence: evidence |
| 170 | + }; |
| 171 | + } |
13 | 172 | const DEFAULT_INFERENCE_CONFIG = { |
14 | 173 | allowedTags: [ "label", "span", "div" ], |
15 | 174 | allowedRoles: [], |
|
691 | 850 | ready_state: document.readyState || null, |
692 | 851 | quiet_ms: quietMs, |
693 | 852 | node_count: nodeCount |
694 | | - } |
| 853 | + }, |
| 854 | + captcha: detectCaptcha() |
695 | 855 | }; |
696 | 856 | } catch (e) {} |
697 | 857 | return { |
|
0 commit comments