package ws import ( "context" "crypto/tls" "encoding/json" "fmt" "log" "net/url" "sync" "time" "github.com/gorilla/websocket" ) type Client struct { backendURL string agentID string apiKey string insecure bool conn *websocket.Conn connMux sync.RWMutex reconnectInterval time.Duration pingInterval time.Duration ctx context.Context cancel context.CancelFunc messageHandler *Handler tunnelManager *TunnelManager } type Message struct { Type string `json:"type"` ID string `json:"id,omitempty"` Command string `json:"command,omitempty"` Event string `json:"event,omitempty"` Status string `json:"status,omitempty"` Params map[string]interface{} `json:"params,omitempty"` Data interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` } func NewClient(backendURL, agentID, apiKey string, insecure bool) *Client { ctx, cancel := context.WithCancel(context.Background()) client := &Client{ backendURL: backendURL, agentID: agentID, apiKey: apiKey, insecure: insecure, reconnectInterval: 5 * time.Second, pingInterval: 30 * time.Second, ctx: ctx, cancel: cancel, } client.messageHandler = NewHandler(client) client.tunnelManager = NewTunnelManager(client) return client } func (c *Client) Start() error { log.Println("WebSocket Client startet...") go c.reconnectLoop() return nil } func (c *Client) Stop() { log.Println("WebSocket Client stoppt...") c.cancel() c.connMux.Lock() if c.conn != nil { c.conn.Close() c.conn = nil } c.connMux.Unlock() } func (c *Client) reconnectLoop() { backoff := c.reconnectInterval maxBackoff := 5 * time.Minute for { select { case <-c.ctx.Done(): return default: } if err := c.connect(); err != nil { log.Printf("WebSocket-Verbindung fehlgeschlagen: %v", err) select { case <-time.After(backoff): // Exponential backoff mit Maximum backoff = time.Duration(float64(backoff) * 1.5) if backoff > maxBackoff { backoff = maxBackoff } case <-c.ctx.Done(): return } continue } // Verbindung erfolgreich - backoff zurücksetzen backoff = c.reconnectInterval log.Println("WebSocket-Verbindung hergestellt") // Ping/Pong und Message-Handling starten c.runConnection() log.Println("WebSocket-Verbindung getrennt") } } func (c *Client) connect() error { // WebSocket-URL aufbauen u, err := url.Parse(c.backendURL) if err != nil { return fmt.Errorf("Backend-URL parsen: %w", err) } if u.Scheme == "https" { u.Scheme = "wss" } else { u.Scheme = "ws" } u.Path = "/api/v1/agent/ws" q := u.Query() q.Set("agent_id", c.agentID) q.Set("api_key", c.apiKey) u.RawQuery = q.Encode() // Dialer mit TLS-Config dialer := websocket.Dialer{ HandshakeTimeout: 10 * time.Second, TLSClientConfig: &tls.Config{ InsecureSkipVerify: c.insecure, }, } conn, _, err := dialer.Dial(u.String(), nil) if err != nil { return fmt.Errorf("WebSocket-Dial: %w", err) } c.connMux.Lock() c.conn = conn c.connMux.Unlock() return nil } func (c *Client) runConnection() { // Ping-Ticker starten pingTicker := time.NewTicker(c.pingInterval) defer pingTicker.Stop() // Pong-Handler setzen c.connMux.RLock() conn := c.conn c.connMux.RUnlock() if conn == nil { return } conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(c.pingInterval * 2)) return nil }) conn.SetReadDeadline(time.Now().Add(c.pingInterval * 2)) // Goroutine für eingehende Nachrichten msgChan := make(chan Message, 10) errChan := make(chan error, 1) go c.readMessages(conn, msgChan, errChan) for { select { case <-c.ctx.Done(): return case <-pingTicker.C: c.connMux.RLock() if c.conn != nil { if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { c.connMux.RUnlock() log.Printf("Ping senden fehlgeschlagen: %v", err) return } } c.connMux.RUnlock() case msg := <-msgChan: go c.handleMessage(msg) case err := <-errChan: if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Printf("WebSocket-Fehler: %v", err) } return } } } func (c *Client) readMessages(conn *websocket.Conn, msgChan chan<- Message, errChan chan<- error) { defer close(msgChan) for { messageType, data, err := conn.ReadMessage() if err != nil { errChan <- err return } if messageType == websocket.BinaryMessage { // Binary-Message für Tunnel-Daten verarbeiten if err := c.tunnelManager.ProcessBinaryMessage(data); err != nil { log.Printf("Binary-Message verarbeiten fehlgeschlagen: %v", err) } continue } // JSON-Message parsen var msg Message if err := json.Unmarshal(data, &msg); err != nil { log.Printf("JSON-Message parsen fehlgeschlagen: %v", err) continue } select { case msgChan <- msg: case <-c.ctx.Done(): return } } } func (c *Client) handleMessage(msg Message) { switch msg.Type { case "command": c.messageHandler.HandleCommand(msg) case "event": // Events vom Backend verarbeiten (falls nötig) log.Printf("Event erhalten: %s", msg.Event) default: log.Printf("Unbekannter Nachrichtentyp: %s", msg.Type) } } func (c *Client) SendMessage(msg Message) error { c.connMux.RLock() conn := c.conn c.connMux.RUnlock() if conn == nil { return fmt.Errorf("keine WebSocket-Verbindung") } return conn.WriteJSON(msg) } func (c *Client) SendBinaryData(data []byte) error { c.connMux.RLock() conn := c.conn c.connMux.RUnlock() if conn == nil { return fmt.Errorf("keine WebSocket-Verbindung") } return conn.WriteMessage(websocket.BinaryMessage, data) } func (c *Client) IsConnected() bool { c.connMux.RLock() connected := c.conn != nil c.connMux.RUnlock() return connected }