From 3b33d33b9e3c2d8d459b479e8d8a94fc562f18cb Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Tue, 7 Jul 2026 19:15:50 +0300 Subject: [PATCH] =?UTF-8?q?fix(captcha):=20=D1=83=D1=81=D1=82=D0=BE=D0=B9?= =?UTF-8?q?=D1=87=D0=B8=D0=B2=D1=8B=D0=B9=20=D1=80=D0=B0=D0=B7=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=20debug=5Finfo=20=D0=B8=20=D1=81=D0=B8=D0=B3=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=B4=D1=80=D0=B5=D0=B9=D1=84=D0=B0=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D0=B8=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../provider/vk/internal/captcha/captcha.go | 56 +++++++++++++++---- .../vk/internal/captcha/captcha_test.go | 53 ++++++++++++++++++ 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/internal/provider/vk/internal/captcha/captcha.go b/internal/provider/vk/internal/captcha/captcha.go index b555057..c0d080e 100644 --- a/internal/provider/vk/internal/captcha/captcha.go +++ b/internal/provider/vk/internal/captcha/captcha.go @@ -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{} diff --git a/internal/provider/vk/internal/captcha/captcha_test.go b/internal/provider/vk/internal/captcha/captcha_test.go index dc3bb08..4d3fea2 100644 --- a/internal/provider/vk/internal/captcha/captcha_test.go +++ b/internal/provider/vk/internal/captcha/captcha_test.go @@ -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