@ -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" , d ebugInfo)
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 ,