339 lines
7.0 KiB
Go
339 lines
7.0 KiB
Go
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
|
|
writeMux sync.Mutex
|
|
|
|
reconnectInterval time.Duration
|
|
pingInterval time.Duration
|
|
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
messageHandler *Handler
|
|
tunnelManager *TunnelManager
|
|
ptyManager *PTYManager
|
|
}
|
|
|
|
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)
|
|
client.ptyManager = NewPTYManager(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):
|
|
backoff = time.Duration(float64(backoff) * 1.5)
|
|
if backoff > maxBackoff {
|
|
backoff = maxBackoff
|
|
}
|
|
case <-c.ctx.Done():
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
|
|
connStart := time.Now()
|
|
log.Println("WebSocket-Verbindung hergestellt")
|
|
|
|
// Ping/Pong und Message-Handling starten
|
|
c.runConnection()
|
|
|
|
connDuration := time.Since(connStart)
|
|
log.Printf("WebSocket-Verbindung getrennt (Dauer: %v)", connDuration)
|
|
|
|
// Alte Verbindung explizit schliessen
|
|
c.connMux.Lock()
|
|
if c.conn != nil {
|
|
c.conn.Close()
|
|
c.conn = nil
|
|
}
|
|
c.connMux.Unlock()
|
|
|
|
// Minimum-Delay zwischen Reconnects (verhindert Tight-Loop)
|
|
if connDuration < 5*time.Second {
|
|
backoff = time.Duration(float64(backoff) * 1.5)
|
|
if backoff > maxBackoff {
|
|
backoff = maxBackoff
|
|
}
|
|
log.Printf("Verbindung zu kurz, warte %v vor Reconnect", backoff)
|
|
select {
|
|
case <-time.After(backoff):
|
|
case <-c.ctx.Done():
|
|
return
|
|
}
|
|
} else {
|
|
backoff = c.reconnectInterval
|
|
}
|
|
}
|
|
}
|
|
|
|
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.writeMux.Lock()
|
|
c.connMux.RLock()
|
|
conn := c.conn
|
|
c.connMux.RUnlock()
|
|
if conn != nil {
|
|
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
|
c.writeMux.Unlock()
|
|
log.Printf("Ping senden fehlgeschlagen: %v", err)
|
|
return
|
|
}
|
|
}
|
|
c.writeMux.Unlock()
|
|
|
|
case msg := <-msgChan:
|
|
go c.handleMessage(msg)
|
|
|
|
case err := <-errChan:
|
|
log.Printf("WebSocket-Lesefehler: %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 {
|
|
// PTY-Messages: session_id beginnt mit "pty-"
|
|
if len(data) > 1 {
|
|
idLen := int(data[0])
|
|
if 1+idLen <= len(data) {
|
|
sessionID := string(data[1 : 1+idLen])
|
|
payload := data[1+idLen:]
|
|
if len(sessionID) >= 4 && sessionID[:4] == "pty-" {
|
|
if err := c.ptyManager.WriteToPTY(sessionID, payload); err != nil {
|
|
log.Printf("PTY-Write fehlgeschlagen: %v", err)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
// 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")
|
|
}
|
|
|
|
c.writeMux.Lock()
|
|
err := conn.WriteJSON(msg)
|
|
c.writeMux.Unlock()
|
|
return err
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
c.writeMux.Lock()
|
|
err := conn.WriteMessage(websocket.BinaryMessage, data)
|
|
c.writeMux.Unlock()
|
|
return err
|
|
}
|
|
|
|
func (c *Client) IsConnected() bool {
|
|
c.connMux.RLock()
|
|
connected := c.conn != nil
|
|
c.connMux.RUnlock()
|
|
|
|
return connected
|
|
} |