mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-08 11:35:16 +03:00
Add call protocol, Rmux. Update AmneziaWG. Fixes and improvements
This commit is contained in:
284
transport/call/vk/api.go
Normal file
284
transport/call/vk/api.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package vk
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"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 TurnServer struct {
|
||||
URLs []string `json:"urls"`
|
||||
Username string `json:"username"`
|
||||
Credential string `json:"credential"`
|
||||
}
|
||||
|
||||
type StunServer struct {
|
||||
URLs []string `json:"urls"`
|
||||
}
|
||||
|
||||
type CallInfo struct {
|
||||
CallID string
|
||||
JoinLink string
|
||||
ShortLink string
|
||||
OKJoinLink string
|
||||
TurnServer TurnServer
|
||||
StunServer StunServer
|
||||
WSEndpoint string
|
||||
}
|
||||
|
||||
type vkTokenResponse struct {
|
||||
Data struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type callSettingsResponse struct {
|
||||
Response struct {
|
||||
Settings struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
} `json:"settings"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type callTokenResponse struct {
|
||||
Response struct {
|
||||
Token string `json:"token"`
|
||||
APIBaseURL string `json:"api_base_url"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type okAuthResponse struct {
|
||||
SessionKey string `json:"session_key"`
|
||||
}
|
||||
|
||||
type joinResponse struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
TurnServer TurnServer `json:"turn_server"`
|
||||
StunServer StunServer `json:"stun_server"`
|
||||
}
|
||||
|
||||
func JoinExistingCall(dialer N.Dialer, cookieStr, vkLink string, cfg VKConfig, logger logger.ContextLogger) (*CallInfo, error) {
|
||||
if cfg.AppID == "" || cfg.APIVersion == "" {
|
||||
return nil, fmt.Errorf("config incomplete: app_id=%q api=%q", cfg.AppID, cfg.APIVersion)
|
||||
}
|
||||
token := extractJoinToken(vkLink)
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("could not extract join token from %q", vkLink)
|
||||
}
|
||||
logger.Info(fmt.Sprintf("[auth] Joining existing call token=%s", token))
|
||||
resp, err := authAndJoin(dialer, cookieStr, token, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CallInfo{
|
||||
JoinLink: vkLink,
|
||||
OKJoinLink: token,
|
||||
TurnServer: resp.TurnServer,
|
||||
StunServer: resp.StunServer,
|
||||
WSEndpoint: resp.Endpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func CreateAndJoinCall(dialer N.Dialer, cookieStr, peerId string, cfg VKConfig, logger logger.ContextLogger) (*CallInfo, error) {
|
||||
if cfg.AppID == "" || cfg.APIVersion == "" {
|
||||
return nil, fmt.Errorf("config incomplete: app_id=%q api=%q", cfg.AppID, cfg.APIVersion)
|
||||
}
|
||||
auth := func(bearer string) map[string]string {
|
||||
return map[string]string{"Authorization": "Bearer " + bearer}
|
||||
}
|
||||
logger.Info("[auth] Getting VK token...")
|
||||
r, err := httpPost(dialer, "https://login.vk.com/?act=web_token",
|
||||
url.Values{"version": {"1"}, "app_id": {cfg.AppID}},
|
||||
map[string]string{"Cookie": cookieStr})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("web_token: %w", err)
|
||||
}
|
||||
var tok vkTokenResponse
|
||||
json.Unmarshal(r, &tok)
|
||||
vkToken := tok.Data.AccessToken
|
||||
if vkToken == "" {
|
||||
return nil, fmt.Errorf("empty VK token, response: %s", string(r))
|
||||
}
|
||||
logger.Info(fmt.Sprintf("[auth] Creating call peer_id=%s...", peerId))
|
||||
r, err = httpPost(dialer, "https://api.vk.com/method/calls.start",
|
||||
url.Values{"v": {cfg.APIVersion}, "peer_id": {peerId}}, auth(vkToken))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("calls.start: %w", err)
|
||||
}
|
||||
var call struct {
|
||||
Response struct {
|
||||
CallID string `json:"call_id"`
|
||||
JoinLink string `json:"join_link"`
|
||||
OKJoinLink string `json:"ok_join_link"`
|
||||
ShortCredentials struct {
|
||||
LinkWithPassword string `json:"link_with_password"`
|
||||
} `json:"short_credentials"`
|
||||
} `json:"response"`
|
||||
}
|
||||
json.Unmarshal(r, &call)
|
||||
c := call.Response
|
||||
if c.CallID == "" {
|
||||
return nil, fmt.Errorf("empty call_id, response: %s", string(r))
|
||||
}
|
||||
if c.OKJoinLink == "" {
|
||||
return nil, fmt.Errorf("empty ok_join_link, response: %s", string(r))
|
||||
}
|
||||
logger.Debug(fmt.Sprintf("[auth] call_id: %s", c.CallID))
|
||||
logger.Debug(fmt.Sprintf("[auth] join_link: %s", c.JoinLink))
|
||||
logger.Info("[auth] Joining conversation...")
|
||||
resp, err := authAndJoin(dialer, cookieStr, c.OKJoinLink, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CallInfo{
|
||||
CallID: c.CallID, JoinLink: c.JoinLink, ShortLink: c.ShortCredentials.LinkWithPassword,
|
||||
OKJoinLink: c.OKJoinLink, TurnServer: resp.TurnServer, StunServer: resp.StunServer,
|
||||
WSEndpoint: resp.Endpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func BuildICEServers(callInfo *CallInfo) []ICEServerSpec {
|
||||
var servers []ICEServerSpec
|
||||
if len(callInfo.StunServer.URLs) > 0 {
|
||||
servers = append(servers, ICEServerSpec{URLs: callInfo.StunServer.URLs})
|
||||
}
|
||||
if len(callInfo.TurnServer.URLs) > 0 {
|
||||
urls := append([]string{}, callInfo.TurnServer.URLs...)
|
||||
urls = append(urls, urls[len(urls)-1]+"?transport=tcp")
|
||||
servers = append(servers, ICEServerSpec{
|
||||
URLs: urls, Username: callInfo.TurnServer.Username, Credential: callInfo.TurnServer.Credential,
|
||||
})
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
type ICEServerSpec struct {
|
||||
URLs []string
|
||||
Username string
|
||||
Credential string
|
||||
}
|
||||
|
||||
func authAndJoin(dialer N.Dialer, cookieStr, okJoinLink string, cfg VKConfig) (*joinResponse, error) {
|
||||
auth := func(bearer string) map[string]string {
|
||||
return map[string]string{"Authorization": "Bearer " + bearer}
|
||||
}
|
||||
r, err := httpPost(dialer, "https://login.vk.com/?act=web_token",
|
||||
url.Values{"version": {"1"}, "app_id": {cfg.AppID}},
|
||||
map[string]string{"Cookie": cookieStr})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("web_token: %w", err)
|
||||
}
|
||||
var tok vkTokenResponse
|
||||
json.Unmarshal(r, &tok)
|
||||
if tok.Data.AccessToken == "" {
|
||||
return nil, fmt.Errorf("empty VK token, response: %s", string(r))
|
||||
}
|
||||
r, err = httpPost(dialer, "https://api.vk.com/method/calls.getSettings",
|
||||
url.Values{"v": {cfg.APIVersion}}, auth(tok.Data.AccessToken))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("calls.getSettings: %w", err)
|
||||
}
|
||||
var settings callSettingsResponse
|
||||
json.Unmarshal(r, &settings)
|
||||
appKey := settings.Response.Settings.PublicKey
|
||||
if appKey == "" {
|
||||
return nil, fmt.Errorf("empty public_key, response: %s", string(r))
|
||||
}
|
||||
r, err = httpPost(dialer, "https://api.vk.com/method/messages.getCallToken",
|
||||
url.Values{"v": {cfg.APIVersion}, "env": {"production"}}, auth(tok.Data.AccessToken))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("messages.getCallToken: %w", err)
|
||||
}
|
||||
var callToken callTokenResponse
|
||||
json.Unmarshal(r, &callToken)
|
||||
if callToken.Response.Token == "" {
|
||||
return nil, fmt.Errorf("empty call token, response: %s", string(r))
|
||||
}
|
||||
if callToken.Response.APIBaseURL == "" {
|
||||
return nil, fmt.Errorf("empty api_base_url, response: %s", string(r))
|
||||
}
|
||||
apiBaseURL := strings.TrimRight(callToken.Response.APIBaseURL, "/")
|
||||
if !strings.HasSuffix(apiBaseURL, "/fb.do") {
|
||||
apiBaseURL += "/fb.do"
|
||||
}
|
||||
sd, _ := json.Marshal(map[string]interface{}{
|
||||
"device_id": "sing-box-go-1", "client_version": cfg.AppVersion,
|
||||
"client_type": "SDK_JS", "auth_token": callToken.Response.Token, "version": 3,
|
||||
})
|
||||
r, err = httpPost(dialer, apiBaseURL, url.Values{
|
||||
"method": {"auth.anonymLogin"}, "application_key": {appKey},
|
||||
"format": {"json"}, "session_data": {string(sd)},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auth.anonymLogin: %w", err)
|
||||
}
|
||||
var okAuth okAuthResponse
|
||||
json.Unmarshal(r, &okAuth)
|
||||
if okAuth.SessionKey == "" {
|
||||
return nil, fmt.Errorf("empty session_key, response: %s", string(r))
|
||||
}
|
||||
ms, _ := json.Marshal(map[string]bool{
|
||||
"isAudioEnabled": false, "isVideoEnabled": true, "isScreenSharingEnabled": false,
|
||||
})
|
||||
r, err = httpPost(dialer, apiBaseURL, url.Values{
|
||||
"method": {"vchat.joinConversationByLink"}, "session_key": {okAuth.SessionKey},
|
||||
"application_key": {appKey}, "format": {"json"}, "joinLink": {okJoinLink},
|
||||
"isVideo": {"true"}, "isAudio": {"false"}, "mediaSettings": {string(ms)},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("vchat.joinConversationByLink: %w", err)
|
||||
}
|
||||
var jr joinResponse
|
||||
json.Unmarshal(r, &jr)
|
||||
if jr.Endpoint == "" {
|
||||
return nil, fmt.Errorf("empty WS endpoint, response: %s", string(r))
|
||||
}
|
||||
return &jr, nil
|
||||
}
|
||||
|
||||
func extractJoinToken(link string) string {
|
||||
link = strings.TrimSpace(link)
|
||||
if link == "" {
|
||||
return ""
|
||||
}
|
||||
if u, err := url.Parse(link); err == nil && u.Scheme != "" {
|
||||
path := strings.Trim(u.Path, "/")
|
||||
if path != "" {
|
||||
parts := strings.Split(path, "/")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
if !strings.ContainsAny(link, "/?&=") {
|
||||
return link
|
||||
}
|
||||
parts := strings.Split(strings.TrimRight(link, "/"), "/")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
func httpPost(dialer N.Dialer, endpoint string, form url.Values, extraHeaders map[string]string) ([]byte, error) {
|
||||
body := form.Encode()
|
||||
req, err := http.NewRequest("POST", endpoint, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("User-Agent", common.UserAgent)
|
||||
req.Header.Set("Origin", "https://vk.com")
|
||||
req.Header.Set("Referer", "https://vk.com/")
|
||||
for k, v := range extraHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := common.HttpClient(dialer).Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
Reference in New Issue
Block a user