package browser import ( "encoding/json" "fmt" ) // detectCaptchaJS es la unica evaluacion que DetectCaptcha corre en el top frame. // Detecta reCAPTCHA, hCaptcha y Cloudflare Turnstile por la presencia de sus // iframes/widgets (los iframe[src] son legibles desde el top aunque su contenido // sea cross-origin) y el JS-challenge de Cloudflare por texto en innerText. // Siempre retorna un JSON serializable; en caso de excepcion devuelve detected=false // con un campo "error" para que el caller no rompa (best-effort). const detectCaptchaJS = `(function(){ try { var sigs = []; var q = function(s){ return document.querySelector(s); }; if (q('iframe[src*="recaptcha/api2"], iframe[src*="recaptcha/enterprise"], .g-recaptcha, #recaptcha')) sigs.push('recaptcha'); if (q('iframe[src*="hcaptcha.com"], .h-captcha')) sigs.push('hcaptcha'); if (q('iframe[src*="challenges.cloudflare.com"], .cf-turnstile')) sigs.push('turnstile'); var t = ((document.body && document.body.innerText) || '').toLowerCase().slice(0, 4000); if (/checking your browser|verify(ing)? you are human|i'?m not a robot|are you a robot|unusual traffic|complete the security check|press and hold/.test(t)) sigs.push('challenge'); var seen = {}, uniq = []; for (var i=0;i0, types: uniq, url: location.href}); } catch(e){ return JSON.stringify({detected:false, types:[], url: (location&&location.href)||'', error:String(e)}); } })()` // captchaResult es el shape del JSON que produce detectCaptchaJS. type captchaResult struct { Detected bool `json:"detected"` Types []string `json:"types"` URL string `json:"url"` Error string `json:"error"` } // parseCaptchaSignals parsea el JSON que produce detectCaptchaJS. Es puro y // testeable sin navegador. Si el JSON trae un campo "error" (excepcion JS en la // pagina) se trata como detected=false best-effort, no como fallo. types es // siempre un slice no nulo (vacio si no hay senales). Solo retorna error si el // JSON es invalido / no parseable. func parseCaptchaSignals(raw string) (detected bool, types []string, url string, err error) { var r captchaResult if err := json.Unmarshal([]byte(raw), &r); err != nil { return false, nil, "", fmt.Errorf("parse captcha signals: json invalido: %w", err) } if r.Types == nil { r.Types = []string{} } return r.Detected, r.Types, r.URL, nil } // DetectCaptcha detecta si la pagina actual presenta un captcha o challenge // anti-bot. Corre UNA evaluacion JS en el top frame y parsea el resultado. // NO resuelve ni notifica nada — solo detecta. Una responsabilidad. // // Retorna detected=true si hay al menos una senal, junto con los tipos // detectados (subconjunto de: "recaptcha", "hcaptcha", "turnstile", // "challenge") y la URL del top frame. Best-effort: una excepcion JS en la // pagina se trata como "no detectado" sin romper. func DetectCaptcha(c *CDPConn) (detected bool, types []string, url string, err error) { if c == nil { return false, nil, "", fmt.Errorf("detect captcha: conexion nula") } raw, err := CdpEvaluate(c, detectCaptchaJS) if err != nil { return false, nil, "", fmt.Errorf("detect captcha: %w", err) } detected, types, url, err = parseCaptchaSignals(raw) if err != nil { return false, nil, "", fmt.Errorf("detect captcha: %w", err) } return detected, types, url, nil }