client.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. package main
  2. import (
  3. "bytes"
  4. "compress/gzip"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "time"
  10. )
  11. // APIClient handles communication with the server
  12. type APIClient struct {
  13. baseURL string
  14. token string
  15. httpClient *http.Client
  16. }
  17. // NewAPIClient creates a new API client
  18. func NewAPIClient(baseURL string) *APIClient {
  19. return &APIClient{
  20. baseURL: baseURL,
  21. httpClient: &http.Client{
  22. Timeout: 30 * time.Second,
  23. },
  24. }
  25. }
  26. // SetToken sets the authentication token
  27. func (c *APIClient) SetToken(token string) {
  28. c.token = token
  29. }
  30. // RegistrationRequest is sent to register a device
  31. type RegistrationRequest struct {
  32. DeviceID string `json:"device_id"`
  33. EthIP *string `json:"eth_ip,omitempty"`
  34. WlanIP *string `json:"wlan_ip,omitempty"`
  35. }
  36. // RegistrationResponse is returned from registration
  37. type RegistrationResponse struct {
  38. DeviceToken string `json:"device_token"`
  39. DevicePassword string `json:"device_password,omitempty"`
  40. SSHTunnel struct {
  41. Enabled bool `json:"enabled"`
  42. RemotePort int `json:"remote_port"`
  43. Server string `json:"server"`
  44. } `json:"ssh_tunnel,omitempty"`
  45. }
  46. // Register registers a device with the server
  47. func (c *APIClient) Register(req *RegistrationRequest) (*RegistrationResponse, error) {
  48. body, err := json.Marshal(req)
  49. if err != nil {
  50. return nil, err
  51. }
  52. httpReq, err := http.NewRequest("POST", c.baseURL+"/registration", bytes.NewReader(body))
  53. if err != nil {
  54. return nil, err
  55. }
  56. httpReq.Header.Set("Content-Type", "application/json")
  57. resp, err := c.httpClient.Do(httpReq)
  58. if err != nil {
  59. return nil, err
  60. }
  61. defer resp.Body.Close()
  62. if resp.StatusCode != 201 && resp.StatusCode != 200 {
  63. respBody, _ := io.ReadAll(resp.Body)
  64. return nil, fmt.Errorf("registration failed: %d %s", resp.StatusCode, string(respBody))
  65. }
  66. var regResp RegistrationResponse
  67. if err := json.NewDecoder(resp.Body).Decode(&regResp); err != nil {
  68. return nil, err
  69. }
  70. return &regResp, nil
  71. }
  72. // ServerConfig is the configuration returned by the server
  73. type ServerConfig struct {
  74. // ForceCloud overrides local mode setting - for remote support
  75. ForceCloud bool `json:"force_cloud"`
  76. BLE struct {
  77. Enabled bool `json:"enabled"`
  78. BatchIntervalMs int `json:"batch_interval_ms"`
  79. UUIDFilterHex string `json:"uuid_filter_hex,omitempty"`
  80. } `json:"ble"`
  81. WiFi struct {
  82. MonitorEnabled bool `json:"monitor_enabled"`
  83. ClientEnabled bool `json:"client_enabled"`
  84. SSID string `json:"ssid"`
  85. PSK string `json:"psk"`
  86. BatchIntervalMs int `json:"batch_interval_ms"`
  87. } `json:"wifi"`
  88. SSHTunnel struct {
  89. Enabled bool `json:"enabled"`
  90. Server string `json:"server"`
  91. Port int `json:"port"`
  92. User string `json:"user"`
  93. RemotePort int `json:"remote_port"`
  94. KeepaliveInterval int `json:"keepalive_interval"`
  95. } `json:"ssh_tunnel"`
  96. Dashboard struct {
  97. Enabled bool `json:"enabled"`
  98. } `json:"dashboard"`
  99. Net struct {
  100. NTP struct {
  101. Servers []string `json:"servers"`
  102. } `json:"ntp"`
  103. } `json:"net"`
  104. Debug bool `json:"debug"`
  105. }
  106. // GetConfig fetches configuration from the server
  107. func (c *APIClient) GetConfig(deviceID string) (*ServerConfig, error) {
  108. httpReq, err := http.NewRequest("GET", c.baseURL+"/config", nil)
  109. if err != nil {
  110. return nil, err
  111. }
  112. q := httpReq.URL.Query()
  113. q.Set("device_id", deviceID)
  114. httpReq.URL.RawQuery = q.Encode()
  115. if c.token != "" {
  116. httpReq.Header.Set("Authorization", "Bearer "+c.token)
  117. }
  118. resp, err := c.httpClient.Do(httpReq)
  119. if err != nil {
  120. return nil, err
  121. }
  122. defer resp.Body.Close()
  123. if resp.StatusCode != 200 {
  124. return nil, fmt.Errorf("config fetch failed: %d", resp.StatusCode)
  125. }
  126. var cfg ServerConfig
  127. if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
  128. return nil, err
  129. }
  130. return &cfg, nil
  131. }
  132. // EventBatch is a batch of events to upload
  133. type EventBatch struct {
  134. DeviceID string `json:"device_id"`
  135. Events []interface{} `json:"events"`
  136. }
  137. // UploadEvents uploads a batch of events (gzipped)
  138. func (c *APIClient) UploadEvents(endpoint string, batch *EventBatch) error {
  139. // Serialize to JSON
  140. jsonData, err := json.Marshal(batch)
  141. if err != nil {
  142. return err
  143. }
  144. // Gzip compress
  145. var buf bytes.Buffer
  146. gw := gzip.NewWriter(&buf)
  147. if _, err := gw.Write(jsonData); err != nil {
  148. return err
  149. }
  150. if err := gw.Close(); err != nil {
  151. return err
  152. }
  153. // Store compressed data for retries
  154. compressedData := buf.Bytes()
  155. url := c.baseURL + endpoint
  156. // Send with retries
  157. var lastErr error
  158. for attempt := 1; attempt <= 3; attempt++ {
  159. // Create fresh request for each attempt (body can only be read once)
  160. httpReq, err := http.NewRequest("POST", url, bytes.NewReader(compressedData))
  161. if err != nil {
  162. return err
  163. }
  164. httpReq.Header.Set("Content-Type", "application/json")
  165. httpReq.Header.Set("Content-Encoding", "gzip")
  166. if c.token != "" {
  167. httpReq.Header.Set("Authorization", "Bearer "+c.token)
  168. }
  169. resp, err := c.httpClient.Do(httpReq)
  170. if err != nil {
  171. lastErr = err
  172. time.Sleep(time.Duration(attempt) * time.Second)
  173. continue
  174. }
  175. resp.Body.Close()
  176. if resp.StatusCode >= 200 && resp.StatusCode < 300 {
  177. return nil
  178. }
  179. lastErr = fmt.Errorf("upload failed: %d", resp.StatusCode)
  180. time.Sleep(time.Duration(attempt) * time.Second)
  181. }
  182. return lastErr
  183. }
  184. // WiFiCredentialsUpdate is the request to update WiFi credentials on server
  185. type WiFiCredentialsUpdate struct {
  186. SSID string `json:"ssid"`
  187. PSK string `json:"psk"`
  188. }
  189. // UpdateWiFiCredentials sends WiFi credentials to server (Cloud Mode only)
  190. func (c *APIClient) UpdateWiFiCredentials(ssid, psk string) error {
  191. body, err := json.Marshal(&WiFiCredentialsUpdate{
  192. SSID: ssid,
  193. PSK: psk,
  194. })
  195. if err != nil {
  196. return err
  197. }
  198. httpReq, err := http.NewRequest("POST", c.baseURL+"/wifi-credentials", bytes.NewReader(body))
  199. if err != nil {
  200. return err
  201. }
  202. httpReq.Header.Set("Content-Type", "application/json")
  203. if c.token != "" {
  204. httpReq.Header.Set("Authorization", "Bearer "+c.token)
  205. }
  206. resp, err := c.httpClient.Do(httpReq)
  207. if err != nil {
  208. return err
  209. }
  210. defer resp.Body.Close()
  211. if resp.StatusCode != 200 && resp.StatusCode != 201 {
  212. respBody, _ := io.ReadAll(resp.Body)
  213. return fmt.Errorf("wifi credentials update failed: %d %s", resp.StatusCode, string(respBody))
  214. }
  215. return nil
  216. }