mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
fix(captcha): адаптация
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
|
||||
Reference in New Issue
Block a user