381 lines
11 KiB
Go
381 lines
11 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/cynfo/rmm-backend/ws"
|
|
)
|
|
|
|
func (h *Handler) setupTunnelRoutes(mux *http.ServeMux, hub *ws.Hub) {
|
|
// Command-Endpoints
|
|
mux.HandleFunc("POST /api/v1/agents/{id}/exec", h.executeCommand(hub))
|
|
mux.HandleFunc("POST /api/v1/agents/{id}/update", h.agentUpdate(hub))
|
|
mux.HandleFunc("POST /api/v1/agents/{id}/major-update", h.agentMajorUpdate(hub))
|
|
mux.HandleFunc("POST /api/v1/agents/{id}/update-check", h.agentUpdateCheck(hub))
|
|
mux.HandleFunc("POST /api/v1/agents/{id}/reboot", h.agentReboot(hub))
|
|
mux.HandleFunc("POST /api/v1/agents/{id}/tunnel", h.openTunnel(hub))
|
|
mux.HandleFunc("DELETE /api/v1/agents/{id}/tunnel/{tunnel_id}", h.closeTunnel(hub))
|
|
mux.HandleFunc("GET /api/v1/agents/{id}/tunnels", h.listTunnels(hub))
|
|
|
|
// HTTP-Proxy durch Tunnel
|
|
mux.HandleFunc("GET /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub))
|
|
mux.HandleFunc("POST /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub))
|
|
mux.HandleFunc("PUT /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub))
|
|
mux.HandleFunc("DELETE /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub))
|
|
|
|
// Metrics-Endpoints
|
|
mux.HandleFunc("GET /api/v1/agents/{id}/metrics", h.getMetrics())
|
|
mux.HandleFunc("GET /api/v1/agents/{id}/metrics/summary", h.getMetricsSummary())
|
|
|
|
// WireGuard-Endpoints
|
|
mux.HandleFunc("POST /api/v1/agents/{id}/wireguard/peers", h.wgAddPeer(hub))
|
|
mux.HandleFunc("GET /api/v1/agents/{id}/wireguard/peers", h.wgListPeers(hub))
|
|
mux.HandleFunc("DELETE /api/v1/agents/{id}/wireguard/peers/{uuid}", h.wgDeletePeer(hub))
|
|
|
|
// Backup-Endpoints
|
|
mux.HandleFunc("POST /api/v1/agents/{id}/backup", h.triggerBackup(hub))
|
|
mux.HandleFunc("GET /api/v1/agents/{id}/backups", h.listBackups())
|
|
mux.HandleFunc("GET /api/v1/agents/{id}/backups/{backup_id}", h.getBackup())
|
|
mux.HandleFunc("DELETE /api/v1/agents/{id}/backups/{backup_id}", h.deleteBackup())
|
|
mux.HandleFunc("GET /api/v1/agents/{id}/backups/diff", h.diffBackups())
|
|
|
|
// Terminal-Endpoint (Browser -> Agent PTY)
|
|
mux.HandleFunc("GET /api/v1/agents/{id}/terminal", h.terminalHandler(hub))
|
|
|
|
// WebSocket-Endpoint
|
|
mux.HandleFunc("GET /api/v1/agent/ws", ws.NewWebSocketHandler(hub))
|
|
|
|
// Hub-Status
|
|
mux.HandleFunc("GET /api/v1/hub/status", h.hubStatus(hub))
|
|
}
|
|
|
|
func (h *Handler) executeCommand(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
|
|
var req struct {
|
|
Command string `json:"command"`
|
|
Timeout int `json:"timeout,omitempty"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
|
|
return
|
|
}
|
|
|
|
if req.Command == "" {
|
|
writeError(w, http.StatusBadRequest, "Parameter 'command' erforderlich")
|
|
return
|
|
}
|
|
|
|
if req.Timeout == 0 {
|
|
req.Timeout = 30
|
|
}
|
|
|
|
// Audit
|
|
cmdTrunc := req.Command
|
|
if len(cmdTrunc) > 100 {
|
|
cmdTrunc = cmdTrunc[:100]
|
|
}
|
|
h.auditLog(r, "exec", "agent", agentID, "", cmdTrunc)
|
|
|
|
// Command senden und auf Antwort warten
|
|
h.sendCommandAndWait(hub, agentID, "exec", map[string]interface{}{
|
|
"command": req.Command,
|
|
"timeout": req.Timeout,
|
|
}, req.Timeout+5, w)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) openTunnel(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
|
|
// Request parsen
|
|
var req struct {
|
|
TargetHost string `json:"target_host"`
|
|
TargetPort int `json:"target_port"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
|
|
return
|
|
}
|
|
|
|
if req.TargetHost == "" || req.TargetPort <= 0 {
|
|
writeError(w, http.StatusBadRequest, "target_host und target_port erforderlich")
|
|
return
|
|
}
|
|
|
|
// Tunnel öffnen
|
|
tunnel, err := hub.GetTunnelManager().OpenTunnel(agentID, req.TargetHost, req.TargetPort)
|
|
if err != nil {
|
|
log.Printf("Tunnel öffnen fehlgeschlagen: %v", err)
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
h.auditLog(r, "tunnel.open", "agent", agentID, "", fmt.Sprintf("%s:%d", req.TargetHost, req.TargetPort))
|
|
|
|
// Response
|
|
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
|
"tunnel_id": tunnel.ID,
|
|
"proxy_port": tunnel.ProxyPort,
|
|
"target_host": tunnel.TargetHost,
|
|
"target_port": tunnel.TargetPort,
|
|
"created_at": tunnel.CreatedAt,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (h *Handler) closeTunnel(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
tunnelID := r.PathValue("tunnel_id")
|
|
|
|
// Tunnel schließen
|
|
if err := hub.GetTunnelManager().CloseTunnel(tunnelID); err != nil {
|
|
writeError(w, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
|
|
agentID := r.PathValue("id")
|
|
h.auditLog(r, "tunnel.close", "agent", agentID, "", tunnelID)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{
|
|
"message": "Tunnel geschlossen",
|
|
})
|
|
}
|
|
}
|
|
|
|
func (h *Handler) listTunnels(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
|
|
// Tunnel für Agent abrufen
|
|
tunnels := hub.GetTunnelManager().GetAgentTunnels(agentID)
|
|
|
|
// Response formatieren
|
|
result := make([]map[string]interface{}, len(tunnels))
|
|
for i, tunnel := range tunnels {
|
|
result[i] = map[string]interface{}{
|
|
"tunnel_id": tunnel.ID,
|
|
"proxy_port": tunnel.ProxyPort,
|
|
"target_host": tunnel.TargetHost,
|
|
"target_port": tunnel.TargetPort,
|
|
"active": tunnel.Active,
|
|
"created_at": tunnel.CreatedAt,
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) httpProxy(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
tunnelID := r.PathValue("tunnel_id")
|
|
|
|
// HTTP-Proxy durch Tunnel
|
|
if err := hub.GetTunnelManager().CreateHTTPProxy(tunnelID, w, r); err != nil {
|
|
writeError(w, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Update Commands ---
|
|
|
|
func (h *Handler) agentUpdateCheck(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
h.auditLog(r, "update.check", "agent", agentID, "", "")
|
|
h.sendCommandAndWait(hub, agentID, "update_check", nil, 120, w)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) agentUpdate(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
|
|
var req struct {
|
|
Reboot bool `json:"reboot"`
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
params := map[string]interface{}{
|
|
"reboot": req.Reboot,
|
|
}
|
|
|
|
rebootStr := "ohne Reboot"
|
|
if req.Reboot {
|
|
rebootStr = "mit Reboot"
|
|
}
|
|
h.auditLog(r, "update.run", "agent", agentID, "", rebootStr)
|
|
h.sendCommandAndWait(hub, agentID, "update", params, 900, w)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) agentMajorUpdate(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
|
|
var req struct {
|
|
Version string `json:"version"`
|
|
Reboot bool `json:"reboot"`
|
|
Phase string `json:"phase"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Version == "" {
|
|
writeError(w, http.StatusBadRequest, "Parameter 'version' erforderlich (z.B. '26.1')")
|
|
return
|
|
}
|
|
|
|
params := map[string]interface{}{
|
|
"version": req.Version,
|
|
"reboot": req.Reboot,
|
|
}
|
|
if req.Phase != "" {
|
|
params["phase"] = req.Phase
|
|
}
|
|
|
|
h.auditLog(r, "update.major", "agent", agentID, "", fmt.Sprintf("Version: %s, Phase: %s", req.Version, req.Phase))
|
|
h.sendCommandAndWait(hub, agentID, "major_update", params, 1200, w)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) agentReboot(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
|
|
var req struct {
|
|
Delay int `json:"delay"`
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
params := map[string]interface{}{}
|
|
if req.Delay > 0 {
|
|
params["delay"] = req.Delay
|
|
}
|
|
|
|
h.auditLog(r, "reboot", "agent", agentID, "", "")
|
|
h.sendCommandAndWait(hub, agentID, "reboot", params, 30, w)
|
|
}
|
|
}
|
|
|
|
// sendAgentCommand erstellt einen einfachen Command-Handler ohne Request-Body
|
|
func (h *Handler) sendAgentCommand(hub *ws.Hub, command string, params map[string]interface{}, timeoutSec int) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
h.sendCommandAndWait(hub, agentID, command, params, timeoutSec, w)
|
|
}
|
|
}
|
|
|
|
// sendCommandAndWait schickt ein Command an den Agent und wartet auf Response
|
|
func (h *Handler) sendCommandAndWait(hub *ws.Hub, agentID, command string, params map[string]interface{}, timeoutSec int, w http.ResponseWriter) {
|
|
if !hub.IsAgentConnected(agentID) {
|
|
writeError(w, http.StatusServiceUnavailable, "Agent nicht verbunden")
|
|
return
|
|
}
|
|
|
|
cmdID := generateID()
|
|
|
|
cmd := map[string]interface{}{
|
|
"type": "command",
|
|
"id": cmdID,
|
|
"command": command,
|
|
}
|
|
if params != nil {
|
|
cmd["params"] = params
|
|
}
|
|
|
|
// 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("Command '%s' senden fehlgeschlagen: %v", command, err)
|
|
writeError(w, http.StatusInternalServerError, "Command senden fehlgeschlagen")
|
|
return
|
|
}
|
|
|
|
log.Printf("Command '%s' (ID: %s) an Agent %s gesendet, warte auf Response...", command, cmdID, agentID)
|
|
|
|
// Auf Response warten
|
|
select {
|
|
case resp := <-responseCh:
|
|
writeJSON(w, http.StatusOK, resp)
|
|
case <-time.After(time.Duration(timeoutSec) * time.Second):
|
|
writeError(w, http.StatusGatewayTimeout, "Timeout: Agent hat nicht rechtzeitig geantwortet")
|
|
}
|
|
}
|
|
|
|
// sendCommandAndWaitResult wie sendCommandAndWait, gibt Response direkt zurueck (kein http.ResponseWriter)
|
|
func (h *Handler) sendCommandAndWaitResult(hub *ws.Hub, agentID, command string, params map[string]interface{}, timeoutSec int) (map[string]interface{}, error) {
|
|
if !hub.IsAgentConnected(agentID) {
|
|
return nil, fmt.Errorf("Agent nicht verbunden")
|
|
}
|
|
|
|
cmdID := generateID()
|
|
cmd := map[string]interface{}{
|
|
"type": "command",
|
|
"id": cmdID,
|
|
"command": command,
|
|
}
|
|
if params != nil {
|
|
cmd["params"] = params
|
|
}
|
|
|
|
responseCh := hub.RegisterResponseWaiter(cmdID)
|
|
defer hub.UnregisterResponseWaiter(cmdID)
|
|
|
|
cmdJSON, _ := json.Marshal(cmd)
|
|
if err := hub.SendToAgent(agentID, cmdJSON); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
select {
|
|
case resp := <-responseCh:
|
|
if status, _ := resp["status"].(string); status == "error" {
|
|
if errMsg, _ := resp["error"].(string); errMsg != "" {
|
|
return nil, fmt.Errorf(errMsg)
|
|
}
|
|
}
|
|
return resp, nil
|
|
case <-time.After(time.Duration(timeoutSec) * time.Second):
|
|
return nil, fmt.Errorf("timeout")
|
|
}
|
|
}
|
|
|
|
// SendCommandToAgent sendet ein Command ohne auf Response zu warten (fire-and-forget)
|
|
func SendCommandToAgent(hub *ws.Hub, agentID, command string, params map[string]interface{}) error {
|
|
cmd := map[string]interface{}{
|
|
"type": "command",
|
|
"id": generateID(),
|
|
"command": command,
|
|
}
|
|
if params != nil {
|
|
cmd["params"] = params
|
|
}
|
|
cmdJSON, _ := json.Marshal(cmd)
|
|
return hub.SendToAgent(agentID, cmdJSON)
|
|
}
|
|
|
|
func (h *Handler) hubStatus(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
stats := hub.GetAgentStats()
|
|
connectedAgents := hub.GetConnectedAgents()
|
|
|
|
response := map[string]interface{}{
|
|
"status": "running",
|
|
"connected_agents": connectedAgents,
|
|
"stats": stats,
|
|
"timestamp": time.Now(),
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, response)
|
|
}
|
|
} |