fix(captcha): привести запросы к поведению живого виджета 1.1.1388

Сверка протокола с бандлом вскрыла три расхождения.

Телеметрия: указатель виджет пишет только через 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 - ей расхождения и найдены.
This commit is contained in:
samosvalishe
2026-08-04 11:56:03 +03:00
parent 8eb3873d27
commit 4fa7810afa
9 changed files with 222 additions and 141 deletions
+1
View File
@@ -18,3 +18,4 @@ client_config.json
node_modules
dist
/mobile-dist/
captcha_trace.md
@@ -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
}
@@ -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
}
}
// Бандл тянет тег <script> страницы captcha, а не fetch: другой Dest и no-cors.
body, err := s.doRaw(fhttp.MethodGet, scriptURL, nil, map[string]string{
"Accept": "text/javascript,*/*",
"Referer": captchaAPIOrigin + "/",
"Accept": "*/*",
"Sec-Fetch-Dest": "script",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "same-site",
"Referer": captchaAPIOrigin + "/",
})
if err != nil {
return "", err
@@ -445,9 +455,8 @@ func (s *captchaSession) captchaRequest(method string, form [][2]string) (map[st
func (s *captchaSession) performCaptchaCheck(
sessionToken string,
answerJSON string,
g gesture,
) (*captchaCheck, error) {
data := buildAnalytics(s.profile, s.sensors, g, time.Since(s.sensorsStart))
data := buildAnalytics(s.profile, s.sensors, time.Since(s.sensorsStart))
values := make([][2]string, 0, 15)
values = append(values,
[2]string{"session_token", sessionToken},
@@ -466,8 +475,8 @@ func (s *captchaSession) performCaptchaCheck(
if err != nil {
return nil, fmt.Errorf("captcha check failed: %w", err)
}
s.logger().Debugf("[Captcha] check payload answer_bytes=%d pointer_samples=%d conn_samples=%d",
len(answerJSON), len(data.cursor)+len(data.taps), len(data.connRtt))
s.logger().Debugf("[Captcha] check payload answer_bytes=%d conn_samples=%d accel_samples=%d",
len(answerJSON), len(data.connRtt), len(data.accelerometer))
check, err := parseCaptchaCheck(resp)
if err != nil {
return nil, err
@@ -542,7 +551,7 @@ func (s *captchaSession) solveCheckboxCaptcha(sessionToken string) (string, erro
return "", err
}
check, err := s.performCaptchaCheck(sessionToken, "{}", g)
check, err := s.performCaptchaCheck(sessionToken, "{}")
if err != nil {
return "", err
}
@@ -607,9 +616,11 @@ func (s *captchaSession) doRaw(
req.Header.Set("Sec-Fetch-Site", "same-site")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Origin", captchaAPIOrigin)
req.Header.Set("Referer", captchaAPIOrigin+"/")
if form != nil {
// Origin ставится только на CORS-запросы виджета; GET навигации и тега
// <script> его не несут.
req.Header.Set("Origin", captchaAPIOrigin)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
for k, v := range extraHeaders {
@@ -632,6 +643,7 @@ func (s *captchaSession) doRaw(
}()
data, readErr := io.ReadAll(resp.Body)
s.logger().Debugf("[Captcha] http %s %s status=%d bytes=%d after=%s form=%s", method, captchaSafeURL(endpoint), resp.StatusCode, len(data), time.Since(start).Truncate(time.Millisecond), captchaFormSummary(form))
traceExchange(req, form, resp, data, time.Since(start))
return data, readErr
}
@@ -109,7 +109,7 @@ func (s *captchaSession) solveSliderCaptcha(
}
handle = target
check, err := s.performCaptchaCheck(sessionToken, string(answerData), g)
check, err := s.performCaptchaCheck(sessionToken, string(answerData))
if err != nil {
return "", err
}
@@ -3,11 +3,13 @@ package captcha
// Телеметрия captcha.
//
// Виджет заводит setInterval(sensors_delay) и на каждом тике кладёт в аккумулятор
// последнюю известную позицию указателя (cursor - мышь, taps - палец) плюс
// navigator.connection.rtt/downlink. Акселерометр пишется отдельным потоком
// событий Generic Sensor API с частотой 1000/sensors_delay. Позиция обновляется
// только по событию ввода, поэтому неподвижный указатель даёт цепочку одинаковых
// сэмплов, а до первого события массив пуст.
// событий Generic Sensor API с частотой 1000/sensors_delay.
//
// Указатель (cursor - мышь, taps - палец) пишется только через externalCoords, а
// их заполняет либо нативный бридж мобильного SDK, либо хост-страница, если
// виджет открыт в iframe. Мы ходим на captcha прямой навигацией, без хоста, -
// в этом режиме виджет живого браузера оставляет оба массива пустыми.
//
// В captchaNotRobot.check уходят все семь массивов, даже пустые: значения,
// которых браузер не дал, виджет просто не кладёт в массив, но сам ключ
@@ -133,32 +135,13 @@ type gesture struct {
// buildAnalytics собирает аккумулятор так, как его собрал бы живой браузер за
// elapsed с момента ответа settings: тиков ровно столько, сколько успел сделать
// таймер, а непокрытые персоной сенсоры остаются пустыми.
func buildAnalytics(p browserprofile.Profile, cfg sensorConfig, g gesture, elapsed time.Duration) analytics {
func buildAnalytics(p browserprofile.Profile, cfg sensorConfig, elapsed time.Duration) analytics {
ticks := ticksIn(elapsed, cfg.delay)
if ticks == 0 {
// Таймер не успел тикнуть ни разу - живой виджет отправил бы пустые массивы.
return analytics{}
}
moveTicks := min(max(ticksIn(g.move, cfg.delay), 1), ticks)
settleTicks := min(ticksIn(g.settle, cfg.delay), ticks-moveTicks)
idleTicks := ticks - moveTicks - settleTicks
track := make([]point, 0, ticks)
if !p.Touch() {
// Мышь видна виджету с первого тика: до движения сыплются одинаковые
// сэмплы стартовой позиции.
track = appendRepeat(track, g.from, idleTicks)
}
track = append(track, samplePath(g.from, g.to, moveTicks)...)
track = appendRepeat(track, track[len(track)-1], settleTicks)
out := analytics{}
switch {
case p.Touch() && cfg.taps:
out.taps = track
case !p.Touch() && cfg.cursor:
out.cursor = track
}
out.connRtt, out.connDownlink = sampleConnection(p.IsMobile(), ticks)
if p.Accelerometer() && cfg.accelerometer {
out.accelerometer = sampleAccelerometer(ticks)
@@ -173,52 +156,6 @@ func ticksIn(d time.Duration, delay time.Duration) int {
return min(int(d/delay), sensorTicksMax)
}
func appendRepeat(dst []point, value point, count int) []point {
for range count {
dst = append(dst, value)
}
return dst
}
// samplePath снимает count позиций с дуги from->to: живая рука идёт не по прямой
// и тормозит у цели, а таймер снимает позицию через равные интервалы.
func samplePath(from, to point, count int) []point {
if count <= 0 {
return nil
}
ctrl := point{
X: (from.X+to.X)/2 + randx.Intn(61) - 30,
Y: (from.Y+to.Y)/2 - randx.Intn(30) - 10,
}
points := make([]point, 0, count)
for i := 1; i <= count; i++ {
t := easeInOut(float64(i) / float64(count))
x := quadBezier(float64(from.X), float64(ctrl.X), float64(to.X), t)
y := quadBezier(float64(from.Y), float64(ctrl.Y), float64(to.Y), t)
// Дрожание руки гаснет к концу движения.
jitter := int(math.Round((1-t)*4)) + 1
points = append(points, point{
X: int(math.Round(x)) + randx.Intn(jitter*2+1) - jitter,
Y: int(math.Round(y)) + randx.Intn(jitter*2+1) - jitter,
})
}
points[len(points)-1] = to
return points
}
func quadBezier(p0, p1, p2, t float64) float64 {
inv := 1 - t
return inv*inv*p0 + 2*t*inv*p1 + t*t*p2
}
func easeInOut(t float64) float64 {
if t < 0.5 {
return 2 * t * t
}
p := -2*t + 2
return 1 - p*p/2
}
// sampleConnection повторяет NetworkInformation: Chrome огрубляет rtt до кратных
// 25 мс и downlink до шага 0.05 с потолком 10 Мбит/с, а между тиками оценка
// меняется редко.
@@ -12,24 +12,13 @@ func testPersona(plat browserprofile.Platform) browserprofile.Profile {
return browserprofile.For(plat, browserprofile.Identity{Seed: "telemetry-test"})
}
func testGesture(p browserprofile.Profile) gesture {
l := layoutFor(p)
target := l.sliderTarget(3, 8)
return gesture{
from: approachFrom(p, l, target),
to: target,
move: 1500 * time.Millisecond,
settle: 400 * time.Millisecond,
}
}
// Виджет всегда сериализует все семь ключей аккумулятора: пустой массив - валидный
// ответ браузера, отсутствующий ключ - невозможное состояние.
func TestAnalyticsAlwaysSendsAllKeys(t *testing.T) {
want := []string{"accelerometer", "gyroscope", "motion", "cursor", "taps", "connectionRtt", "connectionDownlink"}
for _, plat := range []browserprofile.Platform{browserprofile.Desktop, browserprofile.Mobile} {
p := testPersona(plat)
fields := buildAnalytics(p, defaultSensorConfig(), testGesture(p), 4*time.Second).fields()
fields := buildAnalytics(p, defaultSensorConfig(), 4*time.Second).fields()
if len(fields) != len(want) {
t.Fatalf("%s: %d fields, want %d", plat, len(fields), len(want))
}
@@ -47,16 +36,15 @@ func TestAnalyticsAlwaysSendsAllKeys(t *testing.T) {
// Телеметрия не может содержать данных, которых персона физически не соберёт.
func TestAnalyticsRespectsPersonaCapabilities(t *testing.T) {
cases := []struct {
plat browserprofile.Platform
wantAccel bool
wantPointer string
plat browserprofile.Platform
wantAccel bool
}{
{browserprofile.Desktop, false, "cursor"},
{browserprofile.Mobile, true, "taps"},
{browserprofile.Desktop, false},
{browserprofile.Mobile, true},
}
for _, tc := range cases {
p := testPersona(tc.plat)
data := buildAnalytics(p, defaultSensorConfig(), testGesture(p), 4*time.Second)
data := buildAnalytics(p, defaultSensorConfig(), 4*time.Second)
if len(data.connRtt) == 0 {
t.Fatalf("%s: NetworkInformation отсутствует", tc.plat)
@@ -70,37 +58,34 @@ func TestAnalyticsRespectsPersonaCapabilities(t *testing.T) {
if len(data.gyroscope) != 0 || len(data.motion) != 0 {
t.Fatalf("%s: web-персона не может дать gyroscope/motion", tc.plat)
}
if tc.wantPointer == "cursor" && (len(data.cursor) == 0 || len(data.taps) != 0) {
t.Fatalf("%s: мышь обязана писать cursor и не писать taps", tc.plat)
}
if tc.wantPointer == "taps" && (len(data.taps) == 0 || len(data.cursor) != 0) {
t.Fatalf("%s: тач обязан писать taps и не писать cursor", tc.plat)
}
}
// Указатель пишется только через externalCoords, а их заполняет хост-страница или
// нативный бридж. При прямой навигации виджет живого браузера оставляет cursor и
// taps пустыми - и мы обязаны делать то же.
func TestAnalyticsLeavesPointerEmpty(t *testing.T) {
for _, plat := range []browserprofile.Platform{browserprofile.Desktop, browserprofile.Mobile} {
p := testPersona(plat)
data := buildAnalytics(p, defaultSensorConfig(), 10*time.Second)
if len(data.cursor) != 0 || len(data.taps) != 0 {
t.Fatalf("%s: cursor %d, taps %d, want 0", plat, len(data.cursor), len(data.taps))
}
}
}
// Число сэмплов задаёт таймер виджета: сколько тиков уместилось в реальное время
// сессии, столько и точек. Мышь видна с первого тика, палец - только с касания.
// сессии, столько и точек.
func TestAnalyticsSampleCountFollowsElapsed(t *testing.T) {
cfg := sensorConfig{delay: 100 * time.Millisecond, cursor: true, taps: true, accelerometer: true}
elapsed := 5 * time.Second
ticks := int(elapsed / cfg.delay)
desktop := testPersona(browserprofile.Desktop)
data := buildAnalytics(desktop, cfg, testGesture(desktop), elapsed)
if len(data.cursor) != ticks || len(data.connRtt) != ticks {
t.Fatalf("desktop: cursor %d, conn %d, want %d", len(data.cursor), len(data.connRtt), ticks)
}
mobile := testPersona(browserprofile.Mobile)
g := testGesture(mobile)
data = buildAnalytics(mobile, cfg, g, elapsed)
gestureTicks := int((g.move + g.settle) / cfg.delay)
if len(data.taps) > gestureTicks+1 || len(data.taps) == 0 {
t.Fatalf("mobile: taps %d, want <= %d (только время касания)", len(data.taps), gestureTicks+1)
}
if len(data.connRtt) != ticks {
t.Fatalf("mobile: conn %d, want %d", len(data.connRtt), ticks)
for _, plat := range []browserprofile.Platform{browserprofile.Desktop, browserprofile.Mobile} {
data := buildAnalytics(testPersona(plat), cfg, elapsed)
if len(data.connRtt) != ticks {
t.Fatalf("%s: conn %d, want %d", plat, len(data.connRtt), ticks)
}
}
}
@@ -116,7 +101,7 @@ func TestAnalyticsHonoursSensorList(t *testing.T) {
}
p := testPersona(browserprofile.Mobile)
data := buildAnalytics(p, cfg, testGesture(p), 3*time.Second)
data := buildAnalytics(p, cfg, 3*time.Second)
if len(data.taps) != 0 || len(data.accelerometer) != 0 {
t.Fatalf("taps %d, accelerometer %d: сервер их не слушает", len(data.taps), len(data.accelerometer))
}
@@ -126,7 +111,7 @@ func TestAnalyticsHonoursSensorList(t *testing.T) {
func TestAnalyticsWithoutTicksIsEmpty(t *testing.T) {
p := testPersona(browserprofile.Mobile)
cfg := sensorConfig{delay: 2 * time.Second, cursor: true, taps: true, accelerometer: true}
data := buildAnalytics(p, cfg, testGesture(p), 500*time.Millisecond)
data := buildAnalytics(p, cfg, 500*time.Millisecond)
for _, kv := range data.fields() {
if kv[1] != "[]" {
t.Fatalf("field %q = %s, want []", kv[0], kv[1])
@@ -145,19 +130,6 @@ func TestParseSensorConfigFallsBackToDefaults(t *testing.T) {
}
}
// Координаты указателя обязаны лежать внутри окна, заявленного в componentDone.
func TestPointerStaysInsideViewport(t *testing.T) {
for _, plat := range []browserprofile.Platform{browserprofile.Desktop, browserprofile.Mobile} {
p := testPersona(plat)
data := buildAnalytics(p, defaultSensorConfig(), testGesture(p), 4*time.Second)
for _, pt := range append(append([]point{}, data.cursor...), data.taps...) {
if pt.X < 0 || pt.Y < 0 || pt.X > p.Viewport.W || pt.Y > p.Viewport.H {
t.Fatalf("%s: точка %+v вне окна %+v", plat, pt, p.Viewport)
}
}
}
}
// Chrome огрубляет NetworkInformation: rtt кратен 25 мс, downlink - шагу 0.05 и
// не превышает 10 Мбит/с.
func TestConnectionSamplesQuantized(t *testing.T) {
@@ -0,0 +1,114 @@
package captcha
// Временная трассировка: полный дамп запросов и ответов captcha в один файл для
// ручного разбора. Включается FREETURN_CAPTCHA_TRACE=<путь>.
import (
"bytes"
"encoding/json"
"fmt"
"os"
"sort"
"strings"
"sync"
"time"
fhttp "github.com/bogdanfinn/fhttp"
)
var (
traceMu sync.Mutex
traceSeq int
)
func traceValue(v string, limit int) string {
if len(v) <= limit {
return v
}
return fmt.Sprintf("%s…%s [len=%d]", v[:limit/2], v[len(v)-limit/4:], len(v))
}
// traceHeaders отдаёт заголовки в том порядке, в каком их отправит fhttp.
func traceHeaders(h fhttp.Header) []string {
order, _ := h[fhttp.HeaderOrderKey]
seen := map[string]bool{}
out := make([]string, 0, len(h))
for _, name := range order {
for key, values := range h {
if strings.EqualFold(key, name) && !seen[key] {
seen[key] = true
out = append(out, fmt.Sprintf("%-27s %s", key+":", strings.Join(values, ", ")))
}
}
}
rest := make([]string, 0, len(h))
for key, values := range h {
if seen[key] || key == fhttp.HeaderOrderKey || key == fhttp.PHeaderOrderKey {
continue
}
rest = append(rest, fmt.Sprintf("%-27s %s <- вне списка порядка", key+":", strings.Join(values, ", ")))
}
sort.Strings(rest)
return append(out, rest...)
}
func traceBody(body []byte) string {
var pretty bytes.Buffer
if json.Indent(&pretty, body, "", " ") == nil {
return traceValue(pretty.String(), 4000)
}
return traceValue(string(body), 2000)
}
func traceExchange(
req *fhttp.Request,
form [][2]string,
resp *fhttp.Response,
body []byte,
elapsed time.Duration,
) {
path := os.Getenv("FREETURN_CAPTCHA_TRACE")
if path == "" {
return
}
traceMu.Lock()
defer traceMu.Unlock()
traceSeq++
var b strings.Builder
fmt.Fprintf(&b, "\n## %d. %s %s\n\n", traceSeq, req.Method, req.URL.String())
fmt.Fprintf(&b, "### Заголовки запроса (в порядке отправки)\n\n```\n:method %s\n:authority %s\n:scheme %s\n:path %s\n%s\n```\n\n",
req.Method, req.URL.Host, req.URL.Scheme, req.URL.RequestURI(), strings.Join(traceHeaders(req.Header), "\n"))
if len(form) > 0 {
fmt.Fprintf(&b, "### Тело формы\n\n| поле | длина | значение |\n| --- | --- | --- |\n")
for _, kv := range form {
fmt.Fprintf(&b, "| `%s` | %d | `%s` |\n", kv[0], len(kv[1]), traceValue(kv[1], 200))
}
b.WriteString("\n")
}
if resp != nil {
fmt.Fprintf(&b, "### Ответ %s за %s\n\n```\n", resp.Status, elapsed.Truncate(time.Millisecond))
names := make([]string, 0, len(resp.Header))
for key := range resp.Header {
names = append(names, key)
}
sort.Strings(names)
for _, key := range names {
fmt.Fprintf(&b, "%-27s %s\n", key+":", traceValue(strings.Join(resp.Header[key], ", "), 300))
}
fmt.Fprintf(&b, "```\n\n```\n%s\n```\n", traceBody(body))
}
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
Log.Warnf("[Captcha][TRACE] open: %v", err)
return
}
defer func() { _ = f.Close() }()
if _, err := f.WriteString(b.String()); err != nil {
Log.Warnf("[Captcha][TRACE] write: %v", err)
}
}
@@ -0,0 +1,35 @@
package personanet
import (
neturl "net/url"
"time"
fhttp "github.com/bogdanfinn/fhttp"
tlsclient "github.com/bogdanfinn/tls-client"
)
// cookieJar чинит удаление кук в tls-client: его фильтр смотрит только на
// Max-Age (jar.go, проверка Expires закомментирована), а VK гасит куки прошедшим
// Expires. Без этого remixir=DELETED уезжает в каждом запросе - живой браузер
// такую куку удаляет и не шлёт никогда.
type cookieJar struct {
inner tlsclient.CookieJar
}
func NewCookieJar() tlsclient.CookieJar {
return &cookieJar{inner: tlsclient.NewCookieJar()}
}
func (j *cookieJar) SetCookies(u *neturl.URL, cookies []*fhttp.Cookie) {
now := time.Now()
for _, c := range cookies {
if c.MaxAge == 0 && !c.Expires.IsZero() && !c.Expires.After(now) {
c.MaxAge = -1
}
}
j.inner.SetCookies(u, cookies)
}
func (j *cookieJar) Cookies(u *neturl.URL) []*fhttp.Cookie { return j.inner.Cookies(u) }
func (j *cookieJar) GetAllCookies() map[string][]*fhttp.Cookie { return j.inner.GetAllCookies() }
@@ -12,6 +12,7 @@ import (
"github.com/samosvalishe/free-turn-proxy/internal/logx"
"github.com/samosvalishe/free-turn-proxy/internal/provider/vk/internal/browserprofile"
"github.com/samosvalishe/free-turn-proxy/internal/provider/vk/internal/personanet"
"github.com/samosvalishe/free-turn-proxy/internal/randx"
tlsclient "github.com/bogdanfinn/tls-client"
@@ -268,7 +269,7 @@ func (c *Client) fetch(ctx context.Context, link string, streamID int) (string,
var lastErr error
burns := 0
jar := tlsclient.NewCookieJar()
jar := personanet.NewCookieJar()
for i := 0; i < len(c.credentials); {
creds := c.credentials[i]
c.log.Infof("[STREAM %d] [VK Auth] Trying credentials: client_id=%s", streamID, creds.ClientID)
@@ -285,7 +286,7 @@ func (c *Client) fetch(ctx context.Context, link string, streamID int) (string,
// куками, пока не кончатся режимы решения captcha.
if errors.Is(err, ErrPersonaBurned) && burns < maxPersonaBurns {
burns++
jar = tlsclient.NewCookieJar()
jar = personanet.NewCookieJar()
continue
}
i++