From ef01992613001833f7b6afc09026daf0c240990a Mon Sep 17 00:00:00 2001 From: alexmac6574 <215134852+alexmac6574@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:24:34 +0300 Subject: [PATCH] fix: Use PoC method for captcha solving after PoW method --- README.md | 2 +- client/main.go | 115 ++++++++++++++++++++++++++++++-------------- client/main_test.go | 61 +++++++++++++++++++++++ 3 files changed, 141 insertions(+), 37 deletions(-) create mode 100644 client/main_test.go diff --git a/README.md b/README.md index b4e0b46..678875c 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ curl -L -o client https://github.com/cacggghp/vk-turn-proxy/releases/latest/down Для прохождения капчи вручную - `-manual-captcha`. -Для экспериментальной автопопытки решить пазл-слайдер можно добавить флаг `-auto-captcha-slider-poc`. +По умолчанию капча теперь проходит так: обычная автопопытка, затем автопопытка через пазл-слайдер POC, и только потом ручной режим. ## Яндекс телемост diff --git a/client/main.go b/client/main.go index a5d488c..998e00c 100644 --- a/client/main.go +++ b/client/main.go @@ -68,6 +68,49 @@ var ( autoCaptchaSliderPOC bool ) +type captchaSolveMode int + +const ( + captchaSolveModeAuto captchaSolveMode = iota + captchaSolveModeSliderPOC + captchaSolveModeManual +) + +func captchaSolveModeForAttempt(attempt int, manualOnly bool, enableSliderPOC bool) (captchaSolveMode, bool) { + if manualOnly { + return captchaSolveModeManual, attempt == 0 + } + + switch attempt { + case 0: + return captchaSolveModeAuto, true + case 1: + if enableSliderPOC { + return captchaSolveModeSliderPOC, true + } + return captchaSolveModeManual, true + case 2: + if enableSliderPOC { + return captchaSolveModeManual, true + } + } + + return 0, false +} + +func captchaSolveModeLabel(mode captchaSolveMode) string { + switch mode { + case captchaSolveModeAuto: + return "auto captcha" + case captchaSolveModeSliderPOC: + return "auto captcha slider POC" + case captchaSolveModeManual: + return "manual captcha" + default: + return "captcha" + } +} + type UDPPacket struct { Data []byte N int @@ -297,8 +340,12 @@ func (e *VkCaptchaError) IsCaptchaError() bool { return e.ErrorCode == 14 && e.RedirectUri != "" && e.SessionToken != "" } -func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID int, client tlsclient.HttpClient, profile Profile) (string, error) { - log.Printf("[STREAM %d] [Captcha] Solving Not Robot Captcha...", streamID) +func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID int, client tlsclient.HttpClient, profile Profile, useSliderPOC bool) (string, error) { + if useSliderPOC { + log.Printf("[STREAM %d] [Captcha] Solving captcha with slider POC...", streamID) + } else { + log.Printf("[STREAM %d] [Captcha] Solving captcha...", streamID) + } if captchaErr.SessionToken == "" { return "", fmt.Errorf("no session_token in redirect_uri for auto-solve") @@ -318,7 +365,7 @@ func solveVkCaptcha(ctx context.Context, captchaErr *VkCaptchaError, streamID in log.Printf("[STREAM %d] [Captcha] PoW solved: hash=%s", streamID, hash) var successToken string - if autoCaptchaSliderPOC { + if useSliderPOC { successToken, err = callCaptchaNotRobotWithSliderPOC( ctx, captchaErr.SessionToken, @@ -812,11 +859,12 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede urlAddr := fmt.Sprintf("https://api.vk.ru/method/calls.getAnonymousToken?v=5.275&client_id=%s", creds.ClientID) var token2 string - maxAutoAttempts := 2 - if manualCaptcha { - maxAutoAttempts = 0 - } - for attempt := 0; attempt <= maxAutoAttempts+1; attempt++ { + for attempt := 0; ; attempt++ { + solveMode, hasSolveMode := captchaSolveModeForAttempt(attempt, manualCaptcha, autoCaptchaSliderPOC) + if !hasSolveMode { + break + } + resp, err = doRequest(data, urlAddr) if err != nil { return "", "", "", err @@ -829,20 +877,27 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede var captchaKey string var solveErr error - if attempt < maxAutoAttempts { - // Auto Solve Attempts + switch solveMode { + case captchaSolveModeAuto: if captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" { - successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, client, profile) + successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, client, profile, false) if solveErr != nil { - log.Printf("[STREAM %d] [Captcha] Auto solve failed: %v", streamID, solveErr) + log.Printf("[STREAM %d] [Captcha] Auto captcha failed: %v", streamID, solveErr) } } else { solveErr = fmt.Errorf("missing fields for auto solve") } - } else if attempt == maxAutoAttempts { - // Manual Solve Fallback with 60s Timeout - log.Printf("[STREAM %d] [Captcha] Auto failed %d times. Triggering MANUAL fallback...", streamID, maxAutoAttempts) - + case captchaSolveModeSliderPOC: + if captchaErr.SessionToken != "" && captchaErr.RedirectUri != "" { + successToken, solveErr = solveVkCaptcha(ctx, captchaErr, streamID, client, profile, true) + if solveErr != nil { + log.Printf("[STREAM %d] [Captcha] Auto captcha slider POC failed: %v", streamID, solveErr) + } + } else { + solveErr = fmt.Errorf("missing fields for slider POC auto solve") + } + case captchaSolveModeManual: + log.Printf("[STREAM %d] [Captcha] Triggering manual captcha fallback...", streamID) manualCtx, manualCancel := context.WithTimeout(ctx, 60*time.Second) type manualRes struct { @@ -874,29 +929,15 @@ func getTokenChain(ctx context.Context, link string, streamID int, creds VKCrede solveErr = fmt.Errorf("manual captcha timed out after 60s") } manualCancel() - } else { - solveErr = fmt.Errorf("max attempts reached") } // If solving failed (auto or manual) or timed out if solveErr != nil { - log.Printf("[STREAM %d] [Captcha] Failed to solve (attempt %d): %v", streamID, attempt+1, solveErr) + log.Printf("[STREAM %d] [Captcha] %s failed (attempt %d): %v", streamID, captchaSolveModeLabel(solveMode), attempt+1, solveErr) - if attempt < maxAutoAttempts-1 { - log.Printf("[STREAM %d] [Captcha] Backing off for 10 seconds before next auto attempt...", streamID) - select { - case <-ctx.Done(): - return "", "", "", ctx.Err() - case <-time.After(10 * time.Second): - } - continue - } else if attempt == maxAutoAttempts-1 { - log.Printf("[STREAM %d] [Captcha] Backing off for 2 seconds before manual fallback...", streamID) - select { - case <-ctx.Done(): - return "", "", "", ctx.Err() - case <-time.After(2 * time.Second): - } + nextSolveMode, hasNextSolveMode := captchaSolveModeForAttempt(attempt+1, manualCaptcha, autoCaptchaSliderPOC) + if hasNextSolveMode { + log.Printf("[STREAM %d] [Captcha] Falling back to %s...", streamID, captchaSolveModeLabel(nextSolveMode)) continue } @@ -1675,7 +1716,7 @@ func main() { direct := flag.Bool("no-dtls", false, "connect without obfuscation. DO NOT USE") debugFlag := flag.Bool("debug", false, "enable debug logging") manualCaptchaFlag := flag.Bool("manual-captcha", false, "skip auto captcha solving, use manual mode immediately") - autoCaptchaSliderPOCFlag := flag.Bool("auto-captcha-slider-poc", false, "experimental: try local slider captcha solving before manual fallback") + autoCaptchaSliderPOCFlag := flag.Bool("auto-captcha-slider-poc", false, "compatibility flag: slider POC fallback is now enabled by default before manual fallback") flag.Parse() if *peerAddr == "" { log.Panicf("Need peer address!") @@ -1690,9 +1731,11 @@ func main() { isDebug = *debugFlag manualCaptcha = *manualCaptchaFlag - autoCaptchaSliderPOC = *autoCaptchaSliderPOCFlag && !manualCaptcha + autoCaptchaSliderPOC = !manualCaptcha if *autoCaptchaSliderPOCFlag && manualCaptcha { log.Printf("[Captcha] manual-captcha enabled, ignoring -auto-captcha-slider-poc") + } else if *autoCaptchaSliderPOCFlag { + log.Printf("[Captcha] -auto-captcha-slider-poc is now enabled by default") } var link string diff --git a/client/main_test.go b/client/main_test.go new file mode 100644 index 0000000..e0f2d77 --- /dev/null +++ b/client/main_test.go @@ -0,0 +1,61 @@ +package main + +import "testing" + +func TestCaptchaSolveModeForAttempt(t *testing.T) { + t.Parallel() + + t.Run("default flow", func(t *testing.T) { + t.Parallel() + + mode, ok := captchaSolveModeForAttempt(0, false, true) + if !ok || mode != captchaSolveModeAuto { + t.Fatalf("expected first attempt to use auto captcha, got mode=%v ok=%v", mode, ok) + } + + mode, ok = captchaSolveModeForAttempt(1, false, true) + if !ok || mode != captchaSolveModeSliderPOC { + t.Fatalf("expected second attempt to use slider POC, got mode=%v ok=%v", mode, ok) + } + + mode, ok = captchaSolveModeForAttempt(2, false, true) + if !ok || mode != captchaSolveModeManual { + t.Fatalf("expected third attempt to use manual captcha, got mode=%v ok=%v", mode, ok) + } + + if _, ok = captchaSolveModeForAttempt(3, false, true); ok { + t.Fatal("expected no fourth captcha attempt in default flow") + } + }) + + t.Run("manual only flow", func(t *testing.T) { + t.Parallel() + + mode, ok := captchaSolveModeForAttempt(0, true, true) + if !ok || mode != captchaSolveModeManual { + t.Fatalf("expected manual mode on first attempt, got mode=%v ok=%v", mode, ok) + } + + if _, ok = captchaSolveModeForAttempt(1, true, true); ok { + t.Fatal("expected only one manual captcha attempt when manual mode is forced") + } + }) + + t.Run("flow without slider poc", func(t *testing.T) { + t.Parallel() + + mode, ok := captchaSolveModeForAttempt(0, false, false) + if !ok || mode != captchaSolveModeAuto { + t.Fatalf("expected auto captcha first, got mode=%v ok=%v", mode, ok) + } + + mode, ok = captchaSolveModeForAttempt(1, false, false) + if !ok || mode != captchaSolveModeManual { + t.Fatalf("expected manual captcha second when slider POC is disabled, got mode=%v ok=%v", mode, ok) + } + + if _, ok = captchaSolveModeForAttempt(2, false, false); ok { + t.Fatal("expected only two attempts when slider POC is disabled") + } + }) +}