mirror of
https://github.com/lukaszraczylo/mcp-captcharesolver.git
synced 2026-10-16 01:07:54 +00:00
feat(captcha): deterministic HTML detection for known providers
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
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 ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package captcha
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestDetectHTML(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
html string
|
||||||
|
wantType string
|
||||||
|
wantSolvable bool
|
||||||
|
}{
|
||||||
|
{"recaptcha v2", `<div class="g-recaptcha" data-sitekey="6Lc_abc"></div>`, "recaptcha_v2", true},
|
||||||
|
{"recaptcha v3", `<script src="https://www.google.com/recaptcha/api.js?render=6Lc_v3key"></script>`, "recaptcha_v3", false},
|
||||||
|
{"hcaptcha", `<div class="h-captcha" data-sitekey="hk_123"></div>`, "hcaptcha", true},
|
||||||
|
{"turnstile", `<div class="cf-turnstile" data-sitekey="0x4AAA"></div>`, "turnstile", false},
|
||||||
|
{"funcaptcha", `<div id="arkose" data-pkey="ABC"></div>`, "funcaptcha", true},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := DetectHTML(tc.html)
|
||||||
|
if len(got) == 0 {
|
||||||
|
t.Fatalf("no detection")
|
||||||
|
}
|
||||||
|
if got[0].Type != tc.wantType || got[0].LLMSolvable != tc.wantSolvable {
|
||||||
|
t.Fatalf("got %+v", got[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Package captcha provides deterministic detection and LLM-backed solving of
|
||||||
|
// captcha challenges. It never drives a browser and never mints tokens.
|
||||||
|
package captcha
|
||||||
|
|
||||||
|
// Detection describes one captcha found on a page. Fields are ordered
|
||||||
|
// largest-alignment-first for fieldalignment.
|
||||||
|
type Detection struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Sitekey string `json:"sitekey,omitempty"`
|
||||||
|
IframeURL string `json:"iframe_url,omitempty"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
|
LLMSolvable bool `json:"llm_solvable"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user