Browse Source

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.
pull/162/head
Moroka8 2 months ago
parent
commit
b9642c6134
  1. 93
      client/main.go
  2. 60
      client/manual_captcha.go
  3. 35
      client/profiles.go
  4. 145
      client/slider_captcha.go

93
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",

60
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}</style>
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
}

35
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))]
}

145
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,

Loading…
Cancel
Save