package main import ( "bytes" "compress/gzip" "encoding/json" "fmt" "io" "net/http" "time" ) // APIClient handles communication with the server type APIClient struct { baseURL string token string httpClient *http.Client } // NewAPIClient creates a new API client func NewAPIClient(baseURL string) *APIClient { return &APIClient{ baseURL: baseURL, httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } // SetToken sets the authentication token func (c *APIClient) SetToken(token string) { c.token = token } // RegistrationRequest is sent to register a device type RegistrationRequest struct { DeviceID string `json:"device_id"` EthIP *string `json:"eth_ip,omitempty"` WlanIP *string `json:"wlan_ip,omitempty"` } // RegistrationResponse is returned from registration type RegistrationResponse struct { DeviceToken string `json:"device_token"` DevicePassword string `json:"device_password,omitempty"` SSHTunnel struct { Enabled bool `json:"enabled"` RemotePort int `json:"remote_port"` Server string `json:"server"` } `json:"ssh_tunnel,omitempty"` } // Register registers a device with the server func (c *APIClient) Register(req *RegistrationRequest) (*RegistrationResponse, error) { body, err := json.Marshal(req) if err != nil { return nil, err } httpReq, err := http.NewRequest("POST", c.baseURL+"/registration", bytes.NewReader(body)) if err != nil { return nil, err } httpReq.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 201 && resp.StatusCode != 200 { respBody, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("registration failed: %d %s", resp.StatusCode, string(respBody)) } var regResp RegistrationResponse if err := json.NewDecoder(resp.Body).Decode(®Resp); err != nil { return nil, err } return ®Resp, nil } // ServerConfig is the configuration returned by the server type ServerConfig struct { // ForceCloud overrides local mode setting - for remote support ForceCloud bool `json:"force_cloud"` BLE struct { Enabled bool `json:"enabled"` BatchIntervalMs int `json:"batch_interval_ms"` UUIDFilterHex string `json:"uuid_filter_hex,omitempty"` } `json:"ble"` WiFi struct { MonitorEnabled bool `json:"monitor_enabled"` ClientEnabled bool `json:"client_enabled"` SSID string `json:"ssid"` PSK string `json:"psk"` BatchIntervalMs int `json:"batch_interval_ms"` } `json:"wifi"` SSHTunnel struct { Enabled bool `json:"enabled"` Server string `json:"server"` Port int `json:"port"` User string `json:"user"` RemotePort int `json:"remote_port"` KeepaliveInterval int `json:"keepalive_interval"` } `json:"ssh_tunnel"` Net struct { NTP struct { Servers []string `json:"servers"` } `json:"ntp"` } `json:"net"` Debug bool `json:"debug"` } // GetConfig fetches configuration from the server func (c *APIClient) GetConfig(deviceID string) (*ServerConfig, error) { httpReq, err := http.NewRequest("GET", c.baseURL+"/config", nil) if err != nil { return nil, err } q := httpReq.URL.Query() q.Set("device_id", deviceID) httpReq.URL.RawQuery = q.Encode() if c.token != "" { httpReq.Header.Set("Authorization", "Bearer "+c.token) } resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("config fetch failed: %d", resp.StatusCode) } var cfg ServerConfig if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil { return nil, err } return &cfg, nil } // EventBatch is a batch of events to upload type EventBatch struct { DeviceID string `json:"device_id"` Events []interface{} `json:"events"` } // UploadEvents uploads a batch of events (gzipped) func (c *APIClient) UploadEvents(endpoint string, batch *EventBatch) error { // Serialize to JSON jsonData, err := json.Marshal(batch) if err != nil { return err } // Gzip compress var buf bytes.Buffer gw := gzip.NewWriter(&buf) if _, err := gw.Write(jsonData); err != nil { return err } if err := gw.Close(); err != nil { return err } // Store compressed data for retries compressedData := buf.Bytes() url := c.baseURL + endpoint // Send with retries var lastErr error for attempt := 1; attempt <= 3; attempt++ { // Create fresh request for each attempt (body can only be read once) httpReq, err := http.NewRequest("POST", url, bytes.NewReader(compressedData)) if err != nil { return err } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Content-Encoding", "gzip") if c.token != "" { httpReq.Header.Set("Authorization", "Bearer "+c.token) } resp, err := c.httpClient.Do(httpReq) if err != nil { lastErr = err time.Sleep(time.Duration(attempt) * time.Second) continue } resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil } lastErr = fmt.Errorf("upload failed: %d", resp.StatusCode) time.Sleep(time.Duration(attempt) * time.Second) } return lastErr }