package api import ( "crypto/rand" "encoding/hex" "encoding/json" "log" "net/http" "strconv" "strings" "time" "github.com/cynfo/rmm-backend/db" "github.com/cynfo/rmm-backend/models" "github.com/cynfo/rmm-backend/ws" ) type Handler struct { db *db.Database scheduler *TaskScheduler } func NewHandler(database *db.Database) *Handler { return &Handler{db: database} } // SetupRoutes - Alle API-Routes registrieren func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) { // Bestehende Agent-Routes mux.HandleFunc("POST /api/v1/agent/register", h.registerAgent) mux.HandleFunc("POST /api/v1/agent/heartbeat", h.heartbeat) mux.HandleFunc("POST /api/v1/agents", h.preRegisterAgent) mux.HandleFunc("GET /api/v1/agents", h.listAgents) mux.HandleFunc("GET /api/v1/agents/events", h.getAllAgentEvents) mux.HandleFunc("GET /api/v1/agents/{id}", h.getAgent) mux.HandleFunc("GET /api/v1/agents/{id}/events", h.getAgentEvents) mux.HandleFunc("GET /api/v1/agents/{id}/system", h.getSystemData) mux.HandleFunc("DELETE /api/v1/agents/{id}", h.deleteAgent) // Customer-Routes mux.HandleFunc("GET /api/v1/customers", h.listCustomers) mux.HandleFunc("POST /api/v1/customers", h.createCustomer) mux.HandleFunc("GET /api/v1/customers/{id}", h.getCustomer) mux.HandleFunc("PUT /api/v1/customers/{id}", h.updateCustomer) mux.HandleFunc("DELETE /api/v1/customers/{id}", h.deleteCustomer) mux.HandleFunc("GET /api/v1/customers/{id}/agents", h.getCustomerAgents) // Agent-Customer Zuordnung mux.HandleFunc("PUT /api/v1/agents/{id}/customer", h.assignAgentCustomer) mux.HandleFunc("DELETE /api/v1/agents/{id}/customer", h.unassignAgentCustomer) // User-Routes mux.HandleFunc("GET /api/v1/users", h.listUsers) mux.HandleFunc("POST /api/v1/users", h.createUser) mux.HandleFunc("PUT /api/v1/users/{id}/password", h.changePassword) mux.HandleFunc("DELETE /api/v1/users/{id}", h.deleteUser) // API-Key Routes mux.HandleFunc("GET /api/v1/apikeys", h.listAPIKeys) mux.HandleFunc("POST /api/v1/apikeys", h.createAPIKey) mux.HandleFunc("DELETE /api/v1/apikeys/{id}", h.deleteAPIKey) mux.HandleFunc("GET /api/v1/customers/{id}/apikeys", h.listCustomerAPIKeys) // System Settings mux.HandleFunc("GET /api/v1/settings", h.getSettings) mux.HandleFunc("PUT /api/v1/settings", h.updateSettings) // Audit-Log mux.HandleFunc("GET /api/v1/audit", h.getAuditLogs) // 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) // Installer Routes mux.HandleFunc("GET /api/v1/installer", h.getInstallerInfo) mux.HandleFunc("POST /api/v1/installer/upload", h.uploadInstaller) mux.HandleFunc("GET /api/v1/installer/download", h.downloadInstaller) // Task-Routes mux.HandleFunc("GET /api/v1/tasks", h.listTasks) mux.HandleFunc("GET /api/v1/tasks/{id}", h.getTask) mux.HandleFunc("POST /api/v1/tasks", h.createTask(hub)) mux.HandleFunc("DELETE /api/v1/tasks/{id}", h.deleteTask) mux.HandleFunc("POST /api/v1/tasks/{id}/cancel", h.cancelTask) mux.HandleFunc("PUT /api/v1/tasks/{id}", h.updateTask) mux.HandleFunc("PUT /api/v1/tasks/{id}/toggle", h.toggleTask) mux.HandleFunc("POST /api/v1/tasks/{id}/run", h.runTaskNow()) // WebSocket und Tunnel-Routes h.setupTunnelRoutes(mux, hub) } func generateID() string { b := make([]byte, 16) rand.Read(b) return hex.EncodeToString(b) } func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) } // POST /api/v1/agent/register func (h *Handler) registerAgent(w http.ResponseWriter, r *http.Request) { var req models.RegisterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungueltige Anfrage") return } if req.Name == "" || req.Hostname == "" { writeError(w, http.StatusBadRequest, "Name und Hostname sind erforderlich") return } // Agent-ID bestimmen: vom Agent mitgeliefert > nach Hostname suchen > neu generieren agentID := req.AgentID isExisting := false if agentID != "" { // Pruefen ob die ID schon in der DB existiert if existing, _, err := h.db.GetAgent(agentID); err == nil && existing != nil { isExisting = true } } if agentID == "" { // Nach pre-registriertem Agent mit gleichem Namen suchen (hostname="(ausstehend)") // Kein hostname-basiertes Matching mehr — gleiche Hostnamen auf verschiedenen Maschinen // (z.B. zwei "PVE-REP" bei verschiedenen Kunden) wuerden sonst dieselbe ID bekommen if existing, err := h.db.GetAgentByName(req.Name); err == nil && existing != nil { if existing.Hostname == "(ausstehend)" { agentID = existing.ID isExisting = true log.Printf("Pre-registrierten Agent gefunden: %s (%s)", existing.Name, existing.ID) } } } if agentID == "" { agentID = generateID() } platform := req.Platform if platform == "" { platform = "freebsd" // Default fuer bestehende OPNsense Agents } agent := &models.Agent{ ID: agentID, Name: req.Name, Hostname: req.Hostname, IP: req.IP, OPNsenseVersion: req.OPNsenseVersion, AgentVersion: req.AgentVersion, Platform: platform, RegisteredAt: time.Now().UTC(), } if err := h.db.RegisterAgent(agent); err != nil { log.Printf("Fehler bei Registrierung: %v", err) writeError(w, http.StatusInternalServerError, "Registrierung fehlgeschlagen") return } action := "registriert" if isExisting { action = "re-registriert" } log.Printf("Agent %s: %s (%s)", action, agent.Name, agent.ID) writeJSON(w, http.StatusCreated, models.RegisterResponse{ ID: agent.ID, Message: "Agent erfolgreich registriert", }) } // POST /api/v1/agents - Pre-Registration einer Firewall func (h *Handler) preRegisterAgent(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` CustomerID *int `json:"customer_id,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungueltige Anfrage") return } if req.Name == "" { writeError(w, http.StatusBadRequest, "Name ist erforderlich") return } agentID := generateID() agent := &models.Agent{ ID: agentID, Name: req.Name, Hostname: "(ausstehend)", IP: "", RegisteredAt: time.Now().UTC(), } if err := h.db.RegisterAgent(agent); err != nil { log.Printf("Fehler bei Pre-Registration: %v", err) writeError(w, http.StatusInternalServerError, "Pre-Registration fehlgeschlagen") return } // Optionale Kundenzuordnung if req.CustomerID != nil { h.db.AssignAgentCustomer(agentID, *req.CustomerID) } log.Printf("Agent pre-registriert: %s (%s)", req.Name, agentID) h.auditLog(r, "agent.create", "agent", agentID, req.Name, "") writeJSON(w, http.StatusCreated, map[string]interface{}{ "id": agentID, "name": req.Name, "message": "Firewall erfolgreich pre-registriert", }) } // POST /api/v1/agent/heartbeat func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) { var req models.HeartbeatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungueltige Anfrage") return } if req.AgentID == "" { writeError(w, http.StatusBadRequest, "agent_id erforderlich") return } // Agent-Version VOR Update lesen (fuer Update-Flag Logik) prevAgentVersion := h.db.GetAgentVersionFromDB(req.AgentID) // Agent-Version aktualisieren if req.AgentVersion != "" { h.db.UpdateAgentVersion(req.AgentID, req.AgentVersion) } // Genutzten API-Key speichern (aus Middleware-Kontext) if keyInfo := GetAPIKeyFromContext(r); keyInfo != nil { h.db.UpdateAgentAPIKey(req.AgentID, keyInfo.Key) } 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") return } log.Printf("Fehler bei Heartbeat: %v", err) writeError(w, http.StatusInternalServerError, "Heartbeat fehlgeschlagen") return } resp := models.HeartbeatResponse{Message: "OK"} // Pruefen ob Update ansteht — Plattform aus Heartbeat-Request verwenden if h.db.IsUpdateRequested(req.AgentID) { platform := req.Platform if platform == "" { platform = "freebsd" // Default fuer OPNsense Agents ohne Platform-Feld } version, hash, _, err := h.db.GetFirmwareInfo(platform) if err != nil { // Keine Firmware vorhanden — Flag sinnlos, loeschen h.db.ClearUpdateRequest(req.AgentID) } else if version == req.AgentVersion { // Agent hat genau die Firmware-Version — fertig h.db.ClearUpdateRequest(req.AgentID) } else { // Pruefen ob Agent-Version sich seit Flag-Setzung geaendert hat // (z.B. manuelles Update auf andere Version) if prevAgentVersion != "" && prevAgentVersion != req.AgentVersion { // Version hat sich geaendert — Update war erfolgreich (manuell oder anderweitig) h.db.ClearUpdateRequest(req.AgentID) } else { resp.UpdateAvailable = true resp.UpdateVersion = version resp.UpdateHash = hash } } } writeJSON(w, http.StatusOK, resp) } // GET /api/v1/agents func (h *Handler) listAgents(w http.ResponseWriter, r *http.Request) { agents, err := h.db.GetAgents() if err != nil { log.Printf("Fehler beim Laden der Agents: %v", err) writeError(w, http.StatusInternalServerError, "Fehler beim Laden") return } if agents == nil { agents = []models.Agent{} } // Status fuer jeden Agent berechnen var response []models.AgentResponse for _, a := range agents { response = append(response, models.AgentResponse{ Agent: a, Status: models.CalculateAgentStatus(a.LastHeartbeat), }) } if response == nil { response = []models.AgentResponse{} } writeJSON(w, http.StatusOK, response) } // GET /api/v1/agents/{id} func (h *Handler) getAgent(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") agent, sysData, err := h.db.GetAgent(id) if err != nil { log.Printf("Fehler: %v", err) writeError(w, http.StatusInternalServerError, "Fehler beim Laden") return } if agent == nil { writeError(w, http.StatusNotFound, "Agent nicht gefunden") return } result := map[string]interface{}{ "agent": agent, "status": models.CalculateAgentStatus(agent.LastHeartbeat), "system_data": sysData, } writeJSON(w, http.StatusOK, result) } // GET /api/v1/agents/{id}/system func (h *Handler) getSystemData(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") data, err := h.db.GetSystemData(id) if err != nil { log.Printf("Fehler: %v", err) writeError(w, http.StatusInternalServerError, "Fehler beim Laden") return } if data == nil { writeError(w, http.StatusNotFound, "Keine Systemdaten vorhanden") return } writeJSON(w, http.StatusOK, data) } // DELETE /api/v1/agents/{id} func (h *Handler) deleteAgent(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") deleted, err := h.db.DeleteAgent(id) if err != nil { log.Printf("Fehler beim Loeschen: %v", err) writeError(w, http.StatusInternalServerError, "Loeschen fehlgeschlagen") return } if !deleted { writeError(w, http.StatusNotFound, "Agent nicht gefunden") return } log.Printf("Agent geloescht: %s", id) h.auditLog(r, "agent.delete", "agent", id, id, "") writeJSON(w, http.StatusOK, map[string]string{"message": "Agent geloescht"}) } // GET /api/v1/agents/{id}/events func (h *Handler) getAgentEvents(w http.ResponseWriter, r *http.Request) { agentID := r.PathValue("id") eventType := r.URL.Query().Get("type") limit := 100 if l := r.URL.Query().Get("limit"); l != "" { if n, err := strconv.Atoi(l); err == nil && n > 0 { limit = n } } events, err := h.db.GetAgentEvents(agentID, eventType, limit) if err != nil { log.Printf("Fehler beim Laden der Events: %v", err) writeError(w, http.StatusInternalServerError, "Fehler beim Laden") return } writeJSON(w, http.StatusOK, events) } // GET /api/v1/agents/events func (h *Handler) getAllAgentEvents(w http.ResponseWriter, r *http.Request) { eventType := r.URL.Query().Get("type") limit := 100 if l := r.URL.Query().Get("limit"); l != "" { if n, err := strconv.Atoi(l); err == nil && n > 0 { limit = n } } events, err := h.db.GetAllAgentEvents(eventType, limit) if err != nil { log.Printf("Fehler beim Laden der Events: %v", err) writeError(w, http.StatusInternalServerError, "Fehler beim Laden") return } writeJSON(w, http.StatusOK, events) }