//go:build tdlib && cgo package main /* #cgo LDFLAGS: -ltdjson #cgo CFLAGS: -I/usr/include -I/usr/local/include #include #include */ import "C" import ( "encoding/hex" "encoding/json" "fmt" "os" "strconv" "strings" "time" "unsafe" "mtproxy_checker/internal/parseurl" "mtproxy_checker/internal/secret" ) func main() { if len(os.Args) < 2 { outJSON(false, "usage: tdlib_ping ", 2) } run(os.Args[1]) } func outJSON(ok bool, msg string, code int) { enc := json.NewEncoder(os.Stdout) enc.SetEscapeHTML(false) _ = enc.Encode(map[string]interface{}{ "ok": ok, "error": msg, "exit_code": code, }) if ok { os.Exit(0) } switch code { case 1: os.Exit(1) case 4: os.Exit(4) default: os.Exit(2) } } func run(proxyURL string) { deadlineMs, _ := strconv.Atoi(os.Getenv("MTPROXY_TDLIB_PING_MS")) if deadlineMs <= 0 { deadlineMs = 45000 } deadline := time.Now().Add(time.Duration(deadlineMs) * time.Millisecond) t, err := parseurl.ParseTGProxy(proxyURL) if err != nil { outJSON(false, err.Error(), 1) } parsed, err := secret.Parse(t.Secret) if err != nil { outJSON(false, err.Error(), 1) } secHex := mtprotoSecretHex(parsed) apiID := int64(12345) if v := strings.TrimSpace(os.Getenv("MTPROXY_TD_API_ID")); v != "" { if n, e := strconv.ParseInt(v, 10, 32); e == nil { apiID = n } } apiHash := strings.TrimSpace(os.Getenv("MTPROXY_TD_API_HASH")) if apiHash == "" { apiHash = "0123456789abcdef0123456789abcdef" } useTest := strings.TrimSpace(os.Getenv("MTPROXY_TD_USE_TEST_DC")) == "1" dbBase := strings.TrimSpace(os.Getenv("MTPROXY_TDLIB_DATABASE_DIR")) var cleanupTemp string if dbBase == "" { cleanupTemp, err = os.MkdirTemp("", "mtproxy_tdlib_") if err != nil { outJSON(false, err.Error(), 2) } dbBase = cleanupTemp defer func() { _ = os.RemoveAll(cleanupTemp) }() } verb := `{"@type":"setLogVerbosityLevel","new_verbosity_level":1}` if cs := C.CString(verb); cs != nil { _ = C.td_execute(cs) C.free(unsafe.Pointer(cs)) } clientID := int(C.td_create_client_id()) defer closeClient(clientID) if !waitAuthState(clientID, "authorizationStateWaitTdlibParameters", deadline) { outJSON(false, "timeout waiting for authorizationStateWaitTdlibParameters", 4) } params := map[string]interface{}{ "@type": "setTdlibParameters", "use_test_dc": useTest, "database_directory": dbBase + "/db", "files_directory": dbBase + "/files", "use_file_database": true, "use_chat_info_database": true, "use_message_database": true, "use_secret_chats": true, "api_id": apiID, "api_hash": apiHash, "system_language_code": "en", "device_model": "mtproxy-tdlib-ping", "application_version": "1.0", } if err := os.MkdirAll(dbBase+"/db", 0o700); err != nil { outJSON(false, err.Error(), 2) } if err := os.MkdirAll(dbBase+"/files", 0o700); err != nil { outJSON(false, err.Error(), 2) } tdSend(clientID, params) for time.Now().Before(deadline) { ev := tdReceive(1.0) if ev == nil { continue } if typ, _ := ev["@type"].(string); typ == "error" { outJSON(false, fmt.Sprintf("tdlib: %v", ev["message"]), 2) } switch authStateType(ev) { case "authorizationStateWaitEncryptionKey": tdSend(clientID, map[string]interface{}{ "@type": "checkDatabaseEncryptionKey", "encryption_key": "", }) case "authorizationStateReady": goto doProxy case "authorizationStateWaitPhoneNumber": outJSON(false, "TDLib authorizationStateWaitPhoneNumber: use a persistent MTPROXY_TDLIB_DATABASE_DIR after TDLib login, or set MTPROXY_TD_USE_TEST_DC=1", 2) case "authorizationStateClosed": outJSON(false, "unexpected authorizationStateClosed before ping", 2) } } outJSON(false, "timeout in TDLib authorization", 4) doProxy: addBody := map[string]interface{}{ "@type": "addProxy", "server": t.Host, "port": t.Port, "enable": true, "type": map[string]interface{}{ "@type": "proxyTypeMtproto", "secret": secHex, }, "@extra": "addproxy", } tdSend(clientID, addBody) var proxyID int64 for time.Now().Before(deadline) { ev := tdReceive(1.0) if ev == nil { continue } if matchExtra(ev, "addproxy") { if typ, _ := ev["@type"].(string); typ == "error" { outJSON(false, fmt.Sprintf("addProxy: %v", ev["message"]), 2) } if typ, _ := ev["@type"].(string); typ == "proxy" { if idf, ok := ev["id"].(float64); ok { proxyID = int64(idf) break } } outJSON(false, fmt.Sprintf("addProxy unexpected: %#v", ev), 2) } } if proxyID == 0 { outJSON(false, "timeout waiting for addProxy response", 4) } tdSend(clientID, map[string]interface{}{ "@type": "pingProxy", "proxy_id": proxyID, "@extra": "pingpx", }) for time.Now().Before(deadline) { ev := tdReceive(1.0) if ev == nil { continue } if matchExtra(ev, "pingpx") { if typ, _ := ev["@type"].(string); typ == "ok" { outJSON(true, "", 0) } if typ, _ := ev["@type"].(string); typ == "error" { outJSON(false, fmt.Sprintf("pingProxy: %v", ev["message"]), 2) } outJSON(false, fmt.Sprintf("pingProxy unexpected: %#v", ev), 2) } } outJSON(false, "timeout waiting for pingProxy", 4) } func mtprotoSecretHex(p *secret.Parsed) string { var full []byte switch p.Kind { case secret.KindDD: full = append([]byte{0xdd}, p.Key...) case secret.KindEE: full = append(append([]byte{0xee}, p.Key...), p.Domain...) default: return "" } return hex.EncodeToString(full) } func authStateType(m map[string]interface{}) string { if m == nil { return "" } if m["@type"] != "updateAuthorizationState" { return "" } v, _ := m["authorization_state"].(map[string]interface{}) if v == nil { return "" } t, _ := v["@type"].(string) return t } func matchExtra(m map[string]interface{}, want string) bool { if m == nil { return false } ext, ok := m["@extra"].(string) return ok && ext == want } func waitAuthState(clientID int, want string, deadline time.Time) bool { for time.Now().Before(deadline) { ev := tdReceive(1.0) if ev == nil { continue } if authStateType(ev) == want { return true } } return false } func tdSend(clientID int, v interface{}) { b, err := json.Marshal(v) if err != nil { outJSON(false, err.Error(), 2) } cs := C.CString(string(b)) defer C.free(unsafe.Pointer(cs)) C.td_send(C.int(clientID), cs) } func tdReceive(maxSec float64) map[string]interface{} { cs := C.td_receive(C.double(maxSec)) if cs == nil { return nil } s := C.GoString(cs) var m map[string]interface{} if err := json.Unmarshal([]byte(s), &m); err != nil { return nil } return m } func closeClient(clientID int) { tdSend(clientID, map[string]interface{}{"@type": "close"}) deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { ev := tdReceive(0.5) if ev == nil { continue } if authStateType(ev) == "authorizationStateClosed" { break } } }