Files
mcp-captcharesolver/internal/captcha/detect.go
T

57 lines
2.1 KiB
Go

package captcha
import (
"regexp"
"strings"
)
var (
reRecaptchaV3 = regexp.MustCompile(`recaptcha/api\.js\?render=([\w-]+)`)
reSitekey = regexp.MustCompile(`data-sitekey="([^"]+)"`)
rePkey = regexp.MustCompile(`data-pkey="([^"]+)"`)
)
// DetectHTML scans raw page HTML and returns deterministic detections for known
// captcha providers. v3/Turnstile are reported with LLMSolvable=false.
func DetectHTML(html string) []Detection {
var out []Detection
// reCAPTCHA v3 first (script render param). v3 has no visual challenge.
if m := reRecaptchaV3.FindStringSubmatch(html); m != nil {
out = append(out, Detection{
Type: "recaptcha_v3", Sitekey: m[1], LLMSolvable: false,
Notes: "score/behavioral — not LLM-solvable; needs browser reputation",
})
}
if strings.Contains(html, "g-recaptcha") || strings.Contains(html, "google.com/recaptcha/api2") {
out = append(out, Detection{Type: "recaptcha_v2", Sitekey: firstSitekey(html), LLMSolvable: true,
Notes: "image-grid; LLM returns tiles, caller clicks + harvests token"})
}
if strings.Contains(html, "h-captcha") || strings.Contains(html, "hcaptcha.com") {
out = append(out, Detection{Type: "hcaptcha", Sitekey: firstSitekey(html), LLMSolvable: true,
Notes: "image-grid; LLM returns tiles, caller clicks + harvests token"})
}
if strings.Contains(html, "cf-turnstile") || strings.Contains(html, "challenges.cloudflare.com") {
out = append(out, Detection{Type: "turnstile", Sitekey: firstSitekey(html), LLMSolvable: false,
Notes: "proof-of-work/behavioral — not LLM-solvable"})
}
if strings.Contains(html, "arkoselabs") || strings.Contains(html, "funcaptcha") || strings.Contains(html, "arkose") {
out = append(out, Detection{Type: "funcaptcha", Sitekey: firstPkey(html), LLMSolvable: true,
Notes: "rotation/visual — best-effort"})
}
return out
}
func firstSitekey(html string) string {
if m := reSitekey.FindStringSubmatch(html); m != nil {
return m[1]
}
return ""
}
func firstPkey(html string) string {
if m := rePkey.FindStringSubmatch(html); m != nil {
return m[1]
}
return ""
}