fix(captcha): адаптация

This commit is contained in:
samosvalishe
2026-08-19 20:00:02 +03:00
parent ec1b1fce77
commit 9d454dbd41
10 changed files with 411 additions and 136 deletions
@@ -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
@@ -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)
}
}
@@ -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) {
@@ -18,15 +18,15 @@ const assetsPageHTML = `<html><head>
</body></html>`
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 := `<script src="https://ad.mail.ru/static/sync-loader.js"></script>` +
`<script src="https://static.vk.ru/vkid/app.js"></script>`
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(`<script src="https://static.vk.ru/` + string(rune('a'+i%26)) + string(rune('a'+i/26)) + `.js"></script>`)
}
if got := len(parsePageAssets(html.String(), "")); got > assetsMaxCount {
if got := len(parsePageAssets(html.String())); got > assetsMaxCount {
t.Fatalf("assets = %d, want <= %d", got, assetsMaxCount)
}
}
@@ -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
}
}
// Бандл тянет тег <script> страницы captcha, а не fetch: другой Dest и no-cors.
body, err := s.doRaw(fhttp.MethodGet, scriptURL, nil, map[string]string{
"Accept": "*/*",
"Sec-Fetch-Dest": "script",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "same-site",
"Referer": s.pageOrigin + "/",
})
if err != nil {
return "", err
}
m := reCaptchaDebugInfo.FindSubmatch(body)
if len(m) < 2 {
return "", errors.New("debug_info constant not found in JS")
}
v := string(m[1])
debugInfoCache.Store(scriptURL, v)
return v, nil
}
func parseCaptchaPage(html string) (*captchaPage, error) {
pow, err := parsePowParams(html)
if err != nil {
return nil, err
}
page := &captchaPage{Pow: pow, Init: parseCaptchaInitGlobal(html)}
if m := reCaptchaScriptSrc.FindStringSubmatch(html); len(m) >= 2 {
page.ScriptURL = m[1]
debugInfo := parseCaptchaDebugInfo(html)
if debugInfo == "" {
return nil, errors.New("captcha debug_info not found on page")
}
return page, nil
return &captchaPage{Pow: pow, DebugInfo: debugInfo, Init: parseCaptchaInitGlobal(html)}, nil
}
func parseCaptchaDebugInfo(html string) string {
m := reCaptchaVKGlobal.FindStringIndex(html)
if m == nil {
return ""
}
block := balancedJSONObject(html[m[1]-1:])
if block == "" {
return ""
}
found := reCaptchaDebugInfo.FindAllStringSubmatch(block, -1)
if len(found) == 0 {
return ""
}
if len(found) > 1 {
Log.Warnf("[Captcha] window.vk holds %d uuid values, debug_info may be the wrong one", len(found))
}
return found[0][1]
}
// powSnippet - окно вокруг конверта PoW: при следующей смене разметки этого
@@ -236,3 +236,36 @@ func TestSetPageURLFallsBackToWidgetOrigin(t *testing.T) {
t.Fatalf("page = %q / %q", s.pageOrigin, s.pageURL)
}
}
func TestParseCaptchaDebugInfo(t *testing.T) {
tests := []struct {
name, html, want string
}{
{
name: "renamed key",
html: `<script>window.vk = {stDomain: "https://st.vk.ru", qqqqqqqq: "273cc83f-426f-4d98-9ce5-92490107e3a6", id: 0};</script>`,
want: "273cc83f-426f-4d98-9ce5-92490107e3a6",
},
{
name: "quoted keys of nested json are not candidates",
html: `<script>window.vk = {statsMeta: {"hash":"X84u4GhF","uuid":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}, k: "273cc83f-426f-4d98-9ce5-92490107e3a6"};</script>`,
want: "273cc83f-426f-4d98-9ce5-92490107e3a6",
},
{name: "no window.vk", html: `<script>var x = 1;</script>`},
{name: "no uuid in block", html: `<script>window.vk = {id: 0, logoutUrl: ""};</script>`},
}
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 := `<script>window.vk = {a: "273cc83f-426f-4d98-9ce5-92490107e3a6", b: "ec772ebb-0d69-4fa0-b974-904549c8a7d1"};</script>`
if got := parseCaptchaDebugInfo(html); got != "273cc83f-426f-4d98-9ce5-92490107e3a6" {
t.Fatalf("debug_info = %q", got)
}
}
+144 -35
View File
@@ -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(),
}),
}
}
@@ -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
File diff suppressed because one or more lines are too long
@@ -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"})