rmm2/backend/api/handlers.go
cynfo3000 f47154787d WebSocket-Infrastruktur: bidirektionaler Command-Kanal + TCP-Tunnel
- Agent: WS-Client mit Reconnect, Command-Handler (exec, tunnel_open/close/data)
- Backend: WS-Hub, Tunnel-Manager mit dynamischen Proxy-Ports (10000-20000)
- API: /agents/{id}/exec, /tunnel, /tunnels, /proxy/{tunnel_id}
- Binary WebSocket Messages fuer effiziente Tunnel-Daten
- gorilla/websocket (pure Go, kein CGO)
- README aktualisiert
2026-02-28 07:42:33 +01:00

210 lines
5.5 KiB
Go

package api
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log"
"net/http"
"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
}
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("GET /api/v1/agents", h.listAgents)
mux.HandleFunc("GET /api/v1/agents/{id}", h.getAgent)
mux.HandleFunc("GET /api/v1/agents/{id}/system", h.getSystemData)
mux.HandleFunc("DELETE /api/v1/agents/{id}", h.deleteAgent)
// 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 bestehendem Agent mit gleichem Hostname suchen
if existing, err := h.db.GetAgentByHostname(req.Hostname); err == nil && existing != nil {
agentID = existing.ID
isExisting = true
}
}
if agentID == "" {
agentID = generateID()
}
agent := &models.Agent{
ID: agentID,
Name: req.Name,
Hostname: req.Hostname,
IP: req.IP,
OPNsenseVersion: req.OPNsenseVersion,
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/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
}
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
}
writeJSON(w, http.StatusOK, models.HeartbeatResponse{Message: "OK"})
}
// 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{}
}
writeJSON(w, http.StatusOK, agents)
}
// 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,
"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)
writeJSON(w, http.StatusOK, map[string]string{"message": "Agent geloescht"})
}