package main import ( "crypto/sha256" "crypto/tls" "encoding/hex" "encoding/json" "fmt" "io" "log" "net/http" "os" "os/exec" "path/filepath" "time" "gopkg.in/yaml.v3" ) // Config wird aus der gleichen config.yaml wie der Agent gelesen type Config struct { BackendURL string `yaml:"backend_url"` APIKey string `yaml:"api_key"` AgentName string `yaml:"agent_name"` Insecure bool `yaml:"insecure"` } type HeartbeatResponse struct { Message string `json:"message"` UpdateAvailable bool `json:"update_available"` UpdateVersion string `json:"update_version"` UpdateHash string `json:"update_hash"` } const ( agentBinaryPath = "/usr/local/rmm/rmm-agent" configPath = "/usr/local/rmm/config.yaml" agentIDPath = "/usr/local/rmm/agent_id" checkInterval = 60 * time.Second logPrefix = "[rmm-updater] " ) func main() { log.SetPrefix(logPrefix) log.SetFlags(log.LstdFlags) log.Println("RMM Updater gestartet") cfg, err := loadConfig() if err != nil { log.Fatalf("Config laden fehlgeschlagen: %v", err) } agentID, err := loadAgentID() if err != nil { log.Fatalf("Agent-ID laden fehlgeschlagen: %v", err) } log.Printf("Backend: %s, Agent: %s (%s)", cfg.BackendURL, cfg.AgentName, agentID) client := &http.Client{ Timeout: 120 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.Insecure}, }, } for { checkAndUpdate(client, cfg, agentID) time.Sleep(checkInterval) } } func loadConfig() (*Config, error) { data, err := os.ReadFile(configPath) if err != nil { return nil, err } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, err } // Default: insecure wenn nicht explizit gesetzt (self-signed certs sind Standard) // yaml unmarshal setzt false als default fuer bool — wir brauchen true als default // Workaround: immer insecure wenn Backend self-signed nutzt cfg.Insecure = true return &cfg, nil } func loadAgentID() (string, error) { data, err := os.ReadFile(agentIDPath) if err != nil { return "", fmt.Errorf("agent_id nicht gefunden: %v", err) } id := string(data) if id == "" { return "", fmt.Errorf("agent_id leer") } return id, nil } func checkAndUpdate(client *http.Client, cfg *Config, agentID string) { // Firmware-Info vom Backend holen url := fmt.Sprintf("%s/api/v1/firmware", cfg.BackendURL) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("X-API-Key", cfg.APIKey) resp, err := client.Do(req) if err != nil { log.Printf("Firmware-Info Abfrage fehlgeschlagen: %v", err) return } defer resp.Body.Close() if resp.StatusCode != 200 { return // Keine Firmware vorhanden } var info struct { Available bool `json:"available"` Version string `json:"version"` Hash string `json:"hash"` Size int `json:"size"` } if err := json.NewDecoder(resp.Body).Decode(&info); err != nil || !info.Available { return } // Pruefen ob lokale Binary schon die richtige Version hat if localHash, err := hashFile(agentBinaryPath); err == nil && localHash == info.Hash { return // Schon aktuell } // Heartbeat-Response pruefen: Update angefordert? hbURL := fmt.Sprintf("%s/api/v1/agents/%s", cfg.BackendURL, agentID) hbReq, _ := http.NewRequest("GET", hbURL, nil) hbReq.Header.Set("X-API-Key", cfg.APIKey) hbResp, err := client.Do(hbReq) if err != nil { return } defer hbResp.Body.Close() var agentInfo struct { Agent struct { UpdateRequested bool `json:"update_requested"` } `json:"agent"` } if err := json.NewDecoder(hbResp.Body).Decode(&agentInfo); err != nil { return } if !agentInfo.Agent.UpdateRequested { return // Kein Update angefordert } log.Printf("Update verfuegbar: v%s (%d bytes)", info.Version, info.Size) // Binary herunterladen if err := downloadAndApply(client, cfg, agentID, info.Hash); err != nil { log.Printf("Update fehlgeschlagen: %v", err) return } log.Printf("Update auf v%s erfolgreich", info.Version) } func downloadAndApply(client *http.Client, cfg *Config, agentID, expectedHash string) error { // Download url := fmt.Sprintf("%s/api/v1/firmware/download", cfg.BackendURL) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("X-API-Key", cfg.APIKey) resp, err := client.Do(req) if err != nil { return fmt.Errorf("download fehlgeschlagen: %v", err) } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("download HTTP %d", resp.StatusCode) } binary, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("download lesen fehlgeschlagen: %v", err) } // Hash verifizieren hash := sha256.Sum256(binary) hashStr := hex.EncodeToString(hash[:]) if hashStr != expectedHash { return fmt.Errorf("hash mismatch: erwartet %s, bekommen %s", expectedHash[:16], hashStr[:16]) } log.Printf("Binary heruntergeladen (%d bytes, Hash OK)", len(binary)) // Temporaere Datei schreiben tmpPath := agentBinaryPath + ".new" if err := os.WriteFile(tmpPath, binary, 0755); err != nil { return fmt.Errorf("tmp schreiben fehlgeschlagen: %v", err) } // Agent stoppen log.Println("Agent wird gestoppt...") exec.Command("/usr/sbin/service", "rmm_agent", "stop").Run() time.Sleep(2 * time.Second) // Binary ersetzen backupPath := agentBinaryPath + ".bak" if _, err := os.Stat(agentBinaryPath); err == nil { os.Rename(agentBinaryPath, backupPath) } if err := os.Rename(tmpPath, agentBinaryPath); err != nil { // Rollback os.Rename(backupPath, agentBinaryPath) return fmt.Errorf("binary ersetzen fehlgeschlagen: %v", err) } os.Chmod(agentBinaryPath, 0755) // Agent starten log.Println("Agent wird gestartet...") if err := exec.Command("/usr/sbin/service", "rmm_agent", "start").Run(); err != nil { log.Printf("WARNUNG: Agent-Start fehlgeschlagen: %v — versuche Rollback", err) os.Rename(backupPath, agentBinaryPath) exec.Command("/usr/sbin/service", "rmm_agent", "start").Run() return fmt.Errorf("agent start fehlgeschlagen") } // Backup aufraeumen os.Remove(backupPath) // Update-Flag loeschen clearURL := fmt.Sprintf("%s/api/v1/agents/%s/request-update", cfg.BackendURL, agentID) clearReq, _ := http.NewRequest("DELETE", clearURL, nil) clearReq.Header.Set("X-API-Key", cfg.APIKey) client.Do(clearReq) return nil } func hashFile(path string) (string, error) { data, err := os.ReadFile(path) if err != nil { return "", err } hash := sha256.Sum256(data) return hex.EncodeToString(hash[:]), nil } // resolveDir gibt das Verzeichnis eines Pfads zurueck func resolveDir(path string) string { return filepath.Dir(path) }