diff --git a/internal/provider/vk/internal/browserprofile/profile.go b/internal/provider/vk/internal/browserprofile/profile.go index 3fe74a2..694e294 100644 --- a/internal/provider/vk/internal/browserprofile/profile.go +++ b/internal/provider/vk/internal/browserprofile/profile.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/json" "strconv" + "strings" fhttp "github.com/bogdanfinn/fhttp" "github.com/bogdanfinn/tls-client/profiles" @@ -17,6 +18,9 @@ const ( uaWindows = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36" uaMac = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36" uaAndroid = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36" + + timezoneMoscow = "Europe/Moscow" + touchPointsMobile = 5 ) type Platform string @@ -48,6 +52,7 @@ type Profile struct { cores int memGB int webdriver bool + dev device } func (p Profile) IsMobile() bool { return p.Platform == Mobile } @@ -63,6 +68,45 @@ func (p Profile) DeviceMemory() *int { return &p.memGB } +func (p Profile) Languages() []string { return p.dev.Languages } + +func (p Profile) DevicePixelRatio() float64 { return p.dev.DevicePixelRatio } + +func (Profile) Timezone() string { return timezoneMoscow } + +func (p Profile) MaxTouchPoints() int { + if p.IsMobile() { + return touchPointsMobile + } + return 0 +} + +func (p Profile) Orientation() string { + if p.IsMobile() { + return "portrait-primary" + } + return "landscape-primary" +} + +func (p Profile) PlatformName() string { return strings.Trim(p.SecChUaPlatform, `"`) } + +type Brand struct { + Brand string `json:"brand"` + Version string `json:"version"` +} + +func (p Profile) Brands() []Brand { + out := make([]Brand, 0, 3) + for _, part := range strings.Split(p.SecChUa, ", ") { + name, ver, ok := strings.Cut(part, ";v=") + if !ok { + continue + } + out = append(out, Brand{Brand: strings.Trim(name, `"`), Version: strings.Trim(ver, `"`)}) + } + return out +} + type device struct { ScreenWidth int `json:"screenWidth"` ScreenHeight int `json:"screenHeight"` @@ -186,6 +230,7 @@ func withDevice(p Profile, d device) Profile { return p } p.DeviceJSON = string(data) + p.dev = d p.cores, p.webdriver = d.HardwareConcurrency, d.Webdriver if d.DeviceMemory != nil { p.memGB = *d.DeviceMemory diff --git a/internal/provider/vk/internal/browserprofile/profile_test.go b/internal/provider/vk/internal/browserprofile/profile_test.go index fccf907..dce4990 100644 --- a/internal/provider/vk/internal/browserprofile/profile_test.go +++ b/internal/provider/vk/internal/browserprofile/profile_test.go @@ -2,6 +2,7 @@ package browserprofile import ( "encoding/json" + "reflect" "regexp" "slices" "strings" @@ -81,7 +82,7 @@ func TestForIsDeterministic(t *testing.T) { for _, plat := range []Platform{Desktop, Mobile} { id := Identity{Seed: testSeed, Gen: 3} first, again := For(plat, id), For(plat, id) - if first != again { + if !reflect.DeepEqual(first, again) { t.Fatalf("%s: For not deterministic", plat) } } diff --git a/internal/provider/vk/internal/captcha/assets.go b/internal/provider/vk/internal/captcha/assets.go index 7320a7d..29ba66c 100644 --- a/internal/provider/vk/internal/captcha/assets.go +++ b/internal/provider/vk/internal/captcha/assets.go @@ -29,11 +29,8 @@ type asset struct { Dest string } -func parsePageAssets(html, skip string) []asset { +func parsePageAssets(html string) []asset { seen := map[string]struct{}{} - if skip != "" { - seen[skip] = struct{}{} - } out := make([]asset, 0, assetsMaxCount) add := func(raw, dest string) { diff --git a/internal/provider/vk/internal/captcha/assets_test.go b/internal/provider/vk/internal/captcha/assets_test.go index b5297e1..06d473f 100644 --- a/internal/provider/vk/internal/captcha/assets_test.go +++ b/internal/provider/vk/internal/captcha/assets_test.go @@ -18,15 +18,15 @@ const assetsPageHTML = ` ` func TestParsePageAssetsClassifiesDest(t *testing.T) { - bundle := "https://static.vk.ru/vkid/1.1.1391/not_robot_captcha.js" - got := parsePageAssets(assetsPageHTML, bundle) + got := parsePageAssets(assetsPageHTML) want := map[string]string{ - "https://static.vk.ru/vkid/1.1.1391/main.css": "style", - "https://static.vk.ru/fonts/vk.woff2": "font", - "https://static.vk.ru/favicon.ico": "image", - "https://static.vk.ru/vkid/1.1.1391/chunk.js": "script", - "https://static.vk.ru/img/logo.png": "image", + "https://static.vk.ru/vkid/1.1.1391/main.css": "style", + "https://static.vk.ru/fonts/vk.woff2": "font", + "https://static.vk.ru/favicon.ico": "image", + "https://static.vk.ru/vkid/1.1.1391/not_robot_captcha.js": "script", + "https://static.vk.ru/vkid/1.1.1391/chunk.js": "script", + "https://static.vk.ru/img/logo.png": "image", } if len(got) != len(want) { t.Fatalf("assets = %d, want %d: %+v", len(got), len(want), got) @@ -38,20 +38,9 @@ func TestParsePageAssetsClassifiesDest(t *testing.T) { } } -// Бандл тянется отдельно ради debug_info: второй запрос за ним живой браузер не -// делает, у него ресурс уже в кеше. -func TestParsePageAssetsSkipsBundle(t *testing.T) { - bundle := "https://static.vk.ru/vkid/1.1.1391/not_robot_captcha.js" - for _, a := range parsePageAssets(assetsPageHTML, bundle) { - if a.URL == bundle { - t.Fatal("bundle must not be refetched as an asset") - } - } -} - // Относительные пути виджета обслуживает не CDN, и абсолютного URL для них нет. func TestParsePageAssetsDropsRelative(t *testing.T) { - for _, a := range parsePageAssets(assetsPageHTML, "") { + for _, a := range parsePageAssets(assetsPageHTML) { if a.URL == "/local/inline.js" { t.Fatal("relative asset must be dropped") } @@ -62,7 +51,7 @@ func TestParsePageAssetsDropsRelative(t *testing.T) { func TestParsePageAssetsDropsForeignHosts(t *testing.T) { html := `` + `` - got := parsePageAssets(html, "") + got := parsePageAssets(html) if len(got) != 1 || got[0].URL != "https://static.vk.ru/vkid/app.js" { t.Fatalf("assets = %+v, want only the vk.ru script", got) } @@ -73,7 +62,7 @@ func TestParsePageAssetsCaps(t *testing.T) { for i := range assetsMaxCount + 20 { html.WriteString(``) } - if got := len(parsePageAssets(html.String(), "")); got > assetsMaxCount { + if got := len(parsePageAssets(html.String())); got > assetsMaxCount { t.Fatalf("assets = %d, want <= %d", got, assetsMaxCount) } } diff --git a/internal/provider/vk/internal/captcha/captcha.go b/internal/provider/vk/internal/captcha/captcha.go index 87731dd..c731138 100644 --- a/internal/provider/vk/internal/captcha/captcha.go +++ b/internal/provider/vk/internal/captcha/captcha.go @@ -13,7 +13,6 @@ import ( "sort" "strconv" "strings" - "sync" "time" fhttp "github.com/bogdanfinn/fhttp" @@ -36,17 +35,14 @@ const ( ) var ( - reCaptchaScriptSrc = regexp.MustCompile(`src="(https://[^"]+not_robot_captcha[^"]+)"`) reCaptchaInitGlobal = regexp.MustCompile(`window\.init\s*=\s*\{`) - reCaptchaDebugInfo = regexp.MustCompile(`debug_info:(?:[^"]*\|\|)?"([a-fA-F0-9]{64})"`) + reCaptchaVKGlobal = regexp.MustCompile(`window\.vk\s*=\s*\{`) + reCaptchaDebugInfo = regexp.MustCompile(`[A-Za-z_$][\w$]*:\s*"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"`) errCaptchaRateLimit = errors.New("captcha session rate limit reached") errCaptchaBot = errors.New("captcha bot challenge") captchaMaxAttempts = 2 - - // debug_info - константа бандла, одна на все сессии; ключ кэша - URL бандла. - debugInfoCache sync.Map ) type captchaInitSetting struct { @@ -57,7 +53,7 @@ type captchaInitSetting struct { type captchaPage struct { Pow powParams - ScriptURL string + DebugInfo string Init captchaInitData } @@ -193,7 +189,7 @@ func (s *captchaSession) solveOnce(captchaErr *Error) (string, error) { } // Браузер тянет подресурсы параллельно с исполнением скрипта, не после него. - assets := parsePageAssets(html, page.ScriptURL) + assets := parsePageAssets(html) assetsDone := make(chan struct{}) go func() { defer close(assetsDone) @@ -208,10 +204,8 @@ func (s *captchaSession) solveOnce(captchaErr *Error) (string, error) { return "", err } - s.debugInfo, err = s.resolveDebugInfo(page.ScriptURL) - if err != nil { - return "", err - } + s.debugInfo = page.DebugInfo + s.logger().Debugf("[Captcha] debug_info=%s", s.debugInfo) <-assetsDone @@ -407,56 +401,35 @@ func (s *captchaSession) fetchCaptchaHTML(redirectURI string) (string, error) { return string(body), nil } -// resolveDebugInfo берёт константу только из живого бандла: пин означал бы чужой -// debug_info после любой ротации версии виджета. -func (s *captchaSession) resolveDebugInfo(scriptURL string) (string, error) { - if scriptURL == "" { - return "", errors.New("captcha script URL not in HTML") - } - v, err := s.fetchDebugInfoJS(scriptURL) - if err != nil { - return "", fmt.Errorf("captcha debug_info: %w", err) - } - s.logger().Debugf("[Captcha] debug_info from JS len=%d", len(v)) - return v, nil -} - -func (s *captchaSession) fetchDebugInfoJS(scriptURL string) (string, error) { - if cached, ok := debugInfoCache.Load(scriptURL); ok { - if v, ok := cached.(string); ok { - return v, nil - } - } - // Бандл тянет тег `, + want: "273cc83f-426f-4d98-9ce5-92490107e3a6", + }, + { + name: "quoted keys of nested json are not candidates", + html: ``, + want: "273cc83f-426f-4d98-9ce5-92490107e3a6", + }, + {name: "no window.vk", html: ``}, + {name: "no uuid in block", html: ``}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseCaptchaDebugInfo(tt.html); got != tt.want { + t.Fatalf("debug_info = %q, want %q", got, tt.want) + } + }) + } +} + +func TestParseCaptchaDebugInfoAmbiguous(t *testing.T) { + html := `` + if got := parseCaptchaDebugInfo(html); got != "273cc83f-426f-4d98-9ce5-92490107e3a6" { + t.Fatalf("debug_info = %q", got) + } +} diff --git a/internal/provider/vk/internal/captcha/pow.go b/internal/provider/vk/internal/captcha/pow.go index 9fbb9fd..0bbf0d3 100644 --- a/internal/provider/vk/internal/captcha/pow.go +++ b/internal/provider/vk/internal/captcha/pow.go @@ -157,16 +157,22 @@ func marshalJS(v any) ([]byte, error) { // powTelemetryData - пробы PoW-скрипта; порядок полей = порядок проб, так их // пишет JSON.stringify. type powTelemetryData struct { - Globals powProbe `json:"globals"` - UA powProbe `json:"ua"` - Frame powProbe `json:"frame"` - MatchMedia powProbe `json:"match_media"` - Plugins powProbe `json:"plugins"` - NavTamper powProbe `json:"nav_tamper"` - Referrer powProbe `json:"referrer"` - DevTools powProbe `json:"devtools"` - CSS powProbe `json:"css"` - NativeIntegrity powProbe `json:"native_integrity"` + Globals powProbe `json:"globals"` + UA powProbe `json:"ua"` + Frame powProbe `json:"frame"` + MatchMedia powProbe `json:"match_media"` + Plugins powProbe `json:"plugins"` + NavTamper powProbe `json:"nav_tamper"` + Referrer powProbe `json:"referrer"` + DevTools powProbe `json:"devtools"` + CSS powProbe `json:"css"` + NativeIntegrity powProbe `json:"native_integrity"` + CookieTest powProbe `json:"cookie_test"` + AncestorOrigins powProbe `json:"ancestor_origins"` + SandboxBehavior powProbe `json:"sandbox_behavior"` + MaxTouchPoints powProbe `json:"max_touch_points"` + TimezoneLocale powProbe `json:"timezone_locale"` + DevicePixelRatio powProbe `json:"device_pixel_ratio"` } type powProbe struct { @@ -177,16 +183,31 @@ type powProbe struct { func probe(result any) powProbe { return powProbe{OK: true, Result: result} } type powGlobals struct { - Doc bool `json:"doc"` - Win bool `json:"win"` - Nav bool `json:"nav"` - Webdriver bool `json:"webdriver"` - HW int `json:"hw"` - Mem *int `json:"mem"` + Doc bool `json:"doc"` + Win bool `json:"win"` + Nav bool `json:"nav"` + Webdriver bool `json:"webdriver"` + Subtle bool `json:"subtle"` + Secure bool `json:"secure"` + GCS bool `json:"gcs"` + RAF bool `json:"raf"` + Wasm bool `json:"wasm"` + PluginsLen int `json:"plugins_len"` + LanguagesLen int `json:"languages_len"` + HW int `json:"hw"` + Mem *int `json:"mem"` } type powUA struct { - UserAgent string `json:"userAgent"` + UserAgent string `json:"userAgent"` + UserAgentData *powUAData `json:"userAgentData"` +} + +type powUAData struct { + Brands []browserprofile.Brand `json:"brands"` + Platform string `json:"platform"` + Mobile bool `json:"mobile"` + Architecture *string `json:"architecture"` } type powFrame struct { @@ -203,13 +224,20 @@ type powMatchMedia struct { } type powPlugins struct { - Length int `json:"length"` - Names []string `json:"names"` - IsChrome bool `json:"isChrome"` + Length int `json:"length"` + Names []string `json:"names"` + Descriptions []string `json:"descriptions"` + MimeTypes [][]string `json:"mimeTypes"` + IsChrome bool `json:"isChrome"` } type powNavTamper struct { - Tampered bool `json:"tampered"` + Tampered bool `json:"tampered"` + ElCtor string `json:"el_ctor"` + StyleCtor string `json:"style_ctor"` + NavCtor string `json:"nav_ctor"` + AlertNative bool `json:"alert_native"` + ToStringNative bool `json:"to_string_native"` } type powReferrer struct { @@ -219,7 +247,8 @@ type powReferrer struct { } type powDevTools struct { - Open bool `json:"open"` + Open bool `json:"open"` + DelayMs int `json:"delay_ms"` } type powCSS struct { @@ -227,11 +256,43 @@ type powCSS struct { } type powNativeIntegrity struct { - ProtoMatch bool `json:"protoMatch"` - XHRNative bool `json:"xhrNative"` + ProtoMatch bool `json:"protoMatch"` + XHRNative bool `json:"xhrNative"` + XHRSendNative bool `json:"xhrSendNative"` + AddEventListenerNative bool `json:"addEventListenerNative"` + AlertNative bool `json:"alertNative"` + ToStringNative bool `json:"toStringNative"` +} + +type powCookieTest struct { + Write bool `json:"write"` +} + +type powAncestorOrigins struct { + AncestorOrigin *string `json:"ancestorOrigin"` +} + +type powSandboxBehavior struct { + OriginIsNull bool `json:"originIsNull"` + LocalStorage bool `json:"localStorage"` + SessionStorage bool `json:"sessionStorage"` +} + +type powMaxTouchPoints struct { + MaxTouchPoints int `json:"maxTouchPoints"` +} + +type powTimezoneLocale struct { + Timezone string `json:"timezone"` + Languages []string `json:"languages"` +} + +type powDevicePixelRatio struct { + DPR float64 `json:"dpr"` + Orientation string `json:"orientation"` + OrientationAngle int `json:"orientationAngle"` } -// Chrome отдаёт пять фиктивных PDF-плагинов везде, где умеет показывать PDF. var chromePDFPlugins = []string{ "PDF Viewer", "Chrome PDF Viewer", @@ -240,36 +301,84 @@ var chromePDFPlugins = []string{ "WebKit built-in PDF", } +const chromePDFDescription = "Portable Document Format" + +var chromePDFMimeTypes = []string{"application/pdf", "text/pdf"} + +func chromePlugins() powPlugins { + out := powPlugins{ + Length: len(chromePDFPlugins), + Names: chromePDFPlugins, + Descriptions: make([]string, len(chromePDFPlugins)), + MimeTypes: make([][]string, len(chromePDFPlugins)), + IsChrome: true, + } + for i := range chromePDFPlugins { + out.Descriptions[i] = chromePDFDescription + out.MimeTypes[i] = chromePDFMimeTypes + } + return out +} + // powTelemetry описывает страницу такой, какой её видел бы браузер персоны: // страницу мы тянем верхнеуровневой навигацией (Sec-Fetch-Dest: document), // поэтому frame и referrer описывают top-level документ - врозь их менять нельзя. func (s *captchaSession) powTelemetry() powTelemetryData { p := s.profile // На Android нет встроенного PDF-вьювера, значит и списка плагинов. - plugins := powPlugins{Names: []string{}} + plugins := powPlugins{Names: []string{}, Descriptions: []string{}, MimeTypes: [][]string{}} if !p.IsMobile() { - plugins = powPlugins{Length: len(chromePDFPlugins), Names: chromePDFPlugins, IsChrome: true} + plugins = chromePlugins() } dark := prefersDark(p) return powTelemetryData{ Globals: probe(powGlobals{ Doc: true, Win: true, Nav: true, - Webdriver: p.Webdriver(), - HW: p.HardwareConcurrency(), - Mem: p.DeviceMemory(), + Webdriver: p.Webdriver(), + Subtle: true, + Secure: true, + GCS: true, + RAF: true, + Wasm: true, + PluginsLen: plugins.Length, + LanguagesLen: len(p.Languages()), + HW: p.HardwareConcurrency(), + Mem: p.DeviceMemory(), + }), + UA: probe(powUA{ + UserAgent: p.UserAgent, + UserAgentData: &powUAData{ + Brands: p.Brands(), + Platform: p.PlatformName(), + Mobile: p.IsMobile(), + }, }), - UA: probe(powUA{UserAgent: p.UserAgent}), Frame: probe(powFrame{ParentAccessible: true}), MatchMedia: probe(powMatchMedia{PrefersDark: dark, PrefersLight: !dark, PointerFine: !p.IsMobile()}), Plugins: probe(plugins), - NavTamper: probe(powNavTamper{}), + NavTamper: probe(powNavTamper{ + ElCtor: "HTMLDivElement", StyleCtor: "CSSStyleDeclaration", NavCtor: "Navigator", + AlertNative: true, ToStringNative: true, + }), Referrer: probe(powReferrer{ Referrer: "https://" + s.domain + "/", Domain: strings.TrimPrefix(s.pageOrigin, "https://"), }), - DevTools: probe(powDevTools{}), - CSS: probe(powCSS{}), - NativeIntegrity: probe(powNativeIntegrity{ProtoMatch: true, XHRNative: true}), + DevTools: probe(powDevTools{}), + CSS: probe(powCSS{}), + NativeIntegrity: probe(powNativeIntegrity{ + ProtoMatch: true, XHRNative: true, XHRSendNative: true, + AddEventListenerNative: true, AlertNative: true, ToStringNative: true, + }), + CookieTest: probe(powCookieTest{Write: true}), + AncestorOrigins: probe(powAncestorOrigins{}), + SandboxBehavior: probe(powSandboxBehavior{LocalStorage: true, SessionStorage: true}), + MaxTouchPoints: probe(powMaxTouchPoints{MaxTouchPoints: p.MaxTouchPoints()}), + TimezoneLocale: probe(powTimezoneLocale{Timezone: p.Timezone(), Languages: p.Languages()}), + DevicePixelRatio: probe(powDevicePixelRatio{ + DPR: p.DevicePixelRatio(), + Orientation: p.Orientation(), + }), } } diff --git a/internal/provider/vk/internal/captcha/pow_test.go b/internal/provider/vk/internal/captcha/pow_test.go index bf8256c..388430f 100644 --- a/internal/provider/vk/internal/captcha/pow_test.go +++ b/internal/provider/vk/internal/captcha/pow_test.go @@ -26,13 +26,13 @@ func TestParseCaptchaPageFixture(t *testing.T) { if err != nil { t.Fatal(err) } - if page.Pow.Input != "fnZQN7lKKXvo37tH" || page.Pow.Difficulty != 2 || page.Pow.Prefix != "v2." { + if page.Pow.Input != "ST9BEClStK0wSdo4" || page.Pow.Difficulty != 2 || page.Pow.Prefix != "v2." { t.Fatalf("pow = %+v", page.Pow) } - if page.ScriptURL != "https://static.vk.ru/vkid/1.1.1394/not_robot_captcha.js" { - t.Fatalf("script url = %q", page.ScriptURL) + if page.DebugInfo != "273cc83f-426f-4d98-9ce5-92490107e3a6" { + t.Fatalf("debug_info = %q", page.DebugInfo) } - if !page.Init.Found || page.Init.APIHost != "api.vk.ru" || page.Init.ShowType != "checkbox" { + if page.Init.Found { t.Fatalf("init = %+v", page.Init) } } @@ -82,17 +82,24 @@ func TestSolvePoW(t *testing.T) { // Эталон снят с персоны seed=fixture; tel_hash сверен с канонизатором самой // страницы. Любая правка значений телеметрии обязана осознанно двигать эталон. const ( - goldenTelemetry = `{"globals":{"ok":true,"result":{"doc":true,"win":true,"nav":true,"webdriver":false,"hw":12,"mem":8}},` + - `"ua":{"ok":true,"result":{"userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"}},` + + goldenTelemetry = `{"globals":{"ok":true,"result":{"doc":true,"win":true,"nav":true,"webdriver":false,"subtle":true,"secure":true,"gcs":true,"raf":true,"wasm":true,"plugins_len":5,"languages_len":2,"hw":12,"mem":8}},` + + `"ua":{"ok":true,"result":{"userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36","userAgentData":{"brands":[{"brand":"Google Chrome","version":"146"},{"brand":"Chromium","version":"146"},{"brand":"Not)A;Brand","version":"24"}],"platform":"Windows","mobile":false,"architecture":null}}},` + `"frame":{"ok":true,"result":{"frameElement":null,"ancestorOriginsLen":0,"parentAccessible":true}},` + `"match_media":{"ok":true,"result":{"prefersDark":true,"prefersLight":false,"reducedMotion":false,"pointerFine":true}},` + - `"plugins":{"ok":true,"result":{"length":5,"names":["PDF Viewer","Chrome PDF Viewer","Chromium PDF Viewer","Microsoft Edge PDF Viewer","WebKit built-in PDF"],"isChrome":true}},` + - `"nav_tamper":{"ok":true,"result":{"tampered":false}},` + + `"plugins":{"ok":true,"result":{"length":5,"names":["PDF Viewer","Chrome PDF Viewer","Chromium PDF Viewer","Microsoft Edge PDF Viewer","WebKit built-in PDF"],"descriptions":["Portable Document Format","Portable Document Format","Portable Document Format","Portable Document Format","Portable Document Format"],"mimeTypes":[["application/pdf","text/pdf"],["application/pdf","text/pdf"],["application/pdf","text/pdf"],["application/pdf","text/pdf"],["application/pdf","text/pdf"]],"isChrome":true}},` + + `"nav_tamper":{"ok":true,"result":{"tampered":false,"el_ctor":"HTMLDivElement","style_ctor":"CSSStyleDeclaration","nav_ctor":"Navigator","alert_native":true,"to_string_native":true}},` + `"referrer":{"ok":true,"result":{"referrer":"https://vk.com/","inIframe":false,"domain":"id.vk.ru"}},` + - `"devtools":{"ok":true,"result":{"open":false}},` + + `"devtools":{"ok":true,"result":{"open":false,"delay_ms":0}},` + `"css":{"ok":true,"result":{"expectedMissing":0}},` + - `"native_integrity":{"ok":true,"result":{"protoMatch":true,"xhrNative":true}}}` - goldenTelHash = "4edea10bbb2b35b8790cfd79ad510fd8e833158c7095788841c2fd92b2edd3d1" + `"native_integrity":{"ok":true,"result":{"protoMatch":true,"xhrNative":true,"xhrSendNative":true,"addEventListenerNative":true,"alertNative":true,"toStringNative":true}},` + + `"cookie_test":{"ok":true,"result":{"write":true}},` + + `"ancestor_origins":{"ok":true,"result":{"ancestorOrigin":null}},` + + `"sandbox_behavior":{"ok":true,"result":{"originIsNull":false,"localStorage":true,"sessionStorage":true}},` + + `"max_touch_points":{"ok":true,"result":{"maxTouchPoints":0}},` + + `"timezone_locale":{"ok":true,"result":{"timezone":"Europe/Moscow","languages":["ru-RU","en-US"]}},` + + `"device_pixel_ratio":{"ok":true,"result":{"dpr":1,"orientation":"landscape-primary","orientationAngle":0}}}` + + goldenTelHash = "838102524be656361f2f88dba3dd16d3baaa20b5b33251b472b365ff646b99e9" ) // Конверт целиком: порядок ключей, телеметрия персоны и её sha256 - всё, что VK diff --git a/internal/provider/vk/internal/captcha/testdata/not_robot_captcha.html b/internal/provider/vk/internal/captcha/testdata/not_robot_captcha.html index 9c25f3b..f12e6b9 100644 --- a/internal/provider/vk/internal/captcha/testdata/not_robot_captcha.html +++ b/internal/provider/vk/internal/captcha/testdata/not_robot_captcha.html @@ -1,6 +1,126 @@ - - - -
- - + + + + + + + VK ID + + + + + + + +
+ + + diff --git a/internal/provider/vk/internal/vkauth/persona_test.go b/internal/provider/vk/internal/vkauth/persona_test.go index ae7db90..4aa73dd 100644 --- a/internal/provider/vk/internal/vkauth/persona_test.go +++ b/internal/provider/vk/internal/vkauth/persona_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net" + "reflect" "strings" "testing" @@ -20,11 +21,11 @@ func testCaptchaErr() *captcha.Error { func TestPersonaStableUntilBurned(t *testing.T) { c := New(Config{FingerprintSeed: "install-1"}) first, again := c.currentPersona(), c.currentPersona() - if first != again { + if !reflect.DeepEqual(first, again) { t.Fatal("persona not stable") } same := New(Config{FingerprintSeed: "install-1"}) - if same.currentPersona() != c.currentPersona() { + if !reflect.DeepEqual(same.currentPersona(), c.currentPersona()) { t.Fatal("same seed produced different persona") } other := New(Config{FingerprintSeed: "install-2"})