/* SPDX-License-Identifier: Apache-2.0 * * Copyright © 2026 WireGuard LLC. All Rights Reserved. */ package main /* #include // These are JNI functions from Android, they won't work in standalone exe // But we keep the signature for consistency with libwg-go if asked // extern const char* requestCaptcha(const char* redirect_uri); */ // import "C" // Disabled cgo for standalone compatibility unless strictly needed import ( "bytes" "context" "crypto/sha256" "crypto/tls" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "image" "image/color" _ "image/jpeg" "io" "log" "math/rand" "net" "net/http" "net/url" "regexp" "sort" "strconv" "strings" "sync" "syscall" "time" ) // --- Compatibility Shims for Standalone vk-turn-proxy --- func turnLog(format string, v ...interface{}) { log.Printf(format, v...) } // Simple host cache for standalone var hostCache = &simpleHostCache{} type simpleHostCache struct{} func (s *simpleHostCache) Resolve(ctx context.Context, host string) (string, error) { ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) if err != nil { return "", err } if len(ips) == 0 { return "", fmt.Errorf("no IP found for %s", host) } return ips[0].IP.String(), nil } // No-op protectControl for standalone func protectControl(network, address string, c syscall.RawConn) error { return nil } // --- Original libwg-go code (adapted) --- // VkCaptchaError represents a VK captcha error type VkCaptchaError struct { ErrorCode int ErrorMsg string CaptchaSid string CaptchaImg string RedirectUri string IsSoundCaptchaAvailable bool SessionToken string CaptchaTs string // captcha_ts from error CaptchaAttempt string // captcha_attempt from error } // ParseVkCaptchaError parses a VK error response into VkCaptchaError func ParseVkCaptchaError(errData map[string]interface{}) *VkCaptchaError { codeFloat, _ := errData["error_code"].(float64) code := int(codeFloat) redirectUri, _ := errData["redirect_uri"].(string) captchaSid, _ := errData["captcha_sid"].(string) captchaImg, _ := errData["captcha_img"].(string) errorMsg, _ := errData["error_msg"].(string) // Extract session_token from redirect_uri var sessionToken string if redirectUri != "" { if parsed, err := url.Parse(redirectUri); err == nil { sessionToken = parsed.Query().Get("session_token") } } isSound, _ := errData["is_sound_captcha_available"].(bool) // captcha_ts can be float64 (scientific notation) or string var captchaTs string if tsFloat, ok := errData["captcha_ts"].(float64); ok { captchaTs = fmt.Sprintf("%.0f", tsFloat) } else if tsStr, ok := errData["captcha_ts"].(string); ok { captchaTs = tsStr } // captcha_attempt is usually a float64 var captchaAttempt string if attFloat, ok := errData["captcha_attempt"].(float64); ok { captchaAttempt = fmt.Sprintf("%.0f", attFloat) } else if attStr, ok := errData["captcha_attempt"].(string); ok { captchaAttempt = attStr } return &VkCaptchaError{ ErrorCode: code, ErrorMsg: errorMsg, CaptchaSid: captchaSid, CaptchaImg: captchaImg, RedirectUri: redirectUri, IsSoundCaptchaAvailable: isSound, SessionToken: sessionToken, CaptchaTs: captchaTs, CaptchaAttempt: captchaAttempt, } } // IsCaptchaError checks if the error data is a Not Robot Captcha error func (e *VkCaptchaError) IsCaptchaError() bool { return e.ErrorCode == 14 && e.RedirectUri != "" && e.SessionToken != "" } // captchaMutex serializes captcha solving to avoid multiple concurrent attempts var captchaMutex sync.Mutex // SolveVkCaptcha solves the VK Not Robot Captcha and returns success_token // First tries automatic solution, falls back to manual solution if it fails func SolveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError) (string, error) { // Serialize captcha solving to avoid multiple concurrent attempts captchaMutex.Lock() defer captchaMutex.Unlock() turnLog("[Captcha] Solving Not Robot Captcha...") // Step 1: Try automatic solution turnLog("[Captcha] Attempting automatic solution...") successToken, err := solveVkCaptchaAutomatic(ctx, captchaErr) if err == nil && successToken != "" { turnLog("[Captcha] Automatic solution SUCCESS!") return successToken, nil } turnLog("[Captcha] Automatic solution FAILED: %v", err) // Step 2: Fall back to manual solving turnLog("[Captcha] Triggering manual captcha fallback...") if captchaErr.RedirectUri != "" { return solveCaptchaViaProxy(captchaErr.RedirectUri) } else if captchaErr.CaptchaImg != "" { return solveCaptchaViaHTTP(captchaErr.CaptchaImg) } return "", fmt.Errorf("no more solve modes available") } // solveVkCaptchaAutomatic performs the automatic captcha solving without UI func solveVkCaptchaAutomatic(ctx context.Context, captchaErr *VkCaptchaError) (string, error) { sessionToken := captchaErr.SessionToken if sessionToken == "" { return "", fmt.Errorf("no session_token in redirect_uri") } // Step 1: Fetch the captcha HTML page to get powInput bootstrap, err := fetchCaptchaBootstrap(ctx, captchaErr.RedirectUri) if err != nil { return "", fmt.Errorf("failed to fetch captcha bootstrap: %w", err) } turnLog("[Captcha] PoW input: %s, difficulty: %d", bootstrap.PowInput, bootstrap.Difficulty) // Step 2: Solve PoW hash := solvePoW(bootstrap.PowInput, bootstrap.Difficulty) turnLog("[Captcha] PoW solved: hash=%s", hash) // Step 3: Call captchaNotRobot API with slider POC support successToken, err := callCaptchaNotRobotWithSliderPOC(ctx, sessionToken, hash, bootstrap.Settings) if err != nil { return "", fmt.Errorf("captchaNotRobot API failed: %w", err) } turnLog("[Captcha] Success! Got success_token") return successToken, nil } // fetchCaptchaBootstrap fetches the captcha HTML page and extracts PoW input, difficulty, and settings func fetchCaptchaBootstrap(ctx context.Context, redirectUri string) (*captchaBootstrap, error) { parsedURL, err := url.Parse(redirectUri) if err != nil { return nil, fmt.Errorf("failed to parse redirect_uri: %w", err) } domain := parsedURL.Hostname() resolvedIP, err := hostCache.Resolve(ctx, domain) if err != nil { return nil, fmt.Errorf("DNS resolution failed for %s: %w", domain, err) } port := parsedURL.Port() if port == "" { port = "443" } ipURL := "https://" + resolvedIP + ":" + port + parsedURL.Path if parsedURL.RawQuery != "" { ipURL += "?" + parsedURL.RawQuery } req, err := http.NewRequestWithContext(ctx, "GET", ipURL, nil) if err != nil { return nil, err } req.Host = domain req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") req.Header.Set("Accept-Language", "en-US,en;q=0.9") client := &http.Client{ Timeout: 20 * time.Second, Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, // Control: protectControl, // Disabled for standalone }).DialContext, TLSClientConfig: &tls.Config{ ServerName: domain, }, }, } resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } html := string(body) bootstrap, err := parseCaptchaBootstrapHTML(html) if err != nil { return nil, err } return bootstrap, nil } // solvePoW finds nonce where SHA-256(powInput + nonce) starts with '0' * difficulty func solvePoW(powInput string, difficulty int) string { target := strings.Repeat("0", difficulty) for nonce := 1; nonce <= 10000000; nonce++ { data := powInput + strconv.Itoa(nonce) hash := sha256.Sum256([]byte(data)) hexHash := hex.EncodeToString(hash[:]) if strings.HasPrefix(hexHash, target) { return hexHash } } // Fallback: should not happen with difficulty <= 3 return "" } const ( sliderCaptchaType = "slider" defaultSliderAttempts = 4 ) // captchaBootstrap holds parsed captcha bootstrap data type captchaBootstrap struct { PowInput string Difficulty int Settings *captchaSettingsResponse } // captchaSettingsResponse holds captcha settings from VK API type captchaSettingsResponse struct { ShowCaptchaType string SettingsByType map[string]string } // captchaCheckResult holds the result of a captcha check request type captchaCheckResult struct { Status string SuccessToken string ShowCaptchaType string } // sliderCaptchaContent holds decoded slider captcha content type sliderCaptchaContent struct { Image image.Image Size int Steps []int Attempts int } // sliderCandidate represents a ranked slider candidate type sliderCandidate struct { Index int ActiveSteps []int Score int64 } // captchaNotRobotSession represents a captcha solving session type captchaNotRobotSession struct { ctx context.Context sessionToken string hash string browserFp string } // newCaptchaNotRobotSession creates a new captcha solving session func newCaptchaNotRobotSession( ctx context.Context, sessionToken string, hash string, ) *captchaNotRobotSession { // Generate random browser fingerprint browserFp := fmt.Sprintf("%032x", randInt63()) return &captchaNotRobotSession{ ctx: ctx, sessionToken: sessionToken, hash: hash, browserFp: browserFp, } } // baseValues returns base URL values for API requests func (s *captchaNotRobotSession) baseValues() url.Values { values := url.Values{} values.Set("session_token", s.sessionToken) values.Set("domain", "vk.com") values.Set("adFp", "") values.Set("access_token", "") return values } // request makes a VK API request func (s *captchaNotRobotSession) request(method string, values url.Values) (map[string]interface{}, error) { reqURL := "https://api.vk.ru/method/" + method + "?v=5.131" parsedURL, err := url.Parse(reqURL) if err != nil { return nil, err } domain := parsedURL.Hostname() resolvedIP, err := hostCache.Resolve(s.ctx, domain) if err != nil { return nil, fmt.Errorf("DNS resolution failed for %s: %w", domain, err) } port := parsedURL.Port() if port == "" { port = "443" } ipURL := "https://" + resolvedIP + ":" + port + parsedURL.Path if parsedURL.RawQuery != "" { ipURL += "?" + parsedURL.RawQuery } req, err := http.NewRequestWithContext(s.ctx, "POST", ipURL, strings.NewReader(values.Encode())) if err != nil { return nil, err } req.Host = domain req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "*/*") req.Header.Set("Accept-Language", "en-US,en;q=0.9") req.Header.Set("Origin", "https://vk.ru") req.Header.Set("Referer", "https://vk.ru/") req.Header.Set("sec-ch-ua-platform", "\"Linux\"") req.Header.Set("sec-ch-ua", "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"") req.Header.Set("sec-ch-ua-mobile", "?0") req.Header.Set("DNT", "1") req.Header.Set("Sec-Fetch-Site", "same-site") req.Header.Set("Sec-Fetch-Mode", "cors") req.Header.Set("Sec-Fetch-Dest", "empty") req.Header.Set("Sec-GPC", "1") client := &http.Client{ Timeout: 20 * time.Second, Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, // Control: protectControl, }).DialContext, TLSClientConfig: &tls.Config{ ServerName: domain, }, }, } resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var respMap map[string]interface{} if err := json.Unmarshal(body, &respMap); err != nil { return nil, err } return respMap, nil } // requestSettings fetches captcha settings from VK API func (s *captchaNotRobotSession) requestSettings() (*captchaSettingsResponse, error) { resp, err := s.request("captchaNotRobot.settings", s.baseValues()) if err != nil { return nil, fmt.Errorf("settings failed: %w", err) } return parseCaptchaSettingsResponse(resp) } // requestComponentDone marks the component as done func (s *captchaNotRobotSession) requestComponentDone() error { values := s.baseValues() values.Set("browser_fp", s.browserFp) values.Set("device", buildCaptchaDeviceJSON()) resp, err := s.request("captchaNotRobot.componentDone", values) if err != nil { return fmt.Errorf("componentDone failed: %w", err) } respObj, ok := resp["response"].(map[string]interface{}) if ok { if status, _ := respObj["status"].(string); status != "" && status != "OK" { return fmt.Errorf("componentDone status: %s", status) } } return nil } // requestCheckboxCheck performs a checkbox-style captcha check func (s *captchaNotRobotSession) requestCheckboxCheck() (*captchaCheckResult, error) { return s.requestCheck("[]", base64.StdEncoding.EncodeToString([]byte("{}"))) } // requestSliderContent fetches slider captcha content func (s *captchaNotRobotSession) requestSliderContent(sliderSettings string) (*sliderCaptchaContent, error) { values := s.baseValues() if sliderSettings != "" { values.Set("captcha_settings", sliderSettings) } resp, err := s.request("captchaNotRobot.getContent", values) if err != nil { return nil, fmt.Errorf("getContent failed: %w", err) } return parseSliderCaptchaContentResponse(resp) } // requestSliderCheck performs a slider captcha check 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) } // requestCheck performs the main captcha check request func (s *captchaNotRobotSession) requestCheck(cursor string, answer string) (*captchaCheckResult, error) { values := s.baseValues() values.Set("accelerometer", "[]") values.Set("gyroscope", "[]") values.Set("motion", "[]") values.Set("cursor", cursor) values.Set("taps", "[]") values.Set("connectionRtt", "[]") values.Set("connectionDownlink", "[]") values.Set("browser_fp", s.browserFp) values.Set("hash", s.hash) values.Set("answer", answer) values.Set("debug_info", "d44f534ce8deb56ba20be52e05c433309b49ee4d2a70602deeb17a1954257785") resp, err := s.request("captchaNotRobot.check", values) if err != nil { return nil, fmt.Errorf("check failed: %w", err) } return parseCaptchaCheckResult(resp) } // requestEndSession ends the captcha session func (s *captchaNotRobotSession) requestEndSession() { turnLog("[Captcha] Step 4/4: endSession") if _, err := s.request("captchaNotRobot.endSession", s.baseValues()); err != nil { turnLog("[Captcha] Warning: endSession failed: %v", err) } } // callCaptchaNotRobotWithSliderPOC solves captcha with slider POC support func callCaptchaNotRobotWithSliderPOC( ctx context.Context, sessionToken string, hash string, initialSettings *captchaSettingsResponse, ) (string, error) { session := newCaptchaNotRobotSession(ctx, sessionToken, hash) turnLog("[Captcha] Step 1/4: settings") settingsResp, err := session.requestSettings() if err != nil { return "", err } settingsResp = mergeCaptchaSettings(settingsResp, initialSettings) time.Sleep(200 * time.Millisecond) turnLog("[Captcha] Step 2/4: componentDone") if err := session.requestComponentDone(); err != nil { return "", err } time.Sleep(200 * time.Millisecond) turnLog("[Captcha] Step 3/4: check") initialCheck, err := session.requestCheckboxCheck() if err != nil { return "", err } if initialCheck.Status == "OK" { if initialCheck.SuccessToken == "" { return "", fmt.Errorf("success_token not found") } time.Sleep(200 * time.Millisecond) session.requestEndSession() return initialCheck.SuccessToken, nil } sliderSettings, hasSlider := settingsResp.SettingsByType[sliderCaptchaType] turnLog( "[Captcha] Checkbox-style check returned status=%s (settings show_type=%q, check show_type=%q, available_types=%s)", initialCheck.Status, settingsResp.ShowCaptchaType, initialCheck.ShowCaptchaType, describeCaptchaTypes(settingsResp.SettingsByType), ) if !hasSlider { turnLog( "[Captcha] Slider settings not found in settings response. Trying getContent without captcha_settings...", ) } else { turnLog("[Captcha] Trying experimental slider solver...") } sliderContent, err := session.requestSliderContent(sliderSettings) if err != nil { return "", fmt.Errorf("check status: %s (slider getContent failed: %w)", initialCheck.Status, err) } candidates, err := rankSliderCandidates(sliderContent.Image, sliderContent.Size, sliderContent.Steps) if err != nil { return "", err } turnLog( "[Captcha] Ranked %d slider positions locally; submitting top %d based on attempt budget %d", len(candidates), minInt(sliderContent.Attempts, len(candidates)), sliderContent.Attempts, ) successToken, err := trySliderCaptchaCandidates(candidates, sliderContent.Attempts, func(candidate sliderCandidate) (*captchaCheckResult, error) { turnLog( "[Captcha] Slider guess position=%d score=%d", candidate.Index, candidate.Score, ) return session.requestSliderCheck(candidate.ActiveSteps, candidate.Index, len(candidates)) }) if err != nil { return "", err } time.Sleep(200 * time.Millisecond) session.requestEndSession() return successToken, nil } // buildCaptchaDeviceJSON builds device information JSON func buildCaptchaDeviceJSON() string { 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"}`, ) } // parseCaptchaSettingsResponse parses captcha settings from API response func parseCaptchaSettingsResponse(resp map[string]interface{}) (*captchaSettingsResponse, error) { respObj, ok := resp["response"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("invalid settings response: %v", resp) } settings := &captchaSettingsResponse{ SettingsByType: make(map[string]string), } settings.ShowCaptchaType, _ = respObj["show_captcha_type"].(string) rawSettings, ok := expandCaptchaSettings(respObj["captcha_settings"]) if !ok { return settings, nil } for _, rawItem := range rawSettings { item, ok := rawItem.(map[string]interface{}) if !ok { continue } captchaType, _ := item["type"].(string) if captchaType == "" { continue } normalized, err := normalizeCaptchaSettings(item["settings"]) if err != nil { return nil, fmt.Errorf("invalid captcha_settings for %s: %w", captchaType, err) } settings.SettingsByType[captchaType] = normalized } return settings, nil } // parseCaptchaBootstrapHTML parses HTML page to extract PoW input and settings func parseCaptchaBootstrapHTML(html string) (*captchaBootstrap, error) { powInputRe := regexp.MustCompile(`const\s+powInput\s*=\s*"([^"]+)"`) powInputMatch := powInputRe.FindStringSubmatch(html) if len(powInputMatch) < 2 { return nil, fmt.Errorf("powInput not found in captcha HTML") } difficulty := 2 for _, expr := range []*regexp.Regexp{ regexp.MustCompile(`startsWith\('0'\.repeat\((\d+)\)\)`), regexp.MustCompile(`const\s+difficulty\s*=\s*(\d+)`), } { if match := expr.FindStringSubmatch(html); len(match) >= 2 { if parsed, err := strconv.Atoi(match[1]); err == nil { difficulty = parsed break } } } settings, err := parseCaptchaSettingsFromHTML(html) if err != nil { return nil, err } return &captchaBootstrap{ PowInput: powInputMatch[1], Difficulty: difficulty, Settings: settings, }, nil } // parseCaptchaSettingsFromHTML parses captcha settings from HTML window.init func parseCaptchaSettingsFromHTML(html string) (*captchaSettingsResponse, error) { initRe := regexp.MustCompile(`(?s)window\.init\s*=\s*(\{.*?})\s*;\s*window\.lang`) initMatch := initRe.FindStringSubmatch(html) if len(initMatch) < 2 { return &captchaSettingsResponse{SettingsByType: make(map[string]string)}, nil } var initPayload struct { Data struct { ShowCaptchaType string `json:"show_captcha_type"` CaptchaSettings interface{} `json:"captcha_settings"` } `json:"data"` } if err := json.Unmarshal([]byte(initMatch[1]), &initPayload); err != nil { return nil, fmt.Errorf("parse window.init captcha data: %w", err) } return parseCaptchaSettingsResponse(map[string]interface{}{ "response": map[string]interface{}{ "show_captcha_type": initPayload.Data.ShowCaptchaType, "captcha_settings": initPayload.Data.CaptchaSettings, }, }) } // mergeCaptchaSettings merges two captcha settings responses func mergeCaptchaSettings(primary *captchaSettingsResponse, fallback *captchaSettingsResponse) *captchaSettingsResponse { if primary == nil { return cloneCaptchaSettings(fallback) } if primary.SettingsByType == nil { primary.SettingsByType = make(map[string]string) } if fallback == nil { return primary } if primary.ShowCaptchaType == "" { primary.ShowCaptchaType = fallback.ShowCaptchaType } for captchaType, settings := range fallback.SettingsByType { if _, exists := primary.SettingsByType[captchaType]; !exists { primary.SettingsByType[captchaType] = settings } } return primary } // cloneCaptchaSettings clones a captcha settings response func cloneCaptchaSettings(src *captchaSettingsResponse) *captchaSettingsResponse { if src == nil { return nil } cloned := &captchaSettingsResponse{ ShowCaptchaType: src.ShowCaptchaType, SettingsByType: make(map[string]string, len(src.SettingsByType)), } for captchaType, settings := range src.SettingsByType { cloned.SettingsByType[captchaType] = settings } return cloned } // expandCaptchaSettings expands raw captcha settings into a slice func expandCaptchaSettings(raw interface{}) ([]interface{}, bool) { switch value := raw.(type) { case nil: return nil, false case []interface{}: return value, true case map[string]interface{}: items := make([]interface{}, 0, len(value)) for captchaType, settings := range value { items = append(items, map[string]interface{}{ "type": captchaType, "settings": settings, }) } return items, true case string: trimmed := strings.TrimSpace(value) if trimmed == "" { return nil, false } var items []interface{} if err := json.Unmarshal([]byte(trimmed), &items); err == nil { return items, true } var mapping map[string]interface{} if err := json.Unmarshal([]byte(trimmed), &mapping); err == nil { return expandCaptchaSettings(mapping) } } return nil, false } // normalizeCaptchaSettings normalizes captcha settings to string func normalizeCaptchaSettings(raw interface{}) (string, error) { switch value := raw.(type) { case nil: return "", nil case string: return value, nil default: data, err := json.Marshal(value) if err != nil { return "", err } return string(data), nil } } // parseCaptchaCheckResult parses captcha check result from API response func parseCaptchaCheckResult(resp map[string]interface{}) (*captchaCheckResult, error) { respObj, ok := resp["response"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("invalid check response: %v", resp) } result := &captchaCheckResult{} result.Status, _ = respObj["status"].(string) result.SuccessToken, _ = respObj["success_token"].(string) result.ShowCaptchaType, _ = respObj["show_captcha_type"].(string) if result.Status == "" { return nil, fmt.Errorf("check status missing: %v", resp) } return result, nil } // parseSliderCaptchaContentResponse parses slider captcha content from API response func parseSliderCaptchaContentResponse(resp map[string]interface{}) (*sliderCaptchaContent, error) { respObj, ok := resp["response"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("invalid slider content response: %v", resp) } status, _ := respObj["status"].(string) if status != "OK" { return nil, fmt.Errorf("slider getContent status: %s", status) } extension, _ := respObj["extension"].(string) extension = strings.ToLower(extension) if extension != "jpeg" && extension != "jpg" { return nil, fmt.Errorf("unsupported slider image format: %s", extension) } rawImage, _ := respObj["image"].(string) if rawImage == "" { return nil, fmt.Errorf("slider image missing") } rawSteps, ok := respObj["steps"].([]interface{}) if !ok { return nil, fmt.Errorf("slider steps missing") } steps, err := parseIntSlice(rawSteps) if err != nil { return nil, err } size, swaps, attempts, err := parseSliderSteps(steps) if err != nil { return nil, err } img, err := decodeSliderImage(rawImage) if err != nil { return nil, err } return &sliderCaptchaContent{ Image: img, Size: size, Steps: swaps, Attempts: attempts, }, nil } // parseIntSlice parses a slice of integers from interface{} func parseIntSlice(raw []interface{}) ([]int, error) { values := make([]int, 0, len(raw)) for _, item := range raw { number, err := parseIntValue(item) if err != nil { return nil, err } values = append(values, number) } return values, nil } // parseIntValue parses a single integer from interface{} func parseIntValue(raw interface{}) (int, error) { switch value := raw.(type) { case float64: return int(value), nil case int: return value, nil case string: parsed, err := strconv.Atoi(strings.TrimSpace(value)) if err != nil { return 0, fmt.Errorf("invalid numeric value: %v", raw) } return parsed, nil default: return 0, fmt.Errorf("invalid numeric value: %v", raw) } } // parseSliderSteps parses slider steps into size, swaps, and attempts func parseSliderSteps(steps []int) (int, []int, int, error) { if len(steps) < 3 { return 0, nil, 0, fmt.Errorf("slider steps payload too short") } size := steps[0] if size <= 0 { return 0, nil, 0, fmt.Errorf("invalid slider size: %d", size) } remaining := append([]int(nil), steps[1:]...) attempts := defaultSliderAttempts if len(remaining)%2 != 0 { attempts = remaining[len(remaining)-1] remaining = remaining[:len(remaining)-1] } if attempts <= 0 { attempts = defaultSliderAttempts } if len(remaining) == 0 || len(remaining)%2 != 0 { return 0, nil, 0, fmt.Errorf("invalid slider swap payload") } return size, remaining, attempts, nil } // decodeSliderImage decodes base64-encoded slider image func decodeSliderImage(rawImage string) (image.Image, error) { decoded, err := base64.StdEncoding.DecodeString(rawImage) if err != nil { return nil, fmt.Errorf("decode slider image: %w", err) } img, _, err := image.Decode(bytes.NewReader(decoded)) if err != nil { return nil, fmt.Errorf("decode slider image: %w", err) } return img, nil } // encodeSliderAnswer encodes slider answer to base64 JSON func encodeSliderAnswer(activeSteps []int) (string, error) { payload := struct { Value []int `json:"value"` }{ Value: activeSteps, } data, err := json.Marshal(payload) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(data), nil } // buildSliderActiveSteps builds active steps for a candidate func buildSliderActiveSteps(swaps []int, candidateIndex int) []int { if candidateIndex <= 0 { return []int{} } end := candidateIndex * 2 if end > len(swaps) { end = len(swaps) } return append([]int(nil), swaps[:end]...) } // buildSliderTileMapping builds tile mapping for a candidate func buildSliderTileMapping(gridSize int, activeSteps []int) ([]int, error) { tileCount := gridSize * gridSize if tileCount <= 0 { return nil, fmt.Errorf("invalid slider tile count: %d", tileCount) } if len(activeSteps)%2 != 0 { return nil, fmt.Errorf("invalid active steps length: %d", len(activeSteps) / 2) } mapping := make([]int, tileCount) for i := range mapping { mapping[i] = i } for idx := 0; idx < len(activeSteps); idx += 2 { left := activeSteps[idx] right := activeSteps[idx+1] if left < 0 || right < 0 || left >= tileCount || right >= tileCount { return nil, fmt.Errorf("slider step out of range: %d,%d", left, right) } mapping[left], mapping[right] = mapping[right], mapping[left] } return mapping, nil } // rankSliderCandidates ranks slider candidates by score func rankSliderCandidates(img image.Image, gridSize int, swaps []int) ([]sliderCandidate, error) { candidateCount := len(swaps) / 2 if candidateCount == 0 { return nil, fmt.Errorf("slider has no candidates") } candidates := make([]sliderCandidate, 0, candidateCount) for idx := 1; idx <= candidateCount; idx++ { activeSteps := buildSliderActiveSteps(swaps, idx) mapping, err := buildSliderTileMapping(gridSize, activeSteps) if err != nil { return nil, err } score, err := scoreSliderCandidate(img, gridSize, mapping) if err != nil { return nil, err } candidates = append(candidates, sliderCandidate{ Index: idx, ActiveSteps: activeSteps, Score: score, }) } sort.SliceStable(candidates, func(i, j int) bool { if candidates[i].Score == candidates[j].Score { return candidates[i].Index < candidates[j].Index } return candidates[i].Score < candidates[j].Score }) return candidates, nil } // scoreSliderCandidate scores a slider candidate func scoreSliderCandidate(img image.Image, gridSize int, mapping []int) (int64, error) { rendered, err := renderSliderCandidate(img, gridSize, mapping) if err != nil { return 0, err } return scoreRenderedSliderImage(rendered, gridSize), nil } // renderSliderCandidate renders a slider candidate image func renderSliderCandidate(img image.Image, gridSize int, mapping []int) (*image.RGBA, error) { if gridSize <= 0 { return nil, fmt.Errorf("invalid grid size: %d", gridSize) } tileCount := gridSize * gridSize if len(mapping) != tileCount { return nil, fmt.Errorf("unexpected tile mapping length: %d", len(mapping)) } bounds := img.Bounds() rendered := image.NewRGBA(bounds) for dstIndex, srcIndex := range mapping { srcRect := sliderTileRect(bounds, gridSize, srcIndex) dstRect := sliderTileRect(bounds, gridSize, dstIndex) copyScaledTile(rendered, dstRect, img, srcRect) } return rendered, nil } // scoreRenderedSliderImage scores a rendered slider image func scoreRenderedSliderImage(img image.Image, gridSize int) int64 { bounds := img.Bounds() var score int64 for row := 0; row < gridSize; row++ { for col := 0; col < gridSize-1; col++ { leftRect := sliderTileRect(bounds, gridSize, row*gridSize+col) rightRect := sliderTileRect(bounds, gridSize, row*gridSize+col+1) height := minInt(leftRect.Dy(), rightRect.Dy()) for offset := 0; offset < height; offset++ { score += pixelDiff( img.At(leftRect.Max.X-1, leftRect.Min.Y+offset), img.At(rightRect.Min.X, rightRect.Min.Y+offset), ) } } } for row := 0; row < gridSize-1; row++ { for col := 0; col < gridSize; col++ { topRect := sliderTileRect(bounds, gridSize, row*gridSize+col) bottomRect := sliderTileRect(bounds, gridSize, (row+1)*gridSize+col) width := minInt(topRect.Dx(), bottomRect.Dx()) for offset := 0; offset < width; offset++ { score += pixelDiff( img.At(topRect.Min.X+offset, topRect.Max.Y-1), img.At(bottomRect.Min.X+offset, bottomRect.Min.Y), ) } } } return score } // sliderTileRect returns the rectangle for a tile func sliderTileRect(bounds image.Rectangle, gridSize int, index int) image.Rectangle { row := index / gridSize col := index % gridSize x0 := bounds.Min.X + col*bounds.Dx()/gridSize x1 := bounds.Min.X + (col+1)*bounds.Dx()/gridSize y0 := bounds.Min.Y + row*bounds.Dy()/gridSize y1 := bounds.Min.Y + (row+1)*bounds.Dy()/gridSize return image.Rect(x0, y0, x1, y1) } // copyScaledTile copies a scaled tile func copyScaledTile(dst *image.RGBA, dstRect image.Rectangle, src image.Image, srcRect image.Rectangle) { if dstRect.Empty() || srcRect.Empty() { return } dstWidth := dstRect.Dx() dstHeight := dstRect.Dy() srcWidth := srcRect.Dx() srcHeight := srcRect.Dy() for y := 0; y < dstHeight; y++ { sy := srcRect.Min.Y + y*srcHeight/dstHeight for x := 0; x < dstWidth; x++ { sx := srcRect.Min.X + x*srcWidth/dstWidth dst.Set(dstRect.Min.X+x, dstRect.Min.Y+y, src.At(sx, sy)) } } } // pixelDiff calculates pixel difference func pixelDiff(left color.Color, right color.Color) int64 { lr, lg, lb, _ := left.RGBA() rr, rg, rb, _ := right.RGBA() return absDiff(lr, rr) + absDiff(lg, rg) + absDiff(lb, rb) } // absDiff calculates absolute difference func absDiff(left uint32, right uint32) int64 { if left > right { return int64(left - right) } return int64(right - left) } // generateSliderCursor generates a fake slider cursor func generateSliderCursor(candidateIndex int, candidateCount int) string { return buildSliderCursor(candidateIndex, candidateCount, time.Now().Add(-220*time.Millisecond).UnixMilli()) } // buildSliderCursor builds a fake slider cursor func buildSliderCursor(candidateIndex int, candidateCount int, startTime int64) string { if candidateCount <= 0 { return "[]" } type cursorPoint struct { X int `json:"x"` Y int `json:"y"` T int64 `json:"t"` } startX := 140 endX := startX + 620*candidateIndex/candidateCount startY := 430 points := make([]cursorPoint, 0, 12) for step := 0; step < 12; step++ { x := startX + (endX-startX)*step/11 y := startY + ((step % 3) - 1) points = append(points, cursorPoint{ X: x, Y: y, T: startTime + int64(step*18), }) } data, err := json.Marshal(points) if err != nil { return "[]" } return string(data) } // trySliderCaptchaCandidates tries slider captcha candidates func trySliderCaptchaCandidates( candidates []sliderCandidate, maxAttempts int, check func(candidate sliderCandidate) (*captchaCheckResult, error), ) (string, error) { if len(candidates) == 0 { return "", fmt.Errorf("slider has no ranked candidates") } limit := minInt(maxAttempts, len(candidates)) if limit <= 0 { return "", fmt.Errorf("slider has no attempts available") } for idx := 0; idx < limit; idx++ { result, err := check(candidates[idx]) if err != nil { return "", err } switch result.Status { case "OK": if result.SuccessToken == "" { return "", fmt.Errorf("success_token not found") } return result.SuccessToken, nil case "ERROR_LIMIT": return "", fmt.Errorf("slider check status: %s", result.Status) default: continue } } return "", fmt.Errorf("slider guesses exhausted") } // minInt returns the minimum of two integers func minInt(left int, right int) int { if left < right { return left } return right } // describeCaptchaTypes describes available captcha types func describeCaptchaTypes(settingsByType map[string]string) string { if len(settingsByType) == 0 { return "none" } types := make([]string, 0, len(settingsByType)) for captchaType := range settingsByType { types = append(types, captchaType) } sort.Strings(types) return strings.Join(types, ",") } // randInt63 generates a random int63 func randInt63() int64 { return rand.Int63() }