- 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
213 lines
5.2 KiB
Go
213 lines
5.2 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 öffnen
|
|
tunnelID, err := h.client.tunnelManager.OpenTunnel(targetHost, targetPort)
|
|
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
|
|
} |