rmm2/agent/ws/handler.go
cynfo3000 fa68e191f9 Fix Tunnel: ID-Sync Backend<->Agent, bidirektionaler Proxy, API-Key via Query-Param
- Backend gibt tunnel_id vor, Agent uebernimmt sie
- Proxy-Verbindung bidirektional: Daten vom Agent zurueck an TCP-Client
- Middleware akzeptiert api_key als Query-Parameter (fuer WebSocket)
- Getestet: OPNsense WebUI erreichbar ueber Tunnel
2026-02-28 07:52:38 +01:00

216 lines
5.3 KiB
Go

package ws
import (
"context"
"fmt"
"log"
"os/exec"
"strconv"
"time"
)
type Handler struct {
client *Client
}
func NewHandler(client *Client) *Handler {
return &Handler{client: client}
}
func (h *Handler) HandleCommand(msg Message) {
log.Printf("Command erhalten: %s (ID: %s)", msg.Command, msg.ID)
var response Message
response.Type = "response"
response.ID = msg.ID
switch msg.Command {
case "exec":
response = h.handleExec(msg)
case "tunnel_open":
response = h.handleTunnelOpen(msg)
case "tunnel_close":
response = h.handleTunnelClose(msg)
case "tunnel_data":
// Tunnel-Daten werden als Binary-Messages gesendet, nicht als JSON
h.handleTunnelData(msg)
return
default:
response.Status = "error"
response.Error = fmt.Sprintf("Unbekanntes Command: %s", msg.Command)
}
if err := h.client.SendMessage(response); err != nil {
log.Printf("Response senden fehlgeschlagen: %v", err)
}
}
func (h *Handler) handleExec(msg Message) Message {
response := Message{
Type: "response",
ID: msg.ID,
}
// Parameter extrahieren
command, ok := msg.Params["command"].(string)
if !ok || command == "" {
response.Status = "error"
response.Error = "Parameter 'command' erforderlich"
return response
}
timeoutSec := 30 // Default
if t, ok := msg.Params["timeout"]; ok {
if ts, ok := t.(float64); ok {
timeoutSec = int(ts)
} else if ts, ok := t.(string); ok {
if parsed, err := strconv.Atoi(ts); err == nil {
timeoutSec = parsed
}
}
}
// Command ausführen mit Timeout
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
defer cancel()
// FreeBSD/OPNsense nutzt /bin/sh
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
output, err := cmd.CombinedOutput()
if err != nil {
response.Status = "error"
response.Error = err.Error()
response.Data = map[string]interface{}{
"output": string(output),
}
} else {
response.Status = "ok"
response.Data = map[string]interface{}{
"output": string(output),
}
}
return response
}
func (h *Handler) handleTunnelOpen(msg Message) Message {
response := Message{
Type: "response",
ID: msg.ID,
}
// Parameter extrahieren
targetHost, ok := msg.Params["target_host"].(string)
if !ok || targetHost == "" {
response.Status = "error"
response.Error = "Parameter 'target_host' erforderlich"
return response
}
targetPortFloat, ok := msg.Params["target_port"].(float64)
if !ok {
response.Status = "error"
response.Error = "Parameter 'target_port' erforderlich"
return response
}
targetPort := int(targetPortFloat)
// Tunnel-ID vom Backend uebernehmen (falls vorhanden)
backendTunnelID, _ := msg.Params["tunnel_id"].(string)
// Tunnel oeffnen
tunnelID, err := h.client.tunnelManager.OpenTunnel(targetHost, targetPort, backendTunnelID)
if err != nil {
response.Status = "error"
response.Error = fmt.Sprintf("Tunnel öffnen fehlgeschlagen: %v", err)
return response
}
response.Status = "ok"
response.Data = map[string]interface{}{
"tunnel_id": tunnelID,
}
log.Printf("Tunnel geöffnet: %s -> %s:%d", tunnelID, targetHost, targetPort)
return response
}
func (h *Handler) handleTunnelClose(msg Message) Message {
response := Message{
Type: "response",
ID: msg.ID,
}
// Tunnel-ID extrahieren
tunnelID, ok := msg.Params["tunnel_id"].(string)
if !ok || tunnelID == "" {
response.Status = "error"
response.Error = "Parameter 'tunnel_id' erforderlich"
return response
}
// Tunnel schließen
if err := h.client.tunnelManager.CloseTunnel(tunnelID); err != nil {
response.Status = "error"
response.Error = fmt.Sprintf("Tunnel schließen fehlgeschlagen: %v", err)
return response
}
response.Status = "ok"
log.Printf("Tunnel geschlossen: %s", tunnelID)
return response
}
func (h *Handler) handleTunnelData(msg Message) {
// Diese Funktion wird für JSON-basierte Tunnel-Daten verwendet
// In der Praxis verwenden wir Binary WebSocket Messages für bessere Performance
tunnelID, ok := msg.Params["tunnel_id"].(string)
if !ok {
log.Printf("Tunnel-Daten ohne tunnel_id erhalten")
return
}
dataStr, ok := msg.Params["data"].(string)
if !ok {
log.Printf("Tunnel-Daten ohne data erhalten")
return
}
// Base64-Dekodierung würde hier stattfinden
// Aber wir verwenden Binary Messages, daher ist das nur ein Fallback
log.Printf("Tunnel-Daten via JSON erhalten für %s (Länge: %d)", tunnelID, len(dataStr))
}
// SendTunnelData sendet Tunnel-Daten als Binary-Message zurück zum Backend
func (h *Handler) SendTunnelData(tunnelID string, data []byte) error {
// Format: [tunnel_id_length:1][tunnel_id][data]
tunnelIDBytes := []byte(tunnelID)
if len(tunnelIDBytes) > 255 {
return fmt.Errorf("tunnel_id zu lang")
}
message := make([]byte, 1+len(tunnelIDBytes)+len(data))
message[0] = byte(len(tunnelIDBytes))
copy(message[1:], tunnelIDBytes)
copy(message[1+len(tunnelIDBytes):], data)
return h.client.SendBinaryData(message)
}
// ParseTunnelData parst eingehende Binary-Messages mit Tunnel-Daten
func ParseTunnelData(data []byte) (tunnelID string, payload []byte, err error) {
if len(data) < 1 {
return "", nil, fmt.Errorf("Daten zu kurz")
}
tunnelIDLen := int(data[0])
if len(data) < 1+tunnelIDLen {
return "", nil, fmt.Errorf("Tunnel-ID zu kurz")
}
tunnelID = string(data[1 : 1+tunnelIDLen])
payload = data[1+tunnelIDLen:]
return tunnelID, payload, nil
}