| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- 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"`
- 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"`
- 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()
- 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
- if _, err := os.Stat(keyPath); err == nil {
- // Load existing key
- privKeyBytes, err := os.ReadFile(keyPath)
- if err != nil {
- return "", fmt.Errorf("failed to read existing key: %w", err)
- }
- block, _ := pem.Decode(privKeyBytes)
- if block == nil {
- return "", fmt.Errorf("failed to decode PEM block")
- }
- // Parse ED25519 private key
- privKey, err := ssh.ParseRawPrivateKey(privKeyBytes)
- if err != nil {
- return "", fmt.Errorf("failed to parse private key: %w", err)
- }
- ed25519Key, ok := privKey.(ed25519.PrivateKey)
- if !ok {
- return "", fmt.Errorf("key is not ED25519")
- }
- // Extract public key
- pubKey := ed25519Key.Public().(ed25519.PublicKey)
- sshPubKey, err := ssh.NewPublicKey(pubKey)
- if err != nil {
- return "", fmt.Errorf("failed to create SSH public key: %w", err)
- }
- return string(ssh.MarshalAuthorizedKey(sshPubKey)), 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
- pubKeyPath := keyPath + ".pub"
- if err := os.WriteFile(pubKeyPath, []byte(pubKeyStr), 0644); err != nil {
- return "", fmt.Errorf("failed to write public key: %w", err)
- }
- return pubKeyStr, nil
- }
|