From b9642c613402f377174b6a43067dbcf0eaffd57d Mon Sep 17 00:00:00 2001 From: Moroka8 Date: Sat, 18 Apr 2026 19:50:29 +0700 Subject: [PATCH] feat: implement dynamic browser fingerprint capture and reuse Added SavedProfile struct and persistence logic in vk_profile.json. Implemented automatic interception of real browser telemetry (browser_fp, device JSON, headers) during manual captcha solves. Integrated saved profile reuse in automated solve flows to bypass BOT status. Restored dynamic MD5-based fingerprint generation as a fallback mechanism. --- client/main.go | 93 +++++++++++++++++++++---- client/manual_captcha.go | 60 ++++++++++++++-- client/profiles.go | 35 +++++++++- client/slider_captcha.go | 145 ++++++++++++++++++++++++++++++--------- 4 files changed, 278 insertions(+), 55 deletions(-) diff --git a/client/main.go b/client/main.go index 41d90ae..d9b0f4b 100644 --- a/client/main.go +++ b/client/main.go @@ -228,11 +228,14 @@ func applyBrowserProfileFhttp(req *fhttp.Request, profile Profile) { } func generateBrowserFp(profile Profile) string { - data := profile.UserAgent + profile.SecChUa + "1920x1080x24" + strconv.FormatInt(time.Now().UnixNano(), 10) + // Fallback logic for generating a fingerprint if no saved profile is available. + // This uses a simple MD5 hash of UA and a fixed resolution. + data := profile.UserAgent + profile.SecChUa + "1536x864x24" h := md5.Sum([]byte(data)) return hex.EncodeToString(h[:]) } +/* func generateFakeCursor() string { startX := 600 + rand.Intn(400) startY := 300 + rand.Intn(200) @@ -247,6 +250,48 @@ func generateFakeCursor() string { return "[" + strings.Join(points, ",") + "]" } +// generateCheckboxCursor simulates a mouse moving from a random starting position +// towards the VK captcha checkbox area, decelerating as it approaches the target. +// This looks more like a real click than either a stationary cursor or pure random jitter. +func generateCheckboxCursor() string { + type point struct { + X int `json:"x"` + Y int `json:"y"` + T int64 `json:"t"` + } + + // Target is roughly where VK renders the checkbox + targetX := 290 + rand.Intn(20) - 10 + targetY := 437 + rand.Intn(10) - 5 + + // Starting position: somewhere to the upper-right of the checkbox + startX := targetX + 200 + rand.Intn(300) + startY := targetY - 80 - rand.Intn(120) + + steps := 14 + rand.Intn(6) + startTime := time.Now().Add(-time.Duration(400+rand.Intn(600)) * time.Millisecond).UnixMilli() + + points := make([]point, 0, steps) + for i := 0; i < steps; i++ { + // Ease-out: fast at start, slow near target + t := float64(i) / float64(steps-1) + ease := 1 - (1-t)*(1-t) + x := startX + int(float64(targetX-startX)*ease) + rand.Intn(5) - 2 + y := startY + int(float64(targetY-startY)*ease) + rand.Intn(5) - 2 + dt := int64(15 + rand.Intn(25) + int(20*t)) // slower near target + startTime += dt + points = append(points, point{X: x, Y: y, T: startTime}) + } + + data, err := json.Marshal(points) + if err != nil { + return "[]" + } + return string(data) +} +*/ + + func getCustomNetDialer() net.Dialer { return net.Dialer{ Timeout: 20 * time.Second, @@ -399,6 +444,14 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in return "", fmt.Errorf("no redirect_uri for auto-solve") } + // Try to load saved profile from disk + var savedProfile *SavedProfile + if sp, err := LoadProfileFromDisk(); err == nil { + log.Printf("[STREAM %d] [Captcha] Using saved real browser profile", streamID) + savedProfile = sp + profile = sp.Profile // Use saved headers/UA + } + bootstrap, err := fetchCaptchaBootstrap(ctx, captchaErr.RedirectURI, client, profile) if err != nil { return "", fmt.Errorf("failed to fetch captcha bootstrap: %w", err) @@ -419,9 +472,10 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in client, profile, bootstrap.Settings, + savedProfile, // Pass savedProfile if available ) } else { - successToken, err = callCaptchaNotRobot(ctx, captchaErr.SessionToken, hash, streamID, client, profile) + successToken, err = callCaptchaNotRobot(ctx, captchaErr.SessionToken, hash, streamID, client, profile, savedProfile) } if err != nil { return "", fmt.Errorf("captchaNotRobot API failed: %w", err) @@ -478,7 +532,7 @@ func solvePoW(powInput string, difficulty int) string { return "" } -func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamID int, client tlsclient.HttpClient, profile Profile) (string, error) { +func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamID int, client tlsclient.HttpClient, profile Profile, savedProfile *SavedProfile) (string, error) { vkReq := func(method string, postData string) (map[string]interface{}, error) { reqURL := "https://api.vk.ru/method/" + method + "?v=5.131" parsedURL, err := neturl.Parse(reqURL) @@ -496,13 +550,11 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI applyBrowserProfileFhttp(req, profile) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "*/*") - req.Header.Set("Origin", "https://id.vk.ru") - req.Header.Set("Referer", "https://id.vk.ru/") - req.Header.Set("Sec-Fetch-Site", "same-site") + req.Header.Set("Origin", "https://api.vk.ru") + req.Header.Set("Referer", fmt.Sprintf("https://api.vk.ru/not_robot_captcha?domain=vk.com&session_token=%s&variant=popup&blank=1", sessionToken)) + req.Header.Set("Sec-Fetch-Site", "same-origin") req.Header.Set("Sec-Fetch-Mode", "cors") req.Header.Set("Sec-Fetch-Dest", "empty") - req.Header.Set("Sec-GPC", "1") - req.Header.Set("Priority", "u=1, i") httpResp, err := client.Do(req) if err != nil { @@ -523,7 +575,13 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI return resp, nil } - baseParams := fmt.Sprintf("session_token=%s&domain=vk.com&adFp=&access_token=", neturl.QueryEscape(sessionToken)) + adFpBytes := make([]byte, 16) + for i := range adFpBytes { + adFpBytes[i] = byte(rand.Intn(256)) + } + adFp := base64.RawURLEncoding.EncodeToString(adFpBytes)[:21] + + baseParams := fmt.Sprintf("session_token=%s&domain=vk.com&adFp=%s&access_token=", neturl.QueryEscape(sessionToken), neturl.QueryEscape(adFp)) log.Printf("[STREAM %d] [Captcha] Step 1/4: settings", streamID) if _, err := vkReq("captchaNotRobot.settings", baseParams); err != nil { @@ -535,6 +593,10 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI log.Printf("[STREAM %d] [Captcha] Step 2/4: componentDone", streamID) browserFp := generateBrowserFp(profile) deviceJSON := buildCaptchaDeviceJSON(profile) + if savedProfile != nil { + browserFp = savedProfile.BrowserFp + deviceJSON = savedProfile.DeviceJSON + } componentDoneData := baseParams + fmt.Sprintf("&browser_fp=%s&device=%s", browserFp, neturl.QueryEscape(deviceJSON)) if _, err := vkReq("captchaNotRobot.componentDone", componentDoneData); err != nil { @@ -544,15 +606,16 @@ func callCaptchaNotRobot(ctx context.Context, sessionToken, hash string, streamI time.Sleep(200 * time.Millisecond) log.Printf("[STREAM %d] [Captcha] Step 3/4: check", streamID) - cursorJSON := generateFakeCursor() + // The real browser sends an empty array for cursor on the first check. + cursorJSON := "[]" answer := base64.StdEncoding.EncodeToString([]byte("{}")) - // Dynamically generate debug_info to avoid static fingerprint bans - debugInfoBytes := md5.Sum([]byte(profile.UserAgent + strconv.FormatInt(time.Now().UnixNano(), 10))) - debugInfo := hex.EncodeToString(debugInfoBytes[:]) + // The real browser sends a static SHA-256 hash for debug_info. + // We use the exact one captured from the real browser's session. + debugInfo := "f3ef768dab7a20f574c6461f34e4257894d2a3c30a53d8727a3edaf7ab70847d" - connectionRtt := "[50,50,50,50,50,50,50,50,50,50]" - connectionDownlink := "[9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5,9.5]" + connectionRtt := "[250,250,250,250,250]" + connectionDownlink := "[1.45,1.45,1.45,1.45,1.45]" checkData := baseParams + fmt.Sprintf( "&accelerometer=%s&gyroscope=%s&motion=%s&cursor=%s&taps=%s&connectionRtt=%s&connectionDownlink=%s&browser_fp=%s&hash=%s&answer=%s&debug_info=%s", diff --git a/client/manual_captcha.go b/client/manual_captcha.go index ee2ab70..20a94a1 100644 --- a/client/manual_captcha.go +++ b/client/manual_captcha.go @@ -495,7 +495,7 @@ func runCaptchaServerAndWait(handler http.Handler, captchaURL string, keyCh <-ch fmt.Println("==============================================") fmt.Println() - log.Printf("[%s] Opening browser: %s", logPrefix, captchaURL) + log.Printf("[%s] Opening browser...", logPrefix) openBrowser(captchaURL) key := <-keyCh @@ -554,6 +554,52 @@ button{font-size:24px;padding:12px 32px;margin-top:12px;cursor:pointer} return runCaptchaServerAndWait(mux, localCaptchaOrigin(), keyCh, "captcha HTTP server error") } +type loggingTransport struct { + rt http.RoundTripper +} + +func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + isCaptchaRequest := req.Body != nil && (strings.Contains(req.URL.Path, "captchaNotRobot.check") || strings.Contains(req.URL.Path, "captchaNotRobot.componentDone")) + + if isCaptchaRequest { + b, _ := io.ReadAll(req.Body) + req.Body = io.NopCloser(bytes.NewReader(b)) + + if isDebug { + log.Printf("[Captcha Proxy] Real browser sent %s data: %s", req.URL.Path, string(b)) + for k, v := range req.Header { + log.Printf("[Captcha Proxy] Header (%s): %s = %s", req.URL.Path, k, strings.Join(v, ", ")) + } + } + + if strings.Contains(req.URL.Path, "captchaNotRobot.componentDone") || strings.Contains(req.URL.Path, "captchaNotRobot.check") { + parsedBody, _ := neturl.ParseQuery(string(b)) + device := parsedBody.Get("device") + browserFp := parsedBody.Get("browser_fp") + + // We only save it if device is present. componentDone usually has it. + if device != "" && browserFp != "" { + sp := SavedProfile{ + Profile: Profile{ + UserAgent: req.Header.Get("User-Agent"), + SecChUa: req.Header.Get("Sec-Ch-Ua"), + SecChUaMobile: req.Header.Get("Sec-Ch-Ua-Mobile"), + SecChUaPlatform: req.Header.Get("Sec-Ch-Ua-Platform"), + }, + DeviceJSON: device, + BrowserFp: browserFp, + } + if err := SaveProfileToDisk(sp); err != nil { + log.Printf("[Captcha Proxy] Failed to save browser profile: %v", err) + } else { + log.Printf("[Captcha Proxy] Successfully intercepted and saved real browser profile!") + } + } + } + } + return t.rt.RoundTrip(req) +} + func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string, error) { keyCh := make(chan string, 1) @@ -562,7 +608,7 @@ func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string, return "", fmt.Errorf("invalid redirect URI: %v", err) } - transport := newCaptchaProxyTransport(dialer) + transport := &loggingTransport{rt: newCaptchaProxyTransport(dialer)} proxy := &httputil.ReverseProxy{ Transport: transport, @@ -580,7 +626,7 @@ func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string, if res.StatusCode >= 300 && res.StatusCode < 400 { if loc := res.Header.Get("Location"); loc != "" { - log.Printf("[Captcha Proxy] Redirecting to: %s", loc) + // Don't log the full redirect URL to keep console clean if rewritten, ok := rewriteProxyRedirectLocation(loc, targetURL); ok { res.Header.Set("Location", rewritten) } else { @@ -591,7 +637,9 @@ func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string, contentType := res.Header.Get("Content-Type") contentEncoding := res.Header.Get("Content-Encoding") - log.Printf("[Captcha Proxy] %s %d | Content-Type: %q, Encoding: %q", res.Request.Method, res.StatusCode, contentType, contentEncoding) + if isDebug { + log.Printf("[Captcha Proxy] %s %d | Content-Type: %q, Encoding: %q", res.Request.Method, res.StatusCode, contentType, contentEncoding) + } shouldInspectBody := strings.Contains(contentType, "text/html") || strings.Contains(contentType, "application/xhtml+xml") || @@ -723,9 +771,9 @@ func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string, }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - log.Printf("[Captcha Proxy] HTTP %s %s", r.Method, r.URL.String()) + log.Printf("[Captcha Proxy] HTTP %s %s", r.Method, r.URL.Path) if r.URL.Path == "/" && targetURL.Path != "" && targetURL.Path != "/" && r.URL.RawQuery == "" { - log.Printf("[Captcha Proxy] Redirecting ROOT to: %s", localCaptchaURLForTarget(targetURL)) + // Don't log the full redirect URL to keep console clean http.Redirect(w, r, localCaptchaURLForTarget(targetURL), http.StatusTemporaryRedirect) return } diff --git a/client/profiles.go b/client/profiles.go index 01d4f0c..b252e3a 100644 --- a/client/profiles.go +++ b/client/profiles.go @@ -1,7 +1,9 @@ package main import ( + "encoding/json" "math/rand" + "os" ) type Profile struct { @@ -11,8 +13,37 @@ type Profile struct { SecChUaPlatform string } +type SavedProfile struct { + Profile + DeviceJSON string + BrowserFp string +} + +const profileFile = "vk_profile.json" + +func LoadProfileFromDisk() (*SavedProfile, error) { + data, err := os.ReadFile(profileFile) + if err != nil { + return nil, err + } + var sp SavedProfile + if err := json.Unmarshal(data, &sp); err != nil { + return nil, err + } + return &sp, nil +} + +func SaveProfileToDisk(sp SavedProfile) error { + data, err := json.MarshalIndent(sp, "", " ") + if err != nil { + return err + } + return os.WriteFile(profileFile, data, 0644) +} + // profiles contain paired User-Agent and Client Hints strings to harden bot detection. -var profile = []Profile{ +// Used only as a fallback if no saved profile exists (which we shouldn't really use for check anymore). +var profileList = []Profile{ // Windows Chrome { UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", @@ -78,5 +109,5 @@ var profile = []Profile{ // getRandomProfile returns a paired User-Agent and Client Hints profile. func getRandomProfile() Profile { - return profile[rand.Intn(len(profile))] + return profileList[rand.Intn(len(profileList))] } diff --git a/client/slider_captcha.go b/client/slider_captcha.go index b01e1c3..a159df8 100644 --- a/client/slider_captcha.go +++ b/client/slider_captcha.go @@ -11,6 +11,7 @@ import ( _ "image/jpeg" "io" "log" + "math/rand" neturl "net/url" "regexp" "sort" @@ -21,9 +22,7 @@ import ( fhttp "github.com/bogdanfinn/fhttp" tlsclient "github.com/bogdanfinn/tls-client" ) - const ( - captchaDebugInfo = "1d3e9babfd3a74f4588bf90cf5c30d3e8e89a0e2a4544da8de8bbf4d78a32f5c" sliderCaptchaType = "slider" defaultSliderAttempts = 4 ) @@ -36,6 +35,17 @@ type captchaNotRobotSession struct { client tlsclient.HttpClient profile Profile browserFp string + adFp string + savedProfile *SavedProfile +} + +func generateAdFp() string { + b := make([]byte, 16) + // simple random bytes (or any pseudo-random logic that matches the 21-char base64 footprint) + for i := range b { + b[i] = byte(rand.Intn(256)) + } + return base64.RawURLEncoding.EncodeToString(b)[:21] } type captchaSettingsResponse struct { @@ -68,14 +78,12 @@ type captchaBootstrap struct { Settings *captchaSettingsResponse } -func newCaptchaNotRobotSession( - ctx context.Context, - sessionToken string, - hash string, - streamID int, - client tlsclient.HttpClient, - profile Profile, -) *captchaNotRobotSession { +func newCaptchaNotRobotSession(ctx context.Context, sessionToken, hash string, streamID int, client tlsclient.HttpClient, profile Profile, savedProfile *SavedProfile) *captchaNotRobotSession { + browserFp := generateBrowserFp(profile) + if savedProfile != nil { + browserFp = savedProfile.BrowserFp + } + return &captchaNotRobotSession{ ctx: ctx, sessionToken: sessionToken, @@ -83,7 +91,9 @@ func newCaptchaNotRobotSession( streamID: streamID, client: client, profile: profile, - browserFp: generateBrowserFp(profile), + browserFp: browserFp, + adFp: generateAdFp(), + savedProfile: savedProfile, } } @@ -91,7 +101,7 @@ func (s *captchaNotRobotSession) baseValues() neturl.Values { values := neturl.Values{} values.Set("session_token", s.sessionToken) values.Set("domain", "vk.com") - values.Set("adFp", "") + values.Set("adFp", s.adFp) values.Set("access_token", "") return values } @@ -104,6 +114,16 @@ func (s *captchaNotRobotSession) request(method string, values neturl.Values) (m return nil, err } + // Match the headers that the real VK captcha JS sends, same as callCaptchaNotRobot. + applyBrowserProfileFhttp(req, s.profile) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "*/*") + req.Header.Set("Origin", "https://api.vk.ru") + req.Header.Set("Referer", fmt.Sprintf("https://api.vk.ru/not_robot_captcha?domain=vk.com&session_token=%s&variant=popup&blank=1", s.sessionToken)) + req.Header.Set("Sec-Fetch-Site", "same-origin") + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Sec-Fetch-Dest", "empty") + httpResp, err := s.client.Do(req) if err != nil { return nil, err @@ -135,7 +155,12 @@ func (s *captchaNotRobotSession) requestSettings() (*captchaSettingsResponse, er func (s *captchaNotRobotSession) requestComponentDone() error { values := s.baseValues() values.Set("browser_fp", s.browserFp) - values.Set("device", buildCaptchaDeviceJSON(s.profile)) + + deviceJSON := buildCaptchaDeviceJSON(s.profile) + if s.savedProfile != nil { + deviceJSON = s.savedProfile.DeviceJSON + } + values.Set("device", deviceJSON) resp, err := s.request("captchaNotRobot.componentDone", values) if err != nil { @@ -153,7 +178,7 @@ func (s *captchaNotRobotSession) requestComponentDone() error { } func (s *captchaNotRobotSession) requestCheckboxCheck() (*captchaCheckResult, error) { - return s.requestCheck(generateSliderCursor(0, 1), base64.StdEncoding.EncodeToString([]byte("{}"))) + return s.requestCheck("[]", base64.StdEncoding.EncodeToString([]byte("{}"))) } func (s *captchaNotRobotSession) requestSliderContent(sliderSettings string) (*sliderCaptchaContent, error) { @@ -169,28 +194,68 @@ func (s *captchaNotRobotSession) requestSliderContent(sliderSettings string) (*s return parseSliderCaptchaContentResponse(resp) } +// requestSliderContentWithFallback tries to get slider content using multiple strategies: +// first with the provided captcha_settings, then without it (and vice versa). +// VK sometimes reports show_type=checkbox in settings but actually serves slider content, +// so we need to probe both variants. +func (s *captchaNotRobotSession) requestSliderContentWithFallback(sliderSettings string, streamID int) (*sliderCaptchaContent, error) { + type attempt struct { + settings string + desc string + } + var attempts []attempt + if sliderSettings != "" { + attempts = []attempt{ + {settings: sliderSettings, desc: "with captcha_settings"}, + {settings: "", desc: "without captcha_settings"}, + } + } else { + // We have no slider settings; just one attempt without captcha_settings + attempts = []attempt{ + {settings: "", desc: "without captcha_settings"}, + } + } + + var lastErr error + for _, a := range attempts { + log.Printf("[STREAM %d] [Captcha] Requesting slider content (%s)...", streamID, a.desc) + content, err := s.requestSliderContent(a.settings) + if err == nil { + return content, nil + } + log.Printf("[STREAM %d] [Captcha] getContent failed (%s): %v", streamID, a.desc, err) + lastErr = err + } + return nil, lastErr +} + func (s *captchaNotRobotSession) requestSliderCheck(activeSteps []int, candidateIndex int, candidateCount int) (*captchaCheckResult, error) { answer, err := encodeSliderAnswer(activeSteps) if err != nil { return nil, err } - return s.requestCheck(generateSliderCursor(candidateIndex, candidateCount), answer) + return s.requestCheck("[]", answer) } func (s *captchaNotRobotSession) requestCheck(cursor string, answer string) (*captchaCheckResult, error) { values := s.baseValues() + + // The real browser sends a static SHA-256 hash for debug_info. + // We use the exact one captured from the real browser's session. + debugInfo := "f3ef768dab7a20f574c6461f34e4257894d2a3c30a53d8727a3edaf7ab70847d" + values.Set("accelerometer", "[]") values.Set("gyroscope", "[]") values.Set("motion", "[]") values.Set("cursor", cursor) values.Set("taps", "[]") - values.Set("connectionRtt", "[]") - values.Set("connectionDownlink", "[]") + values.Set("connectionRtt", "[250,250,250,250,250]") + values.Set("connectionDownlink", "[1.45,1.45,1.45,1.45,1.45]") values.Set("browser_fp", s.browserFp) values.Set("hash", s.hash) values.Set("answer", answer) - values.Set("debug_info", captchaDebugInfo) + values.Set("debug_info", debugInfo) resp, err := s.request("captchaNotRobot.check", values) if err != nil { @@ -214,8 +279,9 @@ func callCaptchaNotRobotWithSliderPOC( client tlsclient.HttpClient, profile Profile, initialSettings *captchaSettingsResponse, + savedProfile *SavedProfile, ) (string, error) { - session := newCaptchaNotRobotSession(ctx, sessionToken, hash, streamID, client, profile) + session := newCaptchaNotRobotSession(ctx, sessionToken, hash, streamID, client, profile, savedProfile) log.Printf("[STREAM %d] [Captcha] Step 1/4: settings", streamID) settingsResp, err := session.requestSettings() @@ -265,23 +331,24 @@ func callCaptchaNotRobotWithSliderPOC( log.Printf("[STREAM %d] [Captcha] Trying experimental slider solver...", streamID) } - sliderContent, err := session.requestSliderContent(sliderSettings) + // After check returns BOT, a real browser renders the slider widget and calls + // componentDone again to signal "slider component is now loaded". Without this, + // VK refuses getContent with ERROR because it expects the widget lifecycle. + log.Printf("[STREAM %d] [Captcha] Re-registering slider component before getContent...", streamID) + time.Sleep(300 * time.Millisecond) + if err := session.requestComponentDone(); err != nil { + // Non-fatal: log and continue — getContent may still succeed. + log.Printf("[STREAM %d] [Captcha] Warning: slider componentDone failed: %v", streamID, err) + } + time.Sleep(200 * time.Millisecond) + + sliderContent, err := session.requestSliderContentWithFallback(sliderSettings, streamID) if err != nil { log.Printf( - "[STREAM %d] [Captcha] Slider getContent failed (status: %v). Trying to solve as a checkbox instead...", + "[STREAM %d] [Captcha] All slider getContent attempts failed: %v", streamID, err, ) - time.Sleep(300 * time.Millisecond) - finalCheck, err2 := session.requestCheckboxCheck() - if err2 == nil && finalCheck.Status == "OK" { - if finalCheck.SuccessToken == "" { - return "", fmt.Errorf("success_token not found in fallback check") - } - log.Printf("[STREAM %d] [Captcha] Fallback checkbox check succeeded!", streamID) - session.requestEndSession() - return finalCheck.SuccessToken, nil - } return "", fmt.Errorf("check status: %s (slider getContent failed: %w)", initialCheck.Status, err) } @@ -316,8 +383,10 @@ func callCaptchaNotRobotWithSliderPOC( } func buildCaptchaDeviceJSON(profile Profile) string { + // Fallback device JSON if no saved profile is available. + // We include the User-Agent from the current profile to maintain some consistency. return fmt.Sprintf( - `{"screenWidth":1920,"screenHeight":1080,"screenAvailWidth":1920,"screenAvailHeight":1040,"innerWidth":1920,"innerHeight":969,"devicePixelRatio":1,"language":"en-US","languages":["en-US"],"webdriver":false,"hardwareConcurrency":8,"deviceMemory":8,"connectionEffectiveType":"4g","notificationsPermission":"default","userAgent":"%s","platform":"Win32"}`, + `{"screenWidth":1536,"screenHeight":864,"screenAvailWidth":1536,"screenAvailHeight":816,"innerWidth":1536,"innerHeight":730,"devicePixelRatio":1.25,"language":"ru-RU","languages":["ru-RU","ru","en-US","en"],"webdriver":false,"hardwareConcurrency":8,"deviceMemory":8,"connectionEffectiveType":"4g","notificationsPermission":"prompt","userAgent":"%s"}`, profile.UserAgent, ) } @@ -528,6 +597,15 @@ func parseSliderCaptchaContentResponse(resp map[string]interface{}) (*sliderCapt status, _ := respObj["status"].(string) if status != "OK" { + // Log all fields from the response to help diagnose why VK rejected getContent. + var debugFields []string + for k, v := range respObj { + if k != "image" { // skip base64 image blob + debugFields = append(debugFields, fmt.Sprintf("%s=%v", k, v)) + } + } + sort.Strings(debugFields) + log.Printf("[Captcha] getContent ERROR response fields: %s", strings.Join(debugFields, " ")) return nil, fmt.Errorf("slider getContent status: %s", status) } @@ -862,6 +940,7 @@ func absDiff(left uint32, right uint32) int64 { return int64(right - left) } +/* func generateSliderCursor(candidateIndex int, candidateCount int) string { return buildSliderCursor(candidateIndex, candidateCount, time.Now().Add(-220*time.Millisecond).UnixMilli()) } @@ -898,6 +977,8 @@ func buildSliderCursor(candidateIndex int, candidateCount int, startTime int64) } return string(data) } +*/ + func trySliderCaptchaCandidates( candidates []sliderCandidate,