client.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. package main
  2. import (
  3. "bytes"
  4. "compress/gzip"
  5. "crypto/ed25519"
  6. "crypto/rand"
  7. "encoding/json"
  8. "encoding/pem"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "os"
  13. "time"
  14. "golang.org/x/crypto/ssh"
  15. )
  16. // APIClient handles communication with the server
  17. type APIClient struct {
  18. baseURL string
  19. token string
  20. httpClient *http.Client
  21. }
  22. // NewAPIClient creates a new API client
  23. func NewAPIClient(baseURL string) *APIClient {
  24. return &APIClient{
  25. baseURL: baseURL,
  26. httpClient: &http.Client{
  27. Timeout: 30 * time.Second,
  28. },
  29. }
  30. }
  31. // SetToken sets the authentication token
  32. func (c *APIClient) SetToken(token string) {
  33. c.token = token
  34. }
  35. // RegistrationRequest is sent to register a device
  36. type RegistrationRequest struct {
  37. DeviceID string `json:"device_id"`
  38. EthIP *string `json:"eth_ip,omitempty"`
  39. WlanIP *string `json:"wlan_ip,omitempty"`
  40. SSHPublicKey string `json:"ssh_public_key,omitempty"`
  41. }
  42. // RegistrationResponse is returned from registration
  43. type RegistrationResponse struct {
  44. DeviceToken string `json:"device_token"`
  45. DevicePassword string `json:"device_password,omitempty"`
  46. SSHTunnel struct {
  47. Enabled bool `json:"enabled"`
  48. RemotePort int `json:"remote_port"`
  49. Server string `json:"server"`
  50. } `json:"ssh_tunnel,omitempty"`
  51. }
  52. // Register registers a device with the server
  53. func (c *APIClient) Register(req *RegistrationRequest) (*RegistrationResponse, error) {
  54. body, err := json.Marshal(req)
  55. if err != nil {
  56. return nil, err
  57. }
  58. httpReq, err := http.NewRequest("POST", c.baseURL+"/registration", bytes.NewReader(body))
  59. if err != nil {
  60. return nil, err
  61. }
  62. httpReq.Header.Set("Content-Type", "application/json")
  63. resp, err := c.httpClient.Do(httpReq)
  64. if err != nil {
  65. return nil, err
  66. }
  67. defer resp.Body.Close()
  68. if resp.StatusCode != 201 && resp.StatusCode != 200 {
  69. respBody, _ := io.ReadAll(resp.Body)
  70. return nil, fmt.Errorf("registration failed: %d %s", resp.StatusCode, string(respBody))
  71. }
  72. var regResp RegistrationResponse
  73. if err := json.NewDecoder(resp.Body).Decode(&regResp); err != nil {
  74. return nil, err
  75. }
  76. return &regResp, nil
  77. }
  78. // ServerConfig is the configuration returned by the server
  79. type ServerConfig struct {
  80. // ForceCloud overrides local mode setting - for remote support
  81. ForceCloud bool `json:"force_cloud"`
  82. // ConfigPollingInterval in seconds (default: 30)
  83. ConfigPollingInterval int `json:"cfg_polling_timeout"`
  84. BLE struct {
  85. Enabled bool `json:"enabled"`
  86. BatchIntervalMs int `json:"batch_interval_ms"`
  87. UUIDFilterHex string `json:"uuid_filter_hex,omitempty"`
  88. UploadEndpoint string `json:"upload_endpoint,omitempty"`
  89. } `json:"ble"`
  90. WiFi struct {
  91. MonitorEnabled bool `json:"monitor_enabled"`
  92. ClientEnabled bool `json:"client_enabled"`
  93. SSID string `json:"ssid"`
  94. PSK string `json:"psk"`
  95. BatchIntervalMs int `json:"batch_interval_ms"`
  96. UploadEndpoint string `json:"upload_endpoint,omitempty"`
  97. } `json:"wifi"`
  98. SSHTunnel struct {
  99. Enabled bool `json:"enabled"`
  100. Server string `json:"server"`
  101. Port int `json:"port"`
  102. User string `json:"user"`
  103. RemotePort int `json:"remote_port"`
  104. KeepaliveInterval int `json:"keepalive_interval"`
  105. } `json:"ssh_tunnel"`
  106. DashboardTunnel struct {
  107. Enabled bool `json:"enabled"`
  108. Server string `json:"server"`
  109. Port int `json:"port"`
  110. User string `json:"user"`
  111. RemotePort int `json:"remote_port"`
  112. KeepaliveInterval int `json:"keepalive_interval"`
  113. } `json:"dashboard_tunnel"`
  114. Dashboard struct {
  115. Enabled bool `json:"enabled"`
  116. } `json:"dashboard"`
  117. Net struct {
  118. NTP struct {
  119. Servers []string `json:"servers"`
  120. } `json:"ntp"`
  121. } `json:"net"`
  122. Debug bool `json:"debug"`
  123. }
  124. // GetConfig fetches configuration from the server
  125. func (c *APIClient) GetConfig(deviceID string) (*ServerConfig, error) {
  126. httpReq, err := http.NewRequest("GET", c.baseURL+"/config", nil)
  127. if err != nil {
  128. return nil, err
  129. }
  130. q := httpReq.URL.Query()
  131. q.Set("device_id", deviceID)
  132. httpReq.URL.RawQuery = q.Encode()
  133. if c.token != "" {
  134. httpReq.Header.Set("Authorization", "Bearer "+c.token)
  135. }
  136. resp, err := c.httpClient.Do(httpReq)
  137. if err != nil {
  138. return nil, err
  139. }
  140. defer resp.Body.Close()
  141. if resp.StatusCode != 200 {
  142. return nil, fmt.Errorf("config fetch failed: %d", resp.StatusCode)
  143. }
  144. var cfg ServerConfig
  145. if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
  146. return nil, err
  147. }
  148. return &cfg, nil
  149. }
  150. // EventBatch is a batch of events to upload
  151. type EventBatch struct {
  152. DeviceID string `json:"device_id"`
  153. Events []interface{} `json:"events"`
  154. }
  155. // UploadEvents uploads a batch of events (gzipped)
  156. func (c *APIClient) UploadEvents(endpoint string, batch *EventBatch) error {
  157. // Serialize to JSON
  158. jsonData, err := json.Marshal(batch)
  159. if err != nil {
  160. return err
  161. }
  162. // Gzip compress
  163. var buf bytes.Buffer
  164. gw := gzip.NewWriter(&buf)
  165. if _, err := gw.Write(jsonData); err != nil {
  166. return err
  167. }
  168. if err := gw.Close(); err != nil {
  169. return err
  170. }
  171. // Store compressed data for retries
  172. compressedData := buf.Bytes()
  173. // Determine URL - use custom endpoint if it's a full URL, otherwise prepend baseURL
  174. var url string
  175. if len(endpoint) >= 7 && endpoint[:7] == "http://" {
  176. url = endpoint
  177. } else if len(endpoint) >= 8 && endpoint[:8] == "https://" {
  178. url = endpoint
  179. } else {
  180. url = c.baseURL + endpoint
  181. }
  182. // Send with retries
  183. var lastErr error
  184. for attempt := 1; attempt <= 3; attempt++ {
  185. // Create fresh request for each attempt (body can only be read once)
  186. httpReq, err := http.NewRequest("POST", url, bytes.NewReader(compressedData))
  187. if err != nil {
  188. return err
  189. }
  190. httpReq.Header.Set("Content-Type", "application/json")
  191. httpReq.Header.Set("Content-Encoding", "gzip")
  192. if c.token != "" {
  193. httpReq.Header.Set("Authorization", "Bearer "+c.token)
  194. }
  195. resp, err := c.httpClient.Do(httpReq)
  196. if err != nil {
  197. lastErr = err
  198. time.Sleep(time.Duration(attempt) * time.Second)
  199. continue
  200. }
  201. resp.Body.Close()
  202. if resp.StatusCode >= 200 && resp.StatusCode < 300 {
  203. return nil
  204. }
  205. lastErr = fmt.Errorf("upload failed: %d", resp.StatusCode)
  206. time.Sleep(time.Duration(attempt) * time.Second)
  207. }
  208. return lastErr
  209. }
  210. // WiFiCredentialsUpdate is the request to update WiFi credentials on server
  211. type WiFiCredentialsUpdate struct {
  212. SSID string `json:"ssid"`
  213. PSK string `json:"psk"`
  214. }
  215. // UpdateWiFiCredentials sends WiFi credentials to server (Cloud Mode only)
  216. func (c *APIClient) UpdateWiFiCredentials(ssid, psk string) error {
  217. body, err := json.Marshal(&WiFiCredentialsUpdate{
  218. SSID: ssid,
  219. PSK: psk,
  220. })
  221. if err != nil {
  222. return err
  223. }
  224. httpReq, err := http.NewRequest("POST", c.baseURL+"/wifi-credentials", bytes.NewReader(body))
  225. if err != nil {
  226. return err
  227. }
  228. httpReq.Header.Set("Content-Type", "application/json")
  229. if c.token != "" {
  230. httpReq.Header.Set("Authorization", "Bearer "+c.token)
  231. }
  232. resp, err := c.httpClient.Do(httpReq)
  233. if err != nil {
  234. return err
  235. }
  236. defer resp.Body.Close()
  237. if resp.StatusCode != 200 && resp.StatusCode != 201 {
  238. respBody, _ := io.ReadAll(resp.Body)
  239. return fmt.Errorf("wifi credentials update failed: %d %s", resp.StatusCode, string(respBody))
  240. }
  241. return nil
  242. }
  243. // GenerateOrLoadSSHKey generates ED25519 key pair or loads existing one
  244. // Returns OpenSSH public key format
  245. func GenerateOrLoadSSHKey(keyPath string) (string, error) {
  246. // Check if key already exists
  247. pubKeyPath := keyPath + ".pub"
  248. if _, err := os.Stat(keyPath); err == nil {
  249. // Key exists - read public key file
  250. pubKeyBytes, err := os.ReadFile(pubKeyPath)
  251. if err != nil {
  252. return "", fmt.Errorf("failed to read public key: %w", err)
  253. }
  254. return string(pubKeyBytes), nil
  255. }
  256. // Generate new ED25519 key pair
  257. pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
  258. if err != nil {
  259. return "", fmt.Errorf("failed to generate ED25519 key: %w", err)
  260. }
  261. // Convert to OpenSSH format for private key
  262. privKeyPEM, err := ssh.MarshalPrivateKey(privKey, "")
  263. if err != nil {
  264. return "", fmt.Errorf("failed to marshal private key: %w", err)
  265. }
  266. // Save private key
  267. privKeyFile, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
  268. if err != nil {
  269. return "", fmt.Errorf("failed to create private key file: %w", err)
  270. }
  271. defer privKeyFile.Close()
  272. if err := pem.Encode(privKeyFile, privKeyPEM); err != nil {
  273. return "", fmt.Errorf("failed to write private key: %w", err)
  274. }
  275. // Convert public key to OpenSSH format
  276. sshPubKey, err := ssh.NewPublicKey(pubKey)
  277. if err != nil {
  278. return "", fmt.Errorf("failed to create SSH public key: %w", err)
  279. }
  280. pubKeyStr := string(ssh.MarshalAuthorizedKey(sshPubKey))
  281. // Save public key
  282. if err := os.WriteFile(pubKeyPath, []byte(pubKeyStr), 0644); err != nil {
  283. return "", fmt.Errorf("failed to write public key: %w", err)
  284. }
  285. return pubKeyStr, nil
  286. }
  287. // TunnelPortReport is sent to report tunnel port allocation
  288. type TunnelPortReport struct {
  289. TunnelType string `json:"tunnel_type"` // "ssh" or "dashboard"
  290. Port *int `json:"port"` // Allocated port (nil if disconnected)
  291. Status string `json:"status"` // "connected" or "disconnected"
  292. }
  293. // ReportTunnelPort reports tunnel port allocation to server
  294. func (c *APIClient) ReportTunnelPort(tunnelType string, port int, status string) error {
  295. var portPtr *int
  296. if status == "connected" {
  297. portPtr = &port
  298. }
  299. body, err := json.Marshal(&TunnelPortReport{
  300. TunnelType: tunnelType,
  301. Port: portPtr,
  302. Status: status,
  303. })
  304. if err != nil {
  305. return err
  306. }
  307. httpReq, err := http.NewRequest("POST", c.baseURL+"/tunnel-port", bytes.NewReader(body))
  308. if err != nil {
  309. return err
  310. }
  311. httpReq.Header.Set("Content-Type", "application/json")
  312. if c.token != "" {
  313. httpReq.Header.Set("Authorization", "Bearer "+c.token)
  314. }
  315. resp, err := c.httpClient.Do(httpReq)
  316. if err != nil {
  317. return err
  318. }
  319. defer resp.Body.Close()
  320. if resp.StatusCode != 200 {
  321. respBody, _ := io.ReadAll(resp.Body)
  322. return fmt.Errorf("tunnel port report failed: %d %s", resp.StatusCode, string(respBody))
  323. }
  324. return nil
  325. }