Agent-Updater, Multi-Plattform Firmware, Frontend Firmware-Seite, Agent-Tab, Zertifikate-Redesign

Updater:
- Separater Go-Daemon (rmm-updater) als eigener rc.d Service
- Pollt alle 60s: Firmware vorhanden + Hash unterschiedlich + Flag gesetzt
- Download -> SHA256 Verify -> Agent Stop -> Replace -> Agent Start
- Automatisches Rollback bei Fehler
- Deployed und getestet auf allen 3 Firewalls (.1, .19, .33)

Backend:
- Firmware-DB multi-plattform (freebsd/linux/windows)
- update_requested Flag pro Agent in agents Tabelle
- Heartbeat-Response liefert update_available wenn Flag gesetzt
- Neue Endpoints: request-update, request-update-all, firmware/download
- Auth: JWT + CombinedAuth Middleware (API-Key OR JWT)
- Customers CRUD + Agent-Zuordnung
- Users CRUD + Passwort-Aenderung

Frontend:
- Firmware-Seite: Multi-Plattform Upload, Agent-Liste mit Update-Trigger
- Agent-Tab im AgentPanel: Update-Button, nuetzliche Befehle, Agent-Info
- Zertifikate-Tab redesigned: Karten-Layout mit Restlaufzeit und Farbbalken
- Sidebar: Firmware-Eintrag hinzugefuegt

Plugin:
- install.sh erweitert: Installiert Agent + Updater + rc.d Services
- rmm_updater rc.d Service

README:
- Architektur-Diagramm erweitert (Frontend + Updater)
- Auth, Customers, Users, Firmware API dokumentiert
- Updater-Flow dokumentiert
- Frontend-Seiten und Deploy beschrieben
- DB-Schema aktualisiert (neue Tabellen)
- Dateistruktur erweitert (updater/, frontend/, firmware.go, auth.go)
This commit is contained in:
cynfo3000
2026-02-28 22:43:03 +01:00
parent 616a0cb6ac
commit e74cdce0ba
26 changed files with 2857 additions and 241 deletions
+219
View File
@@ -0,0 +1,219 @@
package api
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"github.com/cynfo/rmm-backend/ws"
)
// POST /api/v1/firmware/upload — Neues Agent-Binary hochladen
func (h *Handler) uploadFirmware(w http.ResponseWriter, r *http.Request) {
version := r.URL.Query().Get("version")
if version == "" {
writeError(w, http.StatusBadRequest, "version Parameter erforderlich")
return
}
platform := r.URL.Query().Get("platform")
if platform == "" {
platform = "freebsd" // Default fuer OPNsense
}
// Validieren
validPlatforms := map[string]bool{"freebsd": true, "linux": true, "windows": true}
if !validPlatforms[platform] {
writeError(w, http.StatusBadRequest, "Ungueltige Plattform (freebsd, linux, windows)")
return
}
// Binary aus Body lesen (max 50MB)
r.Body = http.MaxBytesReader(w, r.Body, 50*1024*1024)
binary, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "Binary lesen fehlgeschlagen")
return
}
if len(binary) == 0 {
writeError(w, http.StatusBadRequest, "Leeres Binary")
return
}
hash := sha256.Sum256(binary)
hashStr := hex.EncodeToString(hash[:])
if err := h.db.SaveAgentFirmware(version, platform, hashStr, binary); err != nil {
log.Printf("Firmware speichern fehlgeschlagen: %v", err)
writeError(w, http.StatusInternalServerError, "Speichern fehlgeschlagen")
return
}
log.Printf("Agent-Firmware v%s (%s) hochgeladen (%d bytes, Hash: %s)", version, platform, len(binary), hashStr[:16])
writeJSON(w, http.StatusOK, map[string]interface{}{
"version": version,
"platform": platform,
"hash": hashStr,
"size": len(binary),
"message": "Firmware hochgeladen",
})
}
// GET /api/v1/firmware — Firmware-Info (alle Plattformen oder spezifisch)
func (h *Handler) getFirmwareInfo(w http.ResponseWriter, r *http.Request) {
platform := r.URL.Query().Get("platform")
if platform == "" {
// Alle Plattformen auflisten
all, err := h.db.GetAllFirmwareInfo()
if err != nil || len(all) == 0 {
writeJSON(w, http.StatusOK, map[string]interface{}{
"available": false,
"message": "Keine Firmware hochgeladen",
"platforms": []interface{}{},
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"available": true,
"platforms": all,
})
return
}
version, hash, size, err := h.db.GetFirmwareInfo(platform)
if err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"available": false,
"platform": platform,
"message": "Keine Firmware fuer diese Plattform",
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"available": true,
"platform": platform,
"version": version,
"hash": hash,
"size": size,
})
}
// GET /api/v1/firmware/download — Binary herunterladen (fuer Updater)
func (h *Handler) downloadFirmware(w http.ResponseWriter, r *http.Request) {
platform := r.URL.Query().Get("platform")
if platform == "" {
platform = "freebsd"
}
version, hash, binary, err := h.db.GetLatestFirmware(platform)
if err != nil {
writeError(w, http.StatusNotFound, "Keine Firmware verfuegbar")
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename=rmm-agent")
w.Header().Set("X-Firmware-Version", version)
w.Header().Set("X-Firmware-Hash", hash)
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(binary)))
w.Write(binary)
}
// POST /api/v1/agents/{id}/request-update — Update-Flag setzen
func (h *Handler) requestUpdate(w http.ResponseWriter, r *http.Request) {
agentID := r.PathValue("id")
if err := h.db.SetUpdateRequest(agentID, true); err != nil {
writeError(w, http.StatusInternalServerError, "Fehler beim Setzen des Update-Flags")
return
}
log.Printf("Update angefordert fuer Agent %s", agentID)
writeJSON(w, http.StatusOK, map[string]interface{}{"message": "Update angefordert", "agent_id": agentID})
}
// DELETE /api/v1/agents/{id}/request-update — Update-Flag loeschen
func (h *Handler) cancelUpdate(w http.ResponseWriter, r *http.Request) {
agentID := r.PathValue("id")
h.db.ClearUpdateRequest(agentID)
writeJSON(w, http.StatusOK, map[string]interface{}{"message": "Update-Anforderung geloescht", "agent_id": agentID})
}
// POST /api/v1/agents/request-update-all — Update-Flag fuer alle setzen
func (h *Handler) requestUpdateAll(w http.ResponseWriter, r *http.Request) {
count, err := h.db.SetUpdateRequestAll(true)
if err != nil {
writeError(w, http.StatusInternalServerError, "Fehler")
return
}
log.Printf("Update angefordert fuer %d Agents", count)
writeJSON(w, http.StatusOK, map[string]interface{}{"message": fmt.Sprintf("Update fuer %d Agents angefordert", count), "count": count})
}
// POST /api/v1/agents/{id}/agent-update — Update an einzelnen Agent pushen
func (h *Handler) pushAgentUpdate(hub *ws.Hub) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
agentID := r.PathValue("id")
// TODO: Agent-Plattform aus DB lesen; fuer jetzt default freebsd
version, hash, binary, err := h.db.GetLatestFirmware("freebsd")
if err != nil {
writeError(w, http.StatusNotFound, "Keine Firmware verfuegbar")
return
}
// Agent-Version pruefen
agent, _, err := h.db.GetAgent(agentID)
if err != nil || agent == nil {
writeError(w, http.StatusNotFound, "Agent nicht gefunden")
return
}
if agent.AgentVersion == version {
writeJSON(w, http.StatusOK, map[string]interface{}{
"message": "Agent ist bereits auf dem neuesten Stand",
"current_version": agent.AgentVersion,
"target_version": version,
"updated": false,
})
return
}
// Binary als Base64 an Agent senden
binaryB64 := base64.StdEncoding.EncodeToString(binary)
cmdID := fmt.Sprintf("agent-update-%s", agentID[:8])
cmd := map[string]interface{}{
"type": "command",
"id": cmdID,
"command": "agent_update",
"data": map[string]interface{}{
"binary": binaryB64,
"hash": hash,
"version": version,
"old_version": agent.AgentVersion,
},
}
cmdJSON, _ := json.Marshal(cmd)
if err := hub.SendToAgent(agentID, cmdJSON); err != nil {
writeError(w, http.StatusBadGateway, fmt.Sprintf("Agent nicht erreichbar: %v", err))
return
}
log.Printf("Agent-Update v%s an %s gesendet (%d bytes)", version, agent.Name, len(binary))
writeJSON(w, http.StatusOK, map[string]interface{}{
"message": fmt.Sprintf("Update v%s an %s gesendet", version, agent.Name),
"current_version": agent.AgentVersion,
"target_version": version,
"updated": true,
"size": len(binary),
})
}
}
+32 -1
View File
@@ -53,6 +53,15 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) {
mux.HandleFunc("PUT /api/v1/users/{id}/password", h.changePassword)
mux.HandleFunc("DELETE /api/v1/users/{id}", h.deleteUser)
// Firmware/Agent-Update Routes
mux.HandleFunc("GET /api/v1/firmware", h.getFirmwareInfo)
mux.HandleFunc("POST /api/v1/firmware/upload", h.uploadFirmware)
mux.HandleFunc("GET /api/v1/firmware/download", h.downloadFirmware)
mux.HandleFunc("POST /api/v1/agents/{id}/agent-update", h.pushAgentUpdate(hub))
mux.HandleFunc("POST /api/v1/agents/{id}/request-update", h.requestUpdate)
mux.HandleFunc("DELETE /api/v1/agents/{id}/request-update", h.cancelUpdate)
mux.HandleFunc("POST /api/v1/agents/request-update-all", h.requestUpdateAll)
// WebSocket und Tunnel-Routes
h.setupTunnelRoutes(mux, hub)
}
@@ -115,6 +124,7 @@ func (h *Handler) registerAgent(w http.ResponseWriter, r *http.Request) {
Hostname: req.Hostname,
IP: req.IP,
OPNsenseVersion: req.OPNsenseVersion,
AgentVersion: req.AgentVersion,
RegisteredAt: time.Now().UTC(),
}
@@ -148,6 +158,11 @@ func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) {
return
}
// Agent-Version aktualisieren
if req.AgentVersion != "" {
h.db.UpdateAgentVersion(req.AgentID, req.AgentVersion)
}
if err := h.db.SaveSystemData(req.AgentID, &req.SystemData); err != nil {
if strings.Contains(err.Error(), "nicht gefunden") {
writeError(w, http.StatusNotFound, "Agent nicht gefunden")
@@ -158,7 +173,23 @@ func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) {
return
}
writeJSON(w, http.StatusOK, models.HeartbeatResponse{Message: "OK"})
resp := models.HeartbeatResponse{Message: "OK"}
// Pruefen ob Update ansteht (default freebsd fuer OPNsense)
if h.db.IsUpdateRequested(req.AgentID) {
if version, hash, _, err := h.db.GetFirmwareInfo("freebsd"); err == nil {
if version != req.AgentVersion {
resp.UpdateAvailable = true
resp.UpdateVersion = version
resp.UpdateHash = hash
} else {
// Gleiche Version — Flag zuruecksetzen
h.db.ClearUpdateRequest(req.AgentID)
}
}
}
writeJSON(w, http.StatusOK, resp)
}
// GET /api/v1/agents