package main import ( "bytes" "compress/gzip" "crypto/ed25519" "crypto/rand" "encoding/json" "encoding/pem" "fmt" "io" "net/http" "os" "time" "golang.org/x/crypto/ssh" ) // 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"` SSHPublicKey string `json:"ssh_public_key,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"` // ConfigPollingInterval in seconds (default: 30) ConfigPollingInterval int `json:"cfg_polling_timeout"` BLE struct { Enabled bool `json:"enabled"` BatchIntervalMs int `json:"batch_interval_ms"` UUIDFilterHex string `json:"uuid_filter_hex,omitempty"` UploadEndpoint string `json:"upload_endpoint,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"` UploadEndpoint string `json:"upload_endpoint,omitempty"` } `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"` DashboardTunnel 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:"dashboard_tunnel"` Dashboard struct { Enabled bool `json:"enabled"` } `json:"dashboard"` 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() // Determine URL - use custom endpoint if it's a full URL, otherwise prepend baseURL var url string if len(endpoint) >= 7 && endpoint[:7] == "http://" { url = endpoint } else if len(endpoint) >= 8 && endpoint[:8] == "https://" { url = endpoint } else { 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 } // WiFiCredentialsUpdate is the request to update WiFi credentials on server type WiFiCredentialsUpdate struct { SSID string `json:"ssid"` PSK string `json:"psk"` } // UpdateWiFiCredentials sends WiFi credentials to server (Cloud Mode only) func (c *APIClient) UpdateWiFiCredentials(ssid, psk string) error { body, err := json.Marshal(&WiFiCredentialsUpdate{ SSID: ssid, PSK: psk, }) if err != nil { return err } httpReq, err := http.NewRequest("POST", c.baseURL+"/wifi-credentials", bytes.NewReader(body)) if err != nil { return err } httpReq.Header.Set("Content-Type", "application/json") if c.token != "" { httpReq.Header.Set("Authorization", "Bearer "+c.token) } resp, err := c.httpClient.Do(httpReq) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 200 && resp.StatusCode != 201 { respBody, _ := io.ReadAll(resp.Body) return fmt.Errorf("wifi credentials update failed: %d %s", resp.StatusCode, string(respBody)) } return nil } // GenerateOrLoadSSHKey generates ED25519 key pair or loads existing one // Returns OpenSSH public key format func GenerateOrLoadSSHKey(keyPath string) (string, error) { // Check if key already exists pubKeyPath := keyPath + ".pub" if _, err := os.Stat(keyPath); err == nil { // Key exists - read public key file pubKeyBytes, err := os.ReadFile(pubKeyPath) if err != nil { return "", fmt.Errorf("failed to read public key: %w", err) } return string(pubKeyBytes), nil } // Generate new ED25519 key pair pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { return "", fmt.Errorf("failed to generate ED25519 key: %w", err) } // Convert to OpenSSH format for private key privKeyPEM, err := ssh.MarshalPrivateKey(privKey, "") if err != nil { return "", fmt.Errorf("failed to marshal private key: %w", err) } // Save private key privKeyFile, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return "", fmt.Errorf("failed to create private key file: %w", err) } defer privKeyFile.Close() if err := pem.Encode(privKeyFile, privKeyPEM); err != nil { return "", fmt.Errorf("failed to write private key: %w", err) } // Convert public key to OpenSSH format sshPubKey, err := ssh.NewPublicKey(pubKey) if err != nil { return "", fmt.Errorf("failed to create SSH public key: %w", err) } pubKeyStr := string(ssh.MarshalAuthorizedKey(sshPubKey)) // Save public key if err := os.WriteFile(pubKeyPath, []byte(pubKeyStr), 0644); err != nil { return "", fmt.Errorf("failed to write public key: %w", err) } return pubKeyStr, nil }