mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
fix(captcha): устойчивый разбор debug_info и сигнал дрейфа версии скрипта
This commit is contained in:
@@ -36,7 +36,7 @@ func SetLogger(l logx.Logger) { Log = logx.OrNop(l) }
|
||||
|
||||
const (
|
||||
captchaAPIVersion = "5.131"
|
||||
captchaScriptVersion = "1.1.1370"
|
||||
captchaScriptVersion = "1.1.1374"
|
||||
captchaAPIOrigin = "https://id.vk.ru"
|
||||
captchaDomain = "vk.ru"
|
||||
captchaDeviceInfo = `{"screenWidth":1920,"screenHeight":1080,"screenAvailWidth":1920,"screenAvailHeight":1032,"innerWidth":1147,"innerHeight":945,"devicePixelRatio":1,"language":"ru-RU","languages":["ru-RU"],"webdriver":false,"hardwareConcurrency":8,"deviceMemory":16,"connectionEffectiveType":"4g","notificationsPermission":"denied"}`
|
||||
@@ -49,6 +49,7 @@ var (
|
||||
reCaptchaWindowInit = regexp.MustCompile(`(?s)window\.init\s*=\s*(\{.*?})\s*;`)
|
||||
reCaptchaScriptSrc = regexp.MustCompile(`src="(https://[^"]+not_robot_captcha[^"]+)"`)
|
||||
reCaptchaDebugInfo = regexp.MustCompile(`debug_info:(?:[^"]*\|\|)?"([a-fA-F0-9]{64})"`)
|
||||
reCaptchaHex64 = regexp.MustCompile(`"([a-fA-F0-9]{64})"`)
|
||||
reCaptchaVersion = regexp.MustCompile(`vkid/([0-9.]*)/not_robot_captcha\.js`)
|
||||
|
||||
errCaptchaRateLimit = errors.New("captcha session rate limit reached")
|
||||
@@ -57,6 +58,7 @@ var (
|
||||
captchaMaxAttempts = 2
|
||||
|
||||
captchaDebugCache sync.Map // scriptURL -> string
|
||||
captchaVersionSeen sync.Map // script version -> struct{} (drift warned once)
|
||||
captchaHeaderOrder = []string{
|
||||
"host",
|
||||
"content-length",
|
||||
@@ -224,11 +226,7 @@ func (s *captchaSession) solveOnce(captchaErr *Error) (string, error) {
|
||||
}
|
||||
s.logger().Debugf("[Captcha] browser_fp source=%s len=%d", browserFPSource, len(browserFP))
|
||||
|
||||
if m := reCaptchaVersion.FindStringSubmatch(page.ScriptURL); len(m) > 1 {
|
||||
if m[1] != captchaScriptVersion {
|
||||
s.logger().Debugf("[Captcha] script version drift: known=%s latest=%s", captchaScriptVersion, m[1])
|
||||
}
|
||||
}
|
||||
s.warnVersionDrift(page.ScriptURL)
|
||||
|
||||
debugInfo, err := s.fetchDebugInfo(page.ScriptURL)
|
||||
if err != nil {
|
||||
@@ -331,16 +329,54 @@ func (s *captchaSession) fetchDebugInfo(scriptURL string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m := reCaptchaDebugInfo.FindSubmatch(body)
|
||||
if len(m) < 2 {
|
||||
return "", errors.New("debug_info match not found")
|
||||
v, usedFallback, err := extractDebugInfo(body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if usedFallback {
|
||||
s.logger().Warnf("[Captcha] debug_info primary pattern missed; used windowed fallback (script format drift)")
|
||||
}
|
||||
v := string(m[1])
|
||||
captchaDebugCache.Store(scriptURL, v)
|
||||
s.logger().Debugf("[Captcha] debug_info fetched url=%s", scriptURL)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// warnVersionDrift предупреждает один раз на версию, если VK отдал скрипт новее
|
||||
// оттестированного baseline: wire мог измениться, надо перепроверить против live.
|
||||
func (s *captchaSession) warnVersionDrift(scriptURL string) {
|
||||
m := reCaptchaVersion.FindStringSubmatch(scriptURL)
|
||||
if len(m) < 2 || m[1] == "" || m[1] == captchaScriptVersion {
|
||||
return
|
||||
}
|
||||
if _, seen := captchaVersionSeen.LoadOrStore(m[1], struct{}{}); seen {
|
||||
return
|
||||
}
|
||||
s.logger().Warnf("[Captcha] script version %s differs from tested %s; wire unverified - re-check if BOT-rejections rise", m[1], captchaScriptVersion)
|
||||
}
|
||||
|
||||
// extractDebugInfo достаёт fallback-константу debug_info из тела скрипта.
|
||||
// VK-формат: `debug_info:(...window.vk.X)||"<64hex>"`; имя window.vk.X плавает
|
||||
// от билда к билду, поэтому основной паттерн ловит только hash-литерал.
|
||||
// usedFallback=true - основной паттерн промазал, взят первый 64-hex в окне после
|
||||
// `debug_info` (страховка смены обёртки). Иначе - ошибка со снапшотом окна.
|
||||
func extractDebugInfo(body []byte) (value string, usedFallback bool, err error) {
|
||||
if m := reCaptchaDebugInfo.FindSubmatch(body); len(m) >= 2 {
|
||||
return string(m[1]), false, nil
|
||||
}
|
||||
idx := bytes.Index(body, []byte("debug_info"))
|
||||
if idx < 0 {
|
||||
return "", false, errors.New("debug_info marker not found in script")
|
||||
}
|
||||
end := idx + 256
|
||||
if end > len(body) {
|
||||
end = len(body)
|
||||
}
|
||||
if m := reCaptchaHex64.FindSubmatch(body[idx:end]); len(m) >= 2 {
|
||||
return string(m[1]), true, nil
|
||||
}
|
||||
return "", false, fmt.Errorf("debug_info match not found near: %q", body[idx:end])
|
||||
}
|
||||
|
||||
func parseCaptchaPage(html string) (*captchaPage, error) {
|
||||
page := &captchaPage{}
|
||||
|
||||
|
||||
@@ -77,6 +77,59 @@ func TestEncodeCaptchaPoW(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractDebugInfo(t *testing.T) {
|
||||
const hash = "59f60d917b13be6a22c076adb2c17df37302c5314d8353a27e72f9fbcc9b4838"
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
want string
|
||||
wantFallback bool
|
||||
fail bool
|
||||
}{
|
||||
{
|
||||
name: "primary vk format",
|
||||
body: `x,debug_info:(null===(r=window.vk)||void 0===r?void 0:r.brlefapmjnpg)||"` + hash + `"});var a`,
|
||||
want: hash,
|
||||
},
|
||||
{
|
||||
name: "windowed fallback no or-wrapper",
|
||||
body: `debug_info: someRuntimeCall(),foo:"` + hash + `"`,
|
||||
want: hash,
|
||||
wantFallback: true,
|
||||
},
|
||||
{
|
||||
name: "no hash near marker",
|
||||
body: `debug_info:window.vk.x||"tooshort"`,
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "no marker",
|
||||
body: `nothing here`,
|
||||
fail: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, usedFallback, err := extractDebugInfo([]byte(tt.body))
|
||||
if tt.fail {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got %q", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("extractDebugInfo = %q, want %q", got, tt.want)
|
||||
}
|
||||
if usedFallback != tt.wantFallback {
|
||||
t.Fatalf("usedFallback = %v, want %v", usedFallback, tt.wantFallback)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptchaDomainFromRedirectURI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user