340 lines
9.4 KiB
Go
340 lines
9.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cynfo/rmm-backend/ws"
|
|
)
|
|
|
|
// POST /api/v1/agents/{id}/backup — Backup ausloesen (via WebSocket an Agent)
|
|
func (h *Handler) triggerBackup(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
|
|
if !hub.IsAgentConnected(agentID) {
|
|
writeError(w, http.StatusServiceUnavailable, "Agent nicht verbunden")
|
|
return
|
|
}
|
|
|
|
cmdID := generateID()
|
|
|
|
cmd := map[string]interface{}{
|
|
"type": "command",
|
|
"id": cmdID,
|
|
"command": "backup",
|
|
"params": map[string]interface{}{},
|
|
}
|
|
|
|
// Response-Channel registrieren
|
|
responseCh := hub.RegisterResponseWaiter(cmdID)
|
|
defer hub.UnregisterResponseWaiter(cmdID)
|
|
|
|
cmdJSON, _ := json.Marshal(cmd)
|
|
if err := hub.SendToAgent(agentID, cmdJSON); err != nil {
|
|
log.Printf("Backup-Command senden fehlgeschlagen: %v", err)
|
|
writeError(w, http.StatusInternalServerError, "Command senden fehlgeschlagen")
|
|
return
|
|
}
|
|
|
|
log.Printf("Backup-Command (ID: %s) an Agent %s gesendet", cmdID, agentID)
|
|
|
|
// Auf Response warten (config.xml kann gross sein, 60s Timeout)
|
|
select {
|
|
case resp := <-responseCh:
|
|
status, _ := resp["status"].(string)
|
|
if status != "ok" {
|
|
errMsg, _ := resp["error"].(string)
|
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Backup fehlgeschlagen: %s", errMsg))
|
|
return
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]interface{})
|
|
if !ok {
|
|
writeError(w, http.StatusInternalServerError, "Ungueltiges Response-Format")
|
|
return
|
|
}
|
|
|
|
// Config aus Base64 dekodieren
|
|
configB64, _ := data["config"].(string)
|
|
hash, _ := data["hash"].(string)
|
|
sizeFloat, _ := data["size"].(float64)
|
|
size := int(sizeFloat)
|
|
|
|
configXML, err := base64.StdEncoding.DecodeString(configB64)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Base64-Dekodierung fehlgeschlagen")
|
|
return
|
|
}
|
|
|
|
// In DB speichern (dedupliziert)
|
|
backupID, isNew, err := h.db.SaveConfigBackup(agentID, hash, size, string(configXML))
|
|
if err != nil {
|
|
log.Printf("Backup speichern fehlgeschlagen: %v", err)
|
|
writeError(w, http.StatusInternalServerError, "Backup speichern fehlgeschlagen")
|
|
return
|
|
}
|
|
|
|
msg := "Backup gespeichert"
|
|
if !isNew {
|
|
msg = "Config unveraendert (Hash identisch)"
|
|
}
|
|
|
|
h.auditLog(r, "backup.create", "agent", agentID, "", fmt.Sprintf("backup_id=%d", backupID))
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"backup_id": backupID,
|
|
"hash": hash,
|
|
"size": size,
|
|
"new": isNew,
|
|
"message": msg,
|
|
})
|
|
|
|
case <-time.After(60 * time.Second):
|
|
writeError(w, http.StatusGatewayTimeout, "Timeout: Agent hat nicht rechtzeitig geantwortet")
|
|
}
|
|
}
|
|
}
|
|
|
|
// GET /api/v1/agents/{id}/backups — Alle Backups auflisten
|
|
func (h *Handler) listBackups() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
|
|
limit := 50
|
|
if l := r.URL.Query().Get("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
|
|
backups, err := h.db.GetConfigBackups(agentID, limit)
|
|
if err != nil {
|
|
log.Printf("Backups laden fehlgeschlagen: %v", err)
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, backups)
|
|
}
|
|
}
|
|
|
|
// GET /api/v1/agents/{id}/backups/{backup_id} — Einzelnes Backup herunterladen
|
|
func (h *Handler) getBackup() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
backupIDStr := r.PathValue("backup_id")
|
|
|
|
// "latest" als Spezialwert
|
|
if backupIDStr == "latest" {
|
|
backup, err := h.db.GetLatestConfigBackup(agentID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
|
return
|
|
}
|
|
if backup == nil {
|
|
writeError(w, http.StatusNotFound, "Kein Backup vorhanden")
|
|
return
|
|
}
|
|
|
|
// Format: raw XML oder JSON?
|
|
if r.URL.Query().Get("format") == "xml" {
|
|
w.Header().Set("Content-Type", "application/xml")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=config_%s.xml", agentID[:8]))
|
|
w.Write([]byte(backup["config_xml"].(string)))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, backup)
|
|
return
|
|
}
|
|
|
|
backupID, err := strconv.ParseInt(backupIDStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID")
|
|
return
|
|
}
|
|
|
|
backup, err := h.db.GetConfigBackup(agentID, backupID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
|
return
|
|
}
|
|
if backup == nil {
|
|
writeError(w, http.StatusNotFound, "Backup nicht gefunden")
|
|
return
|
|
}
|
|
|
|
h.auditLog(r, "backup.download", "agent", agentID, "", fmt.Sprintf("backup_id=%s", backupIDStr))
|
|
|
|
// Raw XML Download
|
|
if r.URL.Query().Get("format") == "xml" {
|
|
w.Header().Set("Content-Type", "application/xml")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=config_%s_%d.xml", agentID[:8], backupID))
|
|
w.Write([]byte(backup["config_xml"].(string)))
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, backup)
|
|
}
|
|
}
|
|
|
|
// DELETE /api/v1/agents/{id}/backups/{backup_id} — Backup loeschen
|
|
func (h *Handler) deleteBackup() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
backupIDStr := r.PathValue("backup_id")
|
|
|
|
backupID, err := strconv.ParseInt(backupIDStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID")
|
|
return
|
|
}
|
|
|
|
deleted, err := h.db.DeleteConfigBackup(agentID, backupID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Loeschen fehlgeschlagen")
|
|
return
|
|
}
|
|
if !deleted {
|
|
writeError(w, http.StatusNotFound, "Backup nicht gefunden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Backup geloescht"})
|
|
}
|
|
}
|
|
|
|
// GET /api/v1/agents/{id}/backups/diff?a={id1}&b={id2} — Diff zwischen zwei Backups
|
|
func (h *Handler) diffBackups() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
|
|
aStr := r.URL.Query().Get("a")
|
|
bStr := r.URL.Query().Get("b")
|
|
if aStr == "" || bStr == "" {
|
|
writeError(w, http.StatusBadRequest, "Parameter 'a' und 'b' (Backup-IDs) erforderlich")
|
|
return
|
|
}
|
|
|
|
aID, err := strconv.ParseInt(aStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID 'a'")
|
|
return
|
|
}
|
|
bID, err := strconv.ParseInt(bStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID 'b'")
|
|
return
|
|
}
|
|
|
|
backupA, err := h.db.GetConfigBackup(agentID, aID)
|
|
if err != nil || backupA == nil {
|
|
writeError(w, http.StatusNotFound, fmt.Sprintf("Backup %d nicht gefunden", aID))
|
|
return
|
|
}
|
|
backupB, err := h.db.GetConfigBackup(agentID, bID)
|
|
if err != nil || backupB == nil {
|
|
writeError(w, http.StatusNotFound, fmt.Sprintf("Backup %d nicht gefunden", bID))
|
|
return
|
|
}
|
|
|
|
xmlA := backupA["config_xml"].(string)
|
|
xmlB := backupB["config_xml"].(string)
|
|
|
|
if backupA["hash"] == backupB["hash"] {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"identical": true,
|
|
"a": map[string]interface{}{"id": aID, "hash": backupA["hash"], "created_at": backupA["created_at"]},
|
|
"b": map[string]interface{}{"id": bID, "hash": backupB["hash"], "created_at": backupB["created_at"]},
|
|
"diff": "",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Zeilenweises Diff berechnen
|
|
diff := computeLineDiff(xmlA, xmlB)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"identical": false,
|
|
"a": map[string]interface{}{"id": aID, "hash": backupA["hash"], "created_at": backupA["created_at"]},
|
|
"b": map[string]interface{}{"id": bID, "hash": backupB["hash"], "created_at": backupB["created_at"]},
|
|
"diff": diff,
|
|
})
|
|
}
|
|
}
|
|
|
|
// computeLineDiff erzeugt ein einfaches zeilenweises Diff (unified-style)
|
|
func computeLineDiff(a, b string) string {
|
|
linesA := strings.Split(a, "\n")
|
|
linesB := strings.Split(b, "\n")
|
|
|
|
var result []string
|
|
maxLen := len(linesA)
|
|
if len(linesB) > maxLen {
|
|
maxLen = len(linesB)
|
|
}
|
|
|
|
// Einfaches sequenzielles Diff — fuer XML config.xml voellig ausreichend
|
|
i, j := 0, 0
|
|
for i < len(linesA) || j < len(linesB) {
|
|
if i < len(linesA) && j < len(linesB) && linesA[i] == linesB[j] {
|
|
// Gleiche Zeile — uebergehen (nur Context bei Aenderungen zeigen)
|
|
i++
|
|
j++
|
|
continue
|
|
}
|
|
|
|
// Vorausschauen ob die Zeile weiter unten in der anderen Datei existiert
|
|
foundInB := -1
|
|
if i < len(linesA) {
|
|
for k := j; k < len(linesB) && k < j+10; k++ {
|
|
if linesA[i] == linesB[k] {
|
|
foundInB = k
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
foundInA := -1
|
|
if j < len(linesB) {
|
|
for k := i; k < len(linesA) && k < i+10; k++ {
|
|
if linesB[j] == linesA[k] {
|
|
foundInA = k
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if foundInB >= 0 && (foundInA < 0 || foundInB-j <= foundInA-i) {
|
|
// Zeilen in B eingefuegt
|
|
for j < foundInB {
|
|
result = append(result, fmt.Sprintf("+ %s", linesB[j]))
|
|
j++
|
|
}
|
|
} else if foundInA >= 0 {
|
|
// Zeilen in A entfernt
|
|
for i < foundInA {
|
|
result = append(result, fmt.Sprintf("- %s", linesA[i]))
|
|
i++
|
|
}
|
|
} else {
|
|
// Zeile geaendert
|
|
if i < len(linesA) {
|
|
result = append(result, fmt.Sprintf("- %s", linesA[i]))
|
|
i++
|
|
}
|
|
if j < len(linesB) {
|
|
result = append(result, fmt.Sprintf("+ %s", linesB[j]))
|
|
j++
|
|
}
|
|
}
|
|
}
|
|
|
|
return strings.Join(result, "\n")
|
|
}
|