From 4fa7810afa35bd086f30b66e3e0660df702ba3fe Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Tue, 4 Aug 2026 11:56:03 +0300 Subject: [PATCH] =?UTF-8?q?fix(captcha):=20=D0=BF=D1=80=D0=B8=D0=B2=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B?= =?UTF-8?q?=20=D0=BA=20=D0=BF=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=B6=D0=B8=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=B2=D0=B8?= =?UTF-8?q?=D0=B4=D0=B6=D0=B5=D1=82=D0=B0=201.1.1388?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сверка протокола с бандлом вскрыла три расхождения. Телеметрия: указатель виджет пишет только через externalCoords, а их заполняет хост-страница iframe или нативный бридж мобильного SDK. Мы ходим на captcha прямой навигацией, где живой браузер оставляет cursor и taps пустыми, - вместе с ними ушла синтетическая траектория руки, недостижимая для этого режима. A/B на живой captcha: 10/10 сессий против 9/10 со старым поведением. Заголовки: навигация документа не несла Upgrade-Insecure-Requests и Sec-Fetch-User, приоритет всем запросам ставился как у fetch, Origin уходил на GET-ы, где Chrome его не шлёт, Referer навигации указывал на саму цель вместо страницы-источника, а бандл тянулся профилем fetch вместо тега script. Cookie отсутствовал в списке порядка и уезжал последним заголовком. Куки: tls-client отбрасывает удалённые куки только по Max-Age - проверка Expires в его jar закомментирована. Погашенная сервером remixir=DELETED из-за этого уходила в каждом запросе. Обёртка в personanet транслирует протухший Expires в Max-Age, который фильтр библиотеки понимает. Трассировка запросов под FREETURN_CAPTCHA_TRACE - ей расхождения и найдены. --- .gitignore | 1 + .../vk/internal/browserprofile/profile.go | 15 ++- .../provider/vk/internal/captcha/captcha.go | 34 ++++-- .../provider/vk/internal/captcha/slider.go | 2 +- .../provider/vk/internal/captcha/telemetry.go | 77 ++---------- .../vk/internal/captcha/telemetry_test.go | 80 ++++-------- .../provider/vk/internal/captcha/trace.go | 114 ++++++++++++++++++ .../vk/internal/personanet/cookiejar.go | 35 ++++++ .../provider/vk/internal/vkauth/client.go | 5 +- 9 files changed, 222 insertions(+), 141 deletions(-) create mode 100644 internal/provider/vk/internal/captcha/trace.go create mode 100644 internal/provider/vk/internal/personanet/cookiejar.go diff --git a/.gitignore b/.gitignore index c41bef6..202d160 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ client_config.json node_modules dist /mobile-dist/ +captcha_trace.md diff --git a/internal/provider/vk/internal/browserprofile/profile.go b/internal/provider/vk/internal/browserprofile/profile.go index d364f21..bdf72c4 100644 --- a/internal/provider/vk/internal/browserprofile/profile.go +++ b/internal/provider/vk/internal/browserprofile/profile.go @@ -94,8 +94,9 @@ type device struct { NotificationsPermission string `json:"notificationsPermission"` } -// Порядок заголовков h2 для fetch/XHR. Отсутствующие в запросе имена fhttp -// пропускает, поэтому один список покрывает GET и POST. +// Порядок заголовков h2. Отсутствующие в запросе имена fhttp пропускает, поэтому +// один список покрывает и навигацию (upgrade-insecure-requests, sec-fetch-user), +// и fetch/XHR (origin, content-type). var chromeHeaderOrder = []string{ "content-length", "sec-ch-ua-platform", @@ -103,14 +104,17 @@ var chromeHeaderOrder = []string{ "sec-ch-ua", "content-type", "sec-ch-ua-mobile", + "upgrade-insecure-requests", "accept", "origin", "sec-fetch-site", "sec-fetch-mode", + "sec-fetch-user", "sec-fetch-dest", "referer", "accept-encoding", "accept-language", + "cookie", "priority", } @@ -228,7 +232,12 @@ func ApplyFhttp(req *fhttp.Request, profile Profile) { req.Header.Set("sec-ch-ua-mobile", profile.SecChUaMobile) req.Header.Set("sec-ch-ua-platform", profile.SecChUaPlatform) req.Header.Set("Accept-Language", profile.AcceptLanguage) - req.Header.Set("Priority", "u=1, i") + // Навигация документа идёт с нулевым приоритетом, u=1 - профиль fetch/XHR. + if req.Header.Get("Sec-Fetch-Dest") == "document" { + req.Header.Set("Priority", "u=0, i") + } else { + req.Header.Set("Priority", "u=1, i") + } req.Header[fhttp.HeaderOrderKey] = chromeHeaderOrder } diff --git a/internal/provider/vk/internal/captcha/captcha.go b/internal/provider/vk/internal/captcha/captcha.go index bdcd479..5070c00 100644 --- a/internal/provider/vk/internal/captcha/captcha.go +++ b/internal/provider/vk/internal/captcha/captcha.go @@ -336,10 +336,16 @@ func captchaDomainFromRedirectURI(redirectURI string) string { func (s *captchaSession) fetchCaptchaHTML(redirectURI string) (string, error) { body, err := s.doRaw(fhttp.MethodGet, redirectURI, nil, map[string]string{ - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Sec-Fetch-Dest": "document", - "Sec-Fetch-Mode": "navigate", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + // Переход по редиректу считается пользовательской активацией. + "Sec-Fetch-User": "?1", "Sec-Fetch-Site": "cross-site", + // Referer кросс-сайтового перехода режется политикой до origin страницы, + // с которой ушли, а ей была страница звонка. + "Referer": "https://" + s.domain + "/", }) if err != nil { return "", err @@ -367,9 +373,13 @@ func (s *captchaSession) fetchDebugInfoJS(scriptURL string) (string, error) { return v, nil } } + // Бандл тянет тег