package vk import ( "encoding/json" "fmt" "io" "math/rand" "net/http" "net/url" "strings" "github.com/sagernet/sing-box/transport/call/common" "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" ) type vkAuthConfig struct { AppID string `json:"appID"` ApiVersion string `json:"apiVersion"` AppVersion string `json:"appVersion"` ProtocolVersion string `json:"protocolVersion"` PublicKey string `json:"publicKey,omitempty"` OkJoinLink string `json:"okJoinLink,omitempty"` } type vkCaptchaError struct { captchaSid string redirectURI string captchaTs string captchaAttempt string } func RunVKAuth(dialer N.Dialer, joinLink, displayName string, logger logger.ContextLogger) (string, error) { client := common.HttpClient(dialer) httpPost := func(targetURL string, form url.Values, extraHeaders map[string]string) (map[string]interface{}, error) { req, _ := http.NewRequest("POST", targetURL, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("User-Agent", common.UserAgent) req.Header.Set("Origin", "https://vk.ru") req.Header.Set("Referer", "https://vk.ru/") for k, v := range extraHeaders { req.Header.Set(k, v) } 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, fmt.Errorf("json: %w (body: %s)", err, string(body[:minInt(len(body), 200)])) } return result, nil } cfg := &vkAuthConfig{ AppID: "6287487", ApiVersion: "5.282", AppVersion: "1.1", ProtocolVersion: "5", } logger.Debug(fmt.Sprintf("vk-auth: appID=%s api=%s appVersion=%s proto=%s", cfg.AppID, cfg.ApiVersion, cfg.AppVersion, cfg.ProtocolVersion)) logger.Info("vk-auth: getting anonymous token") anonResp, err := httpPost("https://login.vk.ru/?act=get_anonym_token", url.Values{ "client_id": {cfg.AppID}, }, nil) if err != nil { return "", fmt.Errorf("get_anonym_token: %w", err) } dataMap, _ := anonResp["data"].(map[string]interface{}) accessToken, _ := dataMap["access_token"].(string) if accessToken == "" { return "", fmt.Errorf("empty access_token: %v", anonResp) } logger.Debug("vk-auth: anon token OK") auth := map[string]string{"Authorization": "Bearer " + accessToken} logger.Info("vk-auth: getting call settings") settingsResp, err := httpPost("https://api.vk.ru/method/calls.getSettings", url.Values{ "v": {cfg.ApiVersion}, }, auth) if err != nil { return "", fmt.Errorf("calls.getSettings: %w", err) } if respObj, ok := settingsResp["response"].(map[string]interface{}); ok { if settings, ok := respObj["settings"].(map[string]interface{}); ok { if pk, ok := settings["public_key"].(string); ok { cfg.PublicKey = pk } } } logger.Debug(fmt.Sprintf("vk-auth: publicKey=%s", cfg.PublicKey)) logger.Info("vk-auth: getting call preview") previewResp, err := httpPost("https://api.vk.ru/method/calls.getCallPreview", url.Values{ "v": {cfg.ApiVersion}, "vk_join_link": {joinLink}, }, auth) if err == nil { if respObj, ok := previewResp["response"].(map[string]interface{}); ok { if okLink, ok := respObj["ok_join_link"].(string); ok { cfg.OkJoinLink = okLink } } } logger.Info("vk-auth: getting call token") callParams := url.Values{ "v": {cfg.ApiVersion}, "vk_join_link": {joinLink}, "name": {displayName}, } var callToken string var apiBaseURL string var okJoinLink string for attempt := 0; attempt < 5; attempt++ { callResp, err := httpPost("https://api.vk.ru/method/calls.getAnonymousToken", callParams, auth) if err != nil { return "", fmt.Errorf("getAnonymousToken: %w", err) } if errObj, hasErr := callResp["error"].(map[string]interface{}); hasErr { errCode, _ := errObj["error_code"].(float64) if int(errCode) == 14 { captchaErr := parseVKCaptchaError(errObj) if captchaErr == nil { return "", fmt.Errorf("captcha error missing fields: %v", errObj) } logger.Info("vk-auth: captcha required") proxyPort := StartCaptchaProxy(captchaErr.redirectURI, dialer) if proxyPort == 0 { return "", fmt.Errorf("failed to start captcha proxy") } logger.Notice(fmt.Sprintf("vk-auth: solve the captcha to continue: http://127.0.0.1:%d/", proxyPort)) successToken := GetCaptchaResult() StopCaptchaProxy() if successToken == "" { return "", fmt.Errorf("captcha timed out") } logger.Info("vk-auth: captcha solved, retrying") captchaAttempt := captchaErr.captchaAttempt if captchaAttempt == "" || captchaAttempt == "0" { captchaAttempt = "1" } callParams = url.Values{ "v": {cfg.ApiVersion}, "vk_join_link": {joinLink}, "name": {displayName}, "captcha_key": {""}, "captcha_sid": {captchaErr.captchaSid}, "is_sound_captcha": {"0"}, "success_token": {successToken}, "captcha_ts": {captchaErr.captchaTs}, "captcha_attempt": {captchaAttempt}, } continue } return "", fmt.Errorf("VK API error: %v", errObj) } respMap, ok := callResp["response"].(map[string]interface{}) if !ok { return "", fmt.Errorf("unexpected response: %v", callResp) } callToken, _ = respMap["token"].(string) apiBaseURL, _ = respMap["api_base_url"].(string) okJoinLink, _ = respMap["ok_join_link"].(string) break } if callToken == "" { return "", fmt.Errorf("failed to get call token") } logger.Info("vk-auth: authenticating with OK.ru") baseURL := strings.TrimRight(apiBaseURL, "/") if !strings.HasSuffix(baseURL, "/fb.do") { baseURL += "/fb.do" } deviceID := fmt.Sprintf("%d", rand.Int63n(9e18)) sessionData, _ := json.Marshal(map[string]interface{}{ "version": 2, "device_id": deviceID, "client_version": cfg.AppVersion, "client_type": "SDK_JS", }) okResp, err := httpPost(baseURL, url.Values{ "method": {"auth.anonymLogin"}, "session_data": {string(sessionData)}, "application_key": {cfg.PublicKey}, "format": {"json"}, }, nil) if err != nil { return "", fmt.Errorf("anonymLogin: %w", err) } sessionKey, _ := okResp["session_key"].(string) if sessionKey == "" { return "", fmt.Errorf("missing session_key: %v", okResp) } logger.Debug("vk-auth: OK.ru session OK") finalJoinLink := okJoinLink if finalJoinLink == "" { finalJoinLink = cfg.OkJoinLink } if finalJoinLink == "" { finalJoinLink = joinLink } result := map[string]string{ "sessionKey": sessionKey, "applicationKey": cfg.PublicKey, "apiBaseURL": baseURL, "joinLink": finalJoinLink, "anonymToken": callToken, "appVersion": cfg.AppVersion, "protocolVersion": cfg.ProtocolVersion, } jsonBytes, _ := json.Marshal(result) logger.Debug("vk-auth: done") return string(jsonBytes), nil } func parseVKCaptchaError(errObj map[string]interface{}) *vkCaptchaError { redirectURI, _ := errObj["redirect_uri"].(string) if redirectURI == "" { return nil } captchaSid := "" if sid, ok := errObj["captcha_sid"].(string); ok { captchaSid = sid } else if sidNum, ok := errObj["captcha_sid"].(float64); ok { captchaSid = fmt.Sprintf("%.0f", sidNum) } captchaTs, _ := errObj["captcha_ts"].(string) captchaAttempt, _ := errObj["captcha_attempt"].(string) return &vkCaptchaError{ captchaSid: captchaSid, redirectURI: redirectURI, captchaTs: captchaTs, captchaAttempt: captchaAttempt, } } func minInt(a, b int) int { if a < b { return a } return b }