763e06c127
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
3.4 KiB
Go
77 lines
3.4 KiB
Go
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;i<sigs.length;i++){ if(!seen[sigs[i]]){seen[sigs[i]]=1;uniq.push(sigs[i]);} }
|
|
return JSON.stringify({detected: uniq.length>0, 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
|
|
}
|