feat: Windows Agent OTA-Updater

- PowerShell-Script-Methode: Binary herunterladen, detached PS1 tauscht Binary nach Dienst-Stop
- SHA256-Verifikation vor Update
- v1.0.1 im Firmware-Store
This commit is contained in:
cynfo3000 2026-03-10 01:52:16 +01:00
parent 2fe83e261a
commit e94fea3e91

View File

@ -3,11 +3,17 @@
package main
import (
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"time"
@ -124,7 +130,7 @@ func runAgent(cfg *config.Config, cfgPath string, stop <-chan struct{}) {
defer ticker.Stop()
// Ersten Heartbeat sofort
sendHeartbeat(c, agentID)
sendHeartbeat(c, cfg, agentID)
for {
select {
@ -132,12 +138,12 @@ func runAgent(cfg *config.Config, cfgPath string, stop <-chan struct{}) {
log.Println("Agent wird gestoppt")
return
case <-ticker.C:
sendHeartbeat(c, agentID)
sendHeartbeat(c, cfg, agentID)
}
}
}
func sendHeartbeat(c *client.Client, agentID string) {
func sendHeartbeat(c *client.Client, cfg *config.Config, agentID string) {
sysData, err := collector.Collect()
if err != nil {
log.Printf("Collector-Fehler: %v", err)
@ -160,12 +166,104 @@ func sendHeartbeat(c *client.Client, agentID string) {
}
log.Printf("Heartbeat OK")
if resp.UpdateAvailable {
log.Printf("Update verfuegbar: v%s — OTA-Update noch nicht implementiert", resp.UpdateVersion)
// TODO: OTA-Updater implementieren (aehnlich rmm-updater auf Linux)
if resp.UpdateAvailable && resp.UpdateVersion != "" && resp.UpdateVersion != Version {
log.Printf("Update verfuegbar: v%s → v%s", Version, resp.UpdateVersion)
go doSelfUpdate(cfg, resp.UpdateVersion, resp.UpdateHash)
}
}
// doSelfUpdate laedt das neue Binary herunter und startet einen
// PowerShell-Script der nach Dienst-Stop das Binary tauscht und neu startet.
func doSelfUpdate(cfg *config.Config, newVersion, expectedHash string) {
log.Printf("OTA-Update: Starte Download v%s", newVersion)
exe, err := os.Executable()
if err != nil {
log.Printf("OTA-Update: Executable-Pfad: %v", err)
return
}
dir := filepath.Dir(exe)
tmpBin := filepath.Join(dir, "rmm-agent-windows-update.exe")
scriptPath := filepath.Join(dir, "rmm-update.ps1")
// Binary herunterladen
dlURL := fmt.Sprintf("%s/api/v1/firmware/download?platform=windows&api_key=%s",
cfg.BackendURL, cfg.APIKey)
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.Insecure}}
hc := &http.Client{Transport: tr, Timeout: 10 * time.Minute}
req, err := http.NewRequest("GET", dlURL, nil)
if err != nil {
log.Printf("OTA-Update: Request: %v", err)
return
}
req.Header.Set("X-API-Key", cfg.APIKey)
resp, err := hc.Do(req)
if err != nil {
log.Printf("OTA-Update: Download: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("OTA-Update: HTTP %d", resp.StatusCode)
return
}
f, err := os.Create(tmpBin)
if err != nil {
log.Printf("OTA-Update: Temp-Datei: %v", err)
return
}
h := sha256.New()
if _, err := io.Copy(io.MultiWriter(f, h), resp.Body); err != nil {
f.Close()
os.Remove(tmpBin)
log.Printf("OTA-Update: Schreiben: %v", err)
return
}
f.Close()
// Hash pruefen
gotHash := hex.EncodeToString(h.Sum(nil))
if expectedHash != "" && gotHash != expectedHash {
os.Remove(tmpBin)
log.Printf("OTA-Update: Hash-Mismatch: erwartet=%s, erhalten=%s", expectedHash, gotHash)
return
}
log.Printf("OTA-Update: Download OK (hash=%s), starte Update-Script", gotHash[:12])
// PowerShell-Script schreiben das nach Dienst-Stop das Binary tauscht
psScript := fmt.Sprintf(`
Start-Sleep -Seconds 3
Stop-Service -Name RMMAgent -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Copy-Item -Path "%s" -Destination "%s" -Force
Remove-Item -Path "%s" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "%s" -Force -ErrorAction SilentlyContinue
Start-Service -Name RMMAgent
`, tmpBin, exe, tmpBin, scriptPath)
if err := os.WriteFile(scriptPath, []byte(psScript), 0644); err != nil {
log.Printf("OTA-Update: Script schreiben: %v", err)
return
}
// Script detached starten (laeuft weiter wenn dieser Prozess beendet wird)
cmd := exec.Command("powershell", "-NonInteractive", "-ExecutionPolicy", "Bypass",
"-WindowStyle", "Hidden", "-File", scriptPath)
if err := cmd.Start(); err != nil {
log.Printf("OTA-Update: Script starten: %v", err)
return
}
// cmd.Wait() nicht aufrufen — Script laeuft unabhaengig weiter
log.Printf("OTA-Update: Script gestartet (PID %d), Dienst wird gestoppt", cmd.Process.Pid)
}
// loadOrRegister liest die Agent-ID aus Datei oder registriert sich neu
func loadOrRegister(c *client.Client, cfg *config.Config, cfgPath string) (string, error) {
idFile := filepath.Join(filepath.Dir(cfgPath), AgentIDFile)