Browse Source

captcha fix

pull/26/head
kiper292 2 months ago
parent
commit
b656d10de8
  1. 458
      client/main.go
  2. 325
      client/vk_captcha.go

458
client/main.go

@ -12,8 +12,10 @@ import (
"fmt"
"io"
"log"
"math/rand"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
@ -31,123 +33,378 @@ import (
"github.com/pion/turn/v5"
)
type getCredsFunc func(string) (string, string, string, error)
type getCredsFunc func(context.Context, string, int) (string, string, string, error)
func getVkCreds(link string) (string, string, string, error) {
doRequest := func(data string, url string) (resp map[string]interface{}, err error) {
client := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
}
defer client.CloseIdleConnections()
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
if err != nil {
return nil, err
}
const vkClientID = "6287487"
const vkClientSecret = "QbYic1K3lEV5kTGiqlq2"
const vkAPIVersion = "5.275"
req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
// TurnCredentials stores cached TURN credentials
type TurnCredentials struct {
Username string
Password string
ServerAddr string
ExpiresAt time.Time
Link string
}
httpResp, err := client.Do(req)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
// StreamCredentialsCache holds credentials cache for a single stream
type StreamCredentialsCache struct {
creds TurnCredentials
mutex sync.RWMutex
errorCount atomic.Int32
lastErrorTime atomic.Int64
}
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
const (
credentialLifetime = 10 * time.Minute
cacheSafetyMargin = 60 * time.Second
maxCacheErrors = 3
errorWindow = 10 * time.Second
streamsPerCache = 4 // Number of streams sharing one credentials cache
)
err = json.Unmarshal(body, &resp)
if err != nil {
return nil, err
}
// getCacheID returns the shared cache ID for a given stream ID
func getCacheID(streamID int) int {
return streamID / streamsPerCache
}
// credentialsStore manages per-stream credentials caches
var credentialsStore = struct {
mu sync.RWMutex
caches map[int]*StreamCredentialsCache
}{
caches: make(map[int]*StreamCredentialsCache),
}
// getStreamCache returns or creates a shared cache for the given stream ID
func getStreamCache(streamID int) *StreamCredentialsCache {
cacheID := getCacheID(streamID)
// Try read lock first for fast path
credentialsStore.mu.RLock()
cache, exists := credentialsStore.caches[cacheID]
credentialsStore.mu.RUnlock()
return resp, nil
if exists {
return cache
}
var resp map[string]interface{}
defer func() {
if r := recover(); r != nil {
log.Panicf("get TURN creds error: %v\n\n", resp)
}
}()
/*
data := "client_secret=QbYic1K3lEV5kTGiqlq2&client_id=6287487&scopes=audio_anonymous%2Cvideo_anonymous%2Cphotos_anonymous%2Cprofile_anonymous&isApiOauthAnonymEnabled=false&version=1&app_id=6287487"
url := "https://login.vk.ru/?act=get_anonym_token"
// Need to create new cache
credentialsStore.mu.Lock()
defer credentialsStore.mu.Unlock()
resp, err := doRequest(data, url)
if err != nil {
return "", "", "", fmt.Errorf("request error:%s", err)
// Double-check after acquiring write lock
if cache, exists = credentialsStore.caches[cacheID]; exists {
return cache
}
token1 := resp["data"].(map[string]interface{})["access_token"].(string)
cache = &StreamCredentialsCache{}
credentialsStore.caches[cacheID] = cache
return cache
}
data = fmt.Sprintf("access_token=%s", token1)
url = "https://api.vk.ru/method/calls.getAnonymousAccessTokenPayload?v=5.264&client_id=6287487"
// invalidate invalidates the credentials cache for this stream
func (c *StreamCredentialsCache) invalidate(streamID int) {
c.mutex.Lock()
c.creds = TurnCredentials{}
c.mutex.Unlock()
resp, err = doRequest(data, url)
if err != nil {
return "", "", "", fmt.Errorf("request error:%s", err)
// Reset auth error counter
c.errorCount.Store(0)
c.lastErrorTime.Store(0)
log.Printf("[VK Auth] Credentials cache invalidated for stream %d", streamID)
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// vkDelay sleeps for a random duration between minMs and maxMs to avoid bot detection
func vkDelay(minMs, maxMs int) {
ms := minMs + rand.Intn(maxMs-minMs+1)
time.Sleep(time.Duration(ms) * time.Millisecond)
}
token2 := resp["response"].(map[string]interface{})["payload"].(string)
*/
//data = fmt.Sprintf("client_id=6287487&token_type=messages&payload=%s&client_secret=QbYic1K3lEV5kTGiqlq2&version=1&app_id=6287487", token2)
data := fmt.Sprintf("client_id=6287487&token_type=messages&client_secret=QbYic1K3lEV5kTGiqlq2&version=1&app_id=6287487")
url := "https://login.vk.ru/?act=get_anonym_token"
// vkCredsMu serializes VK credential fetching to avoid BOT detection from parallel requests
var vkCredsMu sync.Mutex
resp, err := doRequest(data, url)
if err != nil {
return "", "", "", fmt.Errorf("request error:%s", err)
// getVkCredsCached checks cache before fetching credentials
func getVkCredsCached(ctx context.Context, link string, streamID int) (string, string, string, error) {
cache := getStreamCache(streamID)
cacheID := getCacheID(streamID)
cache.mutex.Lock()
defer cache.mutex.Unlock()
// Check cache - another stream may have populated it while waiting
if cache.creds.Link == link && time.Now().Before(cache.creds.ExpiresAt) {
expires := time.Until(cache.creds.ExpiresAt)
log.Printf("[VK Auth] Using cached credentials (cache=%d, expires in %v)", cacheID, expires)
return cache.creds.Username, cache.creds.Password, cache.creds.ServerAddr, nil
}
token3 := resp["data"].(map[string]interface{})["access_token"].(string)
log.Printf("[VK Auth] Cache miss (cache=%d), starting credential fetch...", cacheID)
data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=123&access_token=%s", link, token3)
url = "https://api.vk.ru/method/calls.getAnonymousToken?v=5.274&client_id=6287487"
// Check context before long fetch
select {
case <-ctx.Done():
return "", "", "", ctx.Err()
default:
}
resp, err = doRequest(data, url)
// Fetch credentials with mutex to avoid VK flood control
user, pass, addr, err := getVkCredsSafe(ctx, link, streamID)
if err != nil {
return "", "", "", fmt.Errorf("request error:%s", err)
return "", "", "", err
}
token4 := resp["response"].(map[string]interface{})["token"].(string)
// Store in cache
cache.creds = TurnCredentials{
Username: user,
Password: pass,
ServerAddr: addr,
ExpiresAt: time.Now().Add(credentialLifetime - cacheSafetyMargin),
Link: link,
}
log.Printf("[VK Auth] Success! Credentials cached until %v (cache=%d)", cache.creds.ExpiresAt, cacheID)
return user, pass, addr, nil
}
data = fmt.Sprintf("%s%s%s", "session_data=%7B%22version%22%3A2%2C%22device_id%22%3A%22", uuid.New(), "%22%2C%22client_version%22%3A1.1%2C%22client_type%22%3A%22SDK_JS%22%7D&method=auth.anonymLogin&format=JSON&application_key=CGMMEJLGDIHBABABA")
url = "https://calls.okcdn.ru/fb.do"
// getVkCredsSafe wraps getVkCreds with mutex to avoid VK flood control
func getVkCredsSafe(ctx context.Context, link string, streamID int) (string, string, string, error) {
vkCredsMu.Lock()
defer vkCredsMu.Unlock()
return getVkCreds(ctx, link)
}
resp, err = doRequest(data, url)
func vkHTTPPost(ctx context.Context, data string, url string) (map[string]interface{}, error) {
client := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
}
defer client.CloseIdleConnections()
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer([]byte(data)))
if err != nil {
return nil, err
}
// Headers matching HAR capture exactly
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) 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", `"Windows"`)
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("Sec-Fetch-Site", "same-site")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("DNT", "1")
req.Header.Set("Priority", "u=1, i")
httpResp, err := client.Do(req)
if err != nil {
return "", "", "", fmt.Errorf("request error:%s", err)
return nil, err
}
defer httpResp.Body.Close()
token5 := resp["session_key"].(string)
// Handle HTTP errors (redirects, rate limits, etc.)
if httpResp.StatusCode >= 400 {
body, _ := io.ReadAll(httpResp.Body)
return nil, fmt.Errorf("HTTP %d from %s: %s", httpResp.StatusCode, req.URL, string(body[:min(len(body), 500)]))
}
data = fmt.Sprintf("joinLink=%s&isVideo=false&protocolVersion=5&capabilities=2F7F&anonymToken=%s&method=vchat.joinConversationByLink&format=JSON&application_key=CGMMEJLGDIHBABABA&session_key=%s", link, token4, token5)
url = "https://calls.okcdn.ru/fb.do"
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
resp, err = doRequest(data, url)
// Check content type - VK may return HTML instead of JSON (captcha page, redirect, etc.)
contentType := httpResp.Header.Get("Content-Type")
if contentType != "" && !strings.Contains(contentType, "application/json") && !strings.Contains(contentType, "text/javascript") {
// Log first 500 chars of non-JSON response for debugging
logPreview := string(body)
if len(logPreview) > 500 {
logPreview = logPreview[:500] + "...(truncated)"
}
return nil, fmt.Errorf("unexpected content-type %s, status %d, body: %s", contentType, httpResp.StatusCode, logPreview)
}
var resp map[string]interface{}
if err = json.Unmarshal(body, &resp); err != nil {
// Log the raw body for debugging
logPreview := string(body)
if len(logPreview) > 500 {
logPreview = logPreview[:500] + "...(truncated)"
}
return nil, fmt.Errorf("JSON parse error: %w, body: %s", err, logPreview)
}
return resp, nil
}
func getVkCreds(ctx context.Context, link string) (string, string, string, error) {
// Token 1 (messages)
log.Println("[VK Auth] Getting Token 1...")
data := fmt.Sprintf("client_id=%s&token_type=messages&client_secret=%s&version=1&app_id=%s",
vkClientID, vkClientSecret, vkClientID)
resp, err := vkHTTPPost(ctx, data, "https://login.vk.ru/?act=get_anonym_token")
if err != nil {
return "", "", "", fmt.Errorf("Token 1 request error: %w", err)
}
if errMsg, ok := resp["error"].(map[string]interface{}); ok {
return "", "", "", fmt.Errorf("Token 1 VK error: %v", errMsg)
}
dataObj, ok := resp["data"].(map[string]interface{})
if !ok {
return "", "", "", fmt.Errorf("invalid Token 1 response: %v", resp)
}
token1, ok := dataObj["access_token"].(string)
if !ok {
return "", "", "", fmt.Errorf("access_token not found in Token 1 response")
}
log.Println("[VK Auth] Token 1 received")
vkDelay(100, 200) // Token 1 → getCallPreview
// getCallPreview (optional, like browser)
log.Println("[VK Auth] Getting call preview...")
cpData := fmt.Sprintf("vk_join_link=https://vk.ru/call/join/%s&fields=photo_200&access_token=%s",
url.QueryEscape(link), token1)
cpURL := fmt.Sprintf("https://api.vk.ru/method/calls.getCallPreview?v=%s&client_id=%s", vkAPIVersion, vkClientID)
_, _ = vkHTTPPost(ctx, cpData, cpURL) // non-critical
vkDelay(500, 1000) // getCallPreview → Token 2
// Token 2 (may require captcha)
log.Println("[VK Auth] Getting Token 2...")
t2Data := fmt.Sprintf("vk_join_link=https://vk.ru/call/join/%s&name=123&access_token=%s",
url.QueryEscape(link), token1)
t2URL := fmt.Sprintf("https://api.vk.ru/method/calls.getAnonymousToken?v=%s&client_id=%s", vkAPIVersion, vkClientID)
resp, err = vkHTTPPost(ctx, t2Data, t2URL)
if err != nil {
return "", "", "", fmt.Errorf("request error:%s", err)
return "", "", "", fmt.Errorf("Token 2 request error: %w", err)
}
user := resp["turn_server"].(map[string]interface{})["username"].(string)
pass := resp["turn_server"].(map[string]interface{})["credential"].(string)
turn := resp["turn_server"].(map[string]interface{})["urls"].([]interface{})[0].(string)
// Check for captcha error
if errMsg, ok := resp["error"].(map[string]interface{}); ok {
captchaData, isCaptcha := ExtractCaptchaData(errMsg)
if !isCaptcha {
return "", "", "", fmt.Errorf("Token 2 VK error: %v", errMsg)
}
log.Printf("[VK Auth] Captcha detected, solving...")
successToken, solveErr := SolveVkCaptcha(ctx, captchaData)
if solveErr != nil {
return "", "", "", fmt.Errorf("captcha solving failed: %w", solveErr)
}
// Delay before retry (endSession → Token 2 retry)
vkDelay(100, 200)
// Retry Token 2 with captcha solution
log.Println("[VK Auth] Retrying Token 2 with captcha solution...")
t2Data = fmt.Sprintf(
"vk_join_link=https://vk.ru/call/join/%s&name=123"+
"&captcha_key=&captcha_sid=%s&is_sound_captcha=0"+
"&success_token=%s&captcha_ts=%s&captcha_attempt=%s"+
"&access_token=%s",
url.QueryEscape(link),
captchaData.CaptchaSid,
successToken,
captchaData.CaptchaTs,
captchaData.CaptchaAttempt,
token1,
)
resp, err = vkHTTPPost(ctx, t2Data, t2URL)
if err != nil {
return "", "", "", fmt.Errorf("Token 2 retry request error: %w", err)
}
if errMsg2, ok := resp["error"].(map[string]interface{}); ok {
return "", "", "", fmt.Errorf("Token 2 retry VK error: %v", errMsg2)
}
// Token 2 retry → Token 3
vkDelay(100, 200)
}
clean := strings.Split(turn, "?")[0]
token2Obj, ok := resp["response"].(map[string]interface{})
if !ok {
return "", "", "", fmt.Errorf("invalid Token 2 response: %v", resp)
}
token2, ok := token2Obj["token"].(string)
if !ok {
return "", "", "", fmt.Errorf("token not found in Token 2 response")
}
log.Println("[VK Auth] Token 2 received")
// Token 2 → Token 3
vkDelay(100, 200)
// Token 3 (OK auth.anonymLogin)
log.Println("[VK Auth] Getting Token 3...")
sessionData := fmt.Sprintf(`{"version":2,"device_id":"%s","client_version":1.1,"client_type":"SDK_JS"}`, uuid.New())
t3Data := fmt.Sprintf("session_data=%s&method=auth.anonymLogin&format=JSON&application_key=CGMMEJLGDIHBABABA",
url.QueryEscape(sessionData))
resp, err = vkHTTPPost(ctx, t3Data, "https://calls.okcdn.ru/fb.do")
if err != nil {
return "", "", "", fmt.Errorf("Token 3 request error: %w", err)
}
if errMsg, ok := resp["error"].(string); ok && errMsg != "" {
return "", "", "", fmt.Errorf("Token 3 API error: %s", errMsg)
}
token3, ok := resp["session_key"].(string)
if !ok {
return "", "", "", fmt.Errorf("session_key not found in Token 3 response")
}
log.Println("[VK Auth] Token 3 received")
// Token 3 → Final (TURN)
vkDelay(100, 200)
// Final: vchat.joinConversationByLink (Token 4)
log.Println("[VK Auth] Getting TURN credentials (Token 4)...")
finalData := fmt.Sprintf(
"joinLink=%s&isVideo=false&protocolVersion=5&capabilities=2F7F&anonymToken=%s&method=vchat.joinConversationByLink&format=JSON&application_key=CGMMEJLGDIHBABABA&session_key=%s",
url.QueryEscape(link), token2, token3)
resp, err = vkHTTPPost(ctx, finalData, "https://calls.okcdn.ru/fb.do")
if err != nil {
return "", "", "", fmt.Errorf("Final request error: %w", err)
}
if errMsg, ok := resp["error"].(string); ok && errMsg != "" {
return "", "", "", fmt.Errorf("Final API error: %s", errMsg)
}
ts, ok := resp["turn_server"].(map[string]interface{})
if !ok {
return "", "", "", fmt.Errorf("turn_server not found in response: %v", resp)
}
urls, _ := ts["urls"].([]interface{})
if len(urls) == 0 {
return "", "", "", fmt.Errorf("urls not found in turn_server")
}
urlStr, _ := urls[0].(string)
clean := strings.Split(urlStr, "?")[0]
address := strings.TrimPrefix(strings.TrimPrefix(clean, "turn:"), "turns:")
return user, pass, address, nil
username, _ := ts["username"].(string)
credential, _ := ts["credential"].(string)
if username == "" || credential == "" {
return "", "", "", fmt.Errorf("username or credential not found in turn_server")
}
log.Println("[VK Auth] TURN credentials received")
vkDelay(1500, 2500) // Final delay before exit
return username, credential, address, nil
}
func getYandexCreds(link string) (string, string, string, error) {
func getYandexCreds(ctx context.Context, link string, streamID int) (string, string, string, error) {
const debug = false
const telemostConfHost = "cloud-api.yandex.ru"
telemostConfPath := fmt.Sprintf("%s%s%s", "/telemost_front/v2/telemost/conferences/https%3A%2F%2Ftelemost.yandex.ru%2Fj%2F", link, "/connection?next_gen_media_platform_allowed=false")
@ -441,7 +698,8 @@ func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net.
CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
ConnectionIDGenerator: dtls.OnlySendCIDGenerator(),
}
ctx1, cancel := context.WithTimeout(ctx, 30*time.Second)
// Extended timeout to accommodate serialized credential fetching via mutex
ctx1, cancel := context.WithTimeout(ctx, 120*time.Second)
defer cancel()
dtlsConn, err := dtls.Client(conn, peer, config)
if err != nil {
@ -586,13 +844,14 @@ type turnParams struct {
port string
link string
udp bool
streamID int
getCreds getCredsFunc
}
func oneTurnConnection(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, conn2 net.PacketConn, c chan<- error) {
var err error = nil
defer func() { c <- err }()
user, pass, url, err1 := turnParams.getCreds(turnParams.link)
user, pass, url, err1 := turnParams.getCreds(ctx, turnParams.link, turnParams.streamID)
if err1 != nil {
err = fmt.Errorf("failed to get TURN credentials: %s", err1)
return
@ -785,7 +1044,11 @@ func oneDtlsConnectionLoop(ctx context.Context, peer *net.UDPAddr, listenConnCha
}
}
func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, connchan <-chan net.PacketConn, t <-chan time.Time) {
func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *net.UDPAddr, connchan <-chan net.PacketConn, t <-chan time.Time, streamID int) {
// Create a copy of turnParams with the streamID
tp := *turnParams
tp.streamID = streamID
for {
select {
case <-ctx.Done():
@ -794,7 +1057,7 @@ func oneTurnConnectionLoop(ctx context.Context, turnParams *turnParams, peer *ne
select {
case <-t:
c := make(chan error)
go oneTurnConnection(ctx, turnParams, peer, conn2, c)
go oneTurnConnection(ctx, &tp, peer, conn2, c)
if err := <-c; err != nil {
log.Printf("%s", err)
}
@ -846,9 +1109,9 @@ func main() { //nolint:cyclop
if *vklink != "" {
parts := strings.Split(*vklink, "join/")
link = parts[len(parts)-1]
getCreds = getVkCreds
getCreds = getVkCredsCached
if *n <= 0 {
*n = 16
*n = 4
}
} else {
parts := strings.Split(*yalink, "j/")
@ -862,11 +1125,12 @@ func main() { //nolint:cyclop
link = link[:idx]
}
params := &turnParams{
*host,
*port,
link,
*udp,
getCreds,
host: *host,
port: *port,
link: link,
udp: *udp,
streamID: 0,
getCreds: getCreds,
}
var sessionID []byte
@ -905,9 +1169,10 @@ func main() { //nolint:cyclop
if *direct {
for i := 0; i < *n; i++ {
wg1.Add(1)
streamID := i
go func() {
defer wg1.Done()
oneTurnConnectionLoop(ctx, params, peer, listenConnChan, t)
oneTurnConnectionLoop(ctx, params, peer, listenConnChan, t, streamID)
}()
}
} else {
@ -923,7 +1188,7 @@ func main() { //nolint:cyclop
wg1.Add(1)
go func() {
defer wg1.Done()
oneTurnConnectionLoop(ctx, params, peer, connchan, t)
oneTurnConnectionLoop(ctx, params, peer, connchan, t, 0)
}()
select {
@ -932,15 +1197,16 @@ func main() { //nolint:cyclop
}
for i := 0; i < *n-1; i++ {
connchan := make(chan net.PacketConn)
streamID := i + 1
wg1.Add(1)
go func(streamID byte) {
go func(sID byte) {
defer wg1.Done()
oneDtlsConnectionLoop(ctx, peer, listenConnChan, connchan, nil, sessionID, streamID)
}(byte(i + 1))
oneDtlsConnectionLoop(ctx, peer, listenConnChan, connchan, nil, sessionID, sID)
}(byte(streamID))
wg1.Add(1)
go func() {
defer wg1.Done()
oneTurnConnectionLoop(ctx, params, peer, connchan, t)
oneTurnConnectionLoop(ctx, params, peer, connchan, t, streamID)
}()
}
}

325
client/vk_captcha.go

@ -0,0 +1,325 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package main
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
)
const (
vkCaptchaAPIVersion = "5.275"
vkCaptchaNotRobotVer = "5.131"
vkDebugInfo = "d44f534ce8deb56ba20be52e05c433309b49ee4d2a70602deeb17a1954257785"
)
// VkCaptchaData holds captcha challenge data from VK error response
type VkCaptchaData struct {
CaptchaSid string
CaptchaTs string
CaptchaAttempt string
RedirectURI string
SessionToken string
}
// ExtractCaptchaData parses VK API error response for captcha info
func ExtractCaptchaData(errResp map[string]interface{}) (*VkCaptchaData, bool) {
code, _ := errResp["error_code"].(float64)
if int(code) != 14 {
return nil, false
}
redirectURI, _ := errResp["redirect_uri"].(string)
if redirectURI == "" {
return nil, false
}
// Parse session_token from redirect_uri
parsed, err := url.Parse(redirectURI)
if err != nil {
return nil, false
}
sessionToken := parsed.Query().Get("session_token")
if sessionToken == "" {
return nil, false
}
// Extract captcha_sid
captchaSid, _ := errResp["captcha_sid"].(string)
// captcha_ts can be float64 or string
var captchaTs string
if tsFloat, ok := errResp["captcha_ts"].(float64); ok {
captchaTs = fmt.Sprintf("%.0f", tsFloat)
} else if tsStr, ok := errResp["captcha_ts"].(string); ok {
captchaTs = tsStr
}
// captcha_attempt
var captchaAttempt string
if attFloat, ok := errResp["captcha_attempt"].(float64); ok {
captchaAttempt = fmt.Sprintf("%.0f", attFloat)
} else if attStr, ok := errResp["captcha_attempt"].(string); ok {
captchaAttempt = attStr
}
return &VkCaptchaData{
CaptchaSid: captchaSid,
CaptchaTs: captchaTs,
CaptchaAttempt: captchaAttempt,
RedirectURI: redirectURI,
SessionToken: sessionToken,
}, true
}
// SolveVkCaptcha fetches captcha page, solves PoW, and calls captchaNotRobot API
func SolveVkCaptcha(ctx context.Context, captchaData *VkCaptchaData) (string, error) {
log.Printf("[Captcha] Solving Not Robot Captcha...")
// HAR: Token 2 error → Captcha HTML = 2.72s (browser page load + user perception)
time.Sleep(1500*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond)
// Fetch captcha HTML (browser redirect)
powInput, difficulty, cookies, err := fetchCaptchaPowInput(ctx, captchaData.RedirectURI)
if err != nil {
return "", fmt.Errorf("failed to fetch powInput: %w", err)
}
log.Printf("[Captcha] PoW input: %s, difficulty: %d", powInput, difficulty)
// Solve PoW
hash := solveCaptchaPoW(powInput, difficulty)
log.Printf("[Captcha] PoW solved: hash=%s", hash)
// Call captchaNotRobot API with cookies from captcha page
successToken, err := callCaptchaNotRobotAPI(ctx, captchaData.SessionToken, hash, cookies)
if err != nil {
return "", fmt.Errorf("captchaNotRobot API failed: %w", err)
}
log.Printf("[Captcha] Success! Got success_token")
return successToken, nil
}
func fetchCaptchaPowInput(ctx context.Context, redirectURI string) (string, int, string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", redirectURI, nil)
if err != nil {
return "", 0, "", err
}
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{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Do(req)
if err != nil {
return "", 0, "", err
}
defer resp.Body.Close()
// Capture Set-Cookie headers
var cookieValues []string
for _, setCookie := range resp.Header.Values("Set-Cookie") {
// Extract just the cookie name=value part (before ; expires= or ; path=)
cookieParts := strings.Split(setCookie, ";")
cookieValues = append(cookieValues, strings.TrimSpace(cookieParts[0]))
}
cookies := strings.Join(cookieValues, "; ")
if cookies != "" {
log.Printf("[Captcha] Captcha page set %d cookie(s)", len(cookieValues))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", 0, "", err
}
html := string(body)
// Extract powInput: const powInput = "..."
powInputRe := regexp.MustCompile(`const\s+powInput\s*=\s*"([^"]+)"`)
powInputMatch := powInputRe.FindStringSubmatch(html)
if len(powInputMatch) < 2 {
return "", 0, "", fmt.Errorf("powInput not found in captcha HTML")
}
powInput := powInputMatch[1]
// Extract difficulty: '0'.repeat(N)
diffRe := regexp.MustCompile(`startsWith\('0'\.repeat\((\d+)\)\)`)
diffMatch := diffRe.FindStringSubmatch(html)
difficulty := 2 // default
if len(diffMatch) >= 2 {
if d, err := strconv.Atoi(diffMatch[1]); err == nil {
difficulty = d
}
}
return powInput, difficulty, cookies, nil
}
func solveCaptchaPoW(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
}
}
return ""
}
func callCaptchaNotRobotAPI(ctx context.Context, sessionToken, hash, cookies string) (string, error) {
vkReq := func(method string, postData string) (map[string]interface{}, error) {
requestURL := fmt.Sprintf("https://api.vk.ru/method/%s?v=%s", method, vkCaptchaNotRobotVer)
req, err := http.NewRequestWithContext(ctx, "POST", requestURL, strings.NewReader(postData))
if err != nil {
return nil, err
}
// Headers matching HAR capture exactly
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) 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://id.vk.ru")
req.Header.Set("Referer", "https://id.vk.ru/")
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
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("Sec-Fetch-Site", "same-site")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("DNT", "1")
req.Header.Set("Priority", "u=1, i")
// Add cookies captured from captcha page
if cookies != "" {
req.Header.Set("Cookie", cookies)
}
client := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
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 result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
return result, nil
}
domain := "vk.com"
baseParams := fmt.Sprintf("session_token=%s&domain=%s&adFp=&access_token=",
url.QueryEscape(sessionToken), url.QueryEscape(domain))
// Step 1: settings
log.Printf("[Captcha] Step 1/4: settings")
_, err := vkReq("captchaNotRobot.settings", baseParams)
if err != nil {
return "", fmt.Errorf("settings failed: %w", err)
}
// HAR: settings → componentDone = 0.19s
time.Sleep(100*time.Millisecond + time.Duration(rand.Intn(100))*time.Millisecond)
// Step 2: componentDone
log.Printf("[Captcha] Step 2/4: componentDone")
browserFp := fmt.Sprintf("%032x", uint64(time.Now().UnixNano()))
deviceJSON := `{"screenWidth":1920,"screenHeight":1080,"screenAvailWidth":1920,"screenAvailHeight":1032,"innerWidth":1920,"innerHeight":945,"devicePixelRatio":1,"language":"en-US","languages":["en-US"],"webdriver":false,"hardwareConcurrency":16,"deviceMemory":8,"connectionEffectiveType":"4g","notificationsPermission":"denied"}`
componentData := baseParams + fmt.Sprintf("&browser_fp=%s&device=%s", browserFp, url.QueryEscape(deviceJSON))
_, err = vkReq("captchaNotRobot.componentDone", componentData)
if err != nil {
return "", fmt.Errorf("componentDone failed: %w", err)
}
// HAR: componentDone → check ≈ 1.95s + statEvents delay ≈ 3.2s total
time.Sleep(1500*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond)
// Step 3: check
log.Printf("[Captcha] Step 3/4: check")
cursorJSON := `[{"x":950,"y":500},{"x":945,"y":510},{"x":940,"y":520},{"x":938,"y":525},{"x":938,"y":525}]`
answer := base64.StdEncoding.EncodeToString([]byte("{}")) // e30=
// Generate random connectionDownlink values (simulating Network Information API)
// HAR shows browser repeats the same value 7 times: [9.8,9.8,9.8,9.8,9.8,9.8,9.8]
baseDownlink := 8.0 + rand.Float64()*4.0 // Random in [8.0, 12.0) for typical WiFi
downlinkStr := fmt.Sprintf("%.1f", baseDownlink)
connectionDownlink := "[" + downlinkStr + "," + downlinkStr + "," + downlinkStr + "," + downlinkStr + "," + downlinkStr + "," + downlinkStr + "," + downlinkStr + "]"
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",
url.QueryEscape("[]"), // accelerometer
url.QueryEscape("[]"), // gyroscope
url.QueryEscape("[]"), // motion
url.QueryEscape(cursorJSON), // cursor
url.QueryEscape("[]"), // taps
url.QueryEscape("[]"), // connectionRtt
url.QueryEscape(connectionDownlink),
browserFp, // browser_fp
hash, // hash (PoW result)
answer, // answer
vkDebugInfo, // debug_info (static)
)
checkResp, err := vkReq("captchaNotRobot.check", checkData)
if err != nil {
return "", fmt.Errorf("check request failed: %w", err)
}
respObj, ok := checkResp["response"].(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid check response: %v", checkResp)
}
status, _ := respObj["status"].(string)
if status != "OK" {
return "", fmt.Errorf("check response status: %s, full response: %v", status, checkResp)
}
successToken, ok := respObj["success_token"].(string)
if !ok || successToken == "" {
return "", fmt.Errorf("success_token not found in check response: %v", checkResp)
}
// HAR: check → endSession = 0.48s
time.Sleep(200*time.Millisecond + time.Duration(rand.Intn(300))*time.Millisecond)
// Step 4: endSession
log.Printf("[Captcha] Step 4/4: endSession")
vkReq("captchaNotRobot.endSession", baseParams)
return successToken, nil
}
Loading…
Cancel
Save