Initial commit: RMM Agent + Backend
- Go Agent (FreeBSD/amd64) fuer OPNsense - Go Backend (Linux/amd64) mit REST API + SQLite - Collector: Hardware, CPU, Memory, Disks, Network, Services, WireGuard, DHCP (KEA/ISC/dnsmasq), Routes, Gateways, Certificates, Plugins, Updates - Persistente Agent-ID - TLS + API-Key Auth - WebSocket-Infrastruktur (WIP): bidirektionaler Command-Kanal, TCP-Tunnel fuer Remote-Zugriff auf Firewall-WebUI - Lastenheft und README
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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"
|
||||
)
|
||||
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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"})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// APIKeyAuth - Middleware fuer API-Key Authentifizierung
|
||||
func APIKeyAuth(validKeys []string, next http.Handler) http.Handler {
|
||||
keySet := make(map[string]bool, len(validKeys))
|
||||
for _, k := range validKeys {
|
||||
keySet[k] = true
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.Header.Get("X-API-Key")
|
||||
if key == "" || !keySet[key] {
|
||||
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Logging - Request-Logging Middleware
|
||||
func Logging(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user