package ws import ( "encoding/json" "log" "sync" "time" ) type AgentConnection struct { AgentID string Connection *Connection LastSeen time.Time } type Hub struct { agents map[string]*AgentConnection agentsMux sync.RWMutex register chan *AgentConnection unregister chan *AgentConnection broadcast chan []byte tunnelManager *TunnelManager // Response-Waiters: Command-ID -> Response-Channel responseWaiters map[string]chan map[string]interface{} waitersMux sync.RWMutex // Event-Callback fuer DB-Events bei Connect/Disconnect eventCallback func(agentID, eventType, message string) // PTY-Sessions: session_id -> channel fuer Ausgabe an Browser ptySessions map[string]chan []byte ptySessionsMux sync.RWMutex } func NewHub() *Hub { hub := &Hub{ agents: make(map[string]*AgentConnection), register: make(chan *AgentConnection), unregister: make(chan *AgentConnection), broadcast: make(chan []byte, 256), responseWaiters: make(map[string]chan map[string]interface{}), } hub.tunnelManager = NewTunnelManager(hub) hub.ptySessions = make(map[string]chan []byte) return hub } // SetEventCallback setzt die Callback-Funktion fuer Connect/Disconnect Events func (h *Hub) SetEventCallback(cb func(agentID, eventType, message string)) { h.eventCallback = cb } func (h *Hub) Run() { log.Println("WebSocket Hub startet...") // Cleanup-Timer für inaktive Verbindungen cleanupTicker := time.NewTicker(5 * time.Minute) defer cleanupTicker.Stop() for { select { case agent := <-h.register: h.registerAgent(agent) case agent := <-h.unregister: h.unregisterAgent(agent) case message := <-h.broadcast: h.broadcastMessage(message) case <-cleanupTicker.C: h.cleanupInactiveAgents() } } } func (h *Hub) registerAgent(agent *AgentConnection) { h.agentsMux.Lock() defer h.agentsMux.Unlock() // Bestehende Verbindung schließen falls vorhanden if existing, ok := h.agents[agent.AgentID]; ok { log.Printf("Agent %s: Ersetzt bestehende Verbindung", agent.AgentID) existing.Connection.Close() } h.agents[agent.AgentID] = agent agent.LastSeen = time.Now() log.Printf("Agent %s: WebSocket-Verbindung registriert", agent.AgentID) // Event schreiben if h.eventCallback != nil { go h.eventCallback(agent.AgentID, "connected", "WebSocket-Verbindung hergestellt") } } func (h *Hub) unregisterAgent(agent *AgentConnection) { h.agentsMux.Lock() defer h.agentsMux.Unlock() if existing, ok := h.agents[agent.AgentID]; ok && existing.Connection == agent.Connection { delete(h.agents, agent.AgentID) existing.Connection.Close() log.Printf("Agent %s: WebSocket-Verbindung entfernt", agent.AgentID) // Event schreiben if h.eventCallback != nil { go h.eventCallback(agent.AgentID, "disconnected", "WebSocket-Verbindung getrennt") } // Alle Tunnel für diesen Agent schließen h.tunnelManager.CloseAgentTunnels(agent.AgentID) } } func (h *Hub) broadcastMessage(message []byte) { h.agentsMux.RLock() defer h.agentsMux.RUnlock() for agentID, agent := range h.agents { select { case agent.Connection.Send <- message: default: // Agent kann keine Nachrichten mehr empfangen log.Printf("Agent %s: Send-Channel blockiert, trenne Verbindung", agentID) close(agent.Connection.Send) delete(h.agents, agentID) } } } func (h *Hub) SendToAgent(agentID string, message []byte) error { h.agentsMux.RLock() agent, exists := h.agents[agentID] h.agentsMux.RUnlock() if !exists { return ErrAgentNotConnected } agent.LastSeen = time.Now() select { case agent.Connection.Send <- message: return nil case <-time.After(5 * time.Second): return ErrAgentSendBlocked } } func (h *Hub) SendBinaryToAgent(agentID string, data []byte) error { h.agentsMux.RLock() agent, exists := h.agents[agentID] h.agentsMux.RUnlock() if !exists { return ErrAgentNotConnected } agent.LastSeen = time.Now() select { case agent.Connection.BinarySend <- data: return nil case <-time.After(5 * time.Second): return ErrAgentSendBlocked } } func (h *Hub) IsAgentConnected(agentID string) bool { h.agentsMux.RLock() defer h.agentsMux.RUnlock() _, exists := h.agents[agentID] return exists } // WaitForAgentOnline polls until the agent reconnects or timeout is reached. // Returns true if agent came online within the timeout, false otherwise. func (h *Hub) WaitForAgentOnline(agentID string, timeout time.Duration) bool { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { if h.IsAgentConnected(agentID) { return true } time.Sleep(10 * time.Second) } return false } func (h *Hub) GetConnectedAgents() []string { h.agentsMux.RLock() defer h.agentsMux.RUnlock() agents := make([]string, 0, len(h.agents)) for agentID := range h.agents { agents = append(agents, agentID) } return agents } func (h *Hub) GetAgentStats() map[string]interface{} { h.agentsMux.RLock() defer h.agentsMux.RUnlock() stats := map[string]interface{}{ "connected_agents": len(h.agents), "active_tunnels": h.tunnelManager.GetActiveTunnelCount(), } return stats } func (h *Hub) cleanupInactiveAgents() { h.agentsMux.Lock() defer h.agentsMux.Unlock() timeout := 10 * time.Minute now := time.Now() for agentID, agent := range h.agents { if now.Sub(agent.LastSeen) > timeout { log.Printf("Agent %s: Timeout, trenne Verbindung", agentID) delete(h.agents, agentID) agent.Connection.Close() h.tunnelManager.CloseAgentTunnels(agentID) } } } // RegisterResponseWaiter registriert einen Channel fuer eine Command-Response func (h *Hub) RegisterResponseWaiter(cmdID string) chan map[string]interface{} { ch := make(chan map[string]interface{}, 1) h.waitersMux.Lock() h.responseWaiters[cmdID] = ch h.waitersMux.Unlock() return ch } // UnregisterResponseWaiter entfernt einen Response-Waiter func (h *Hub) UnregisterResponseWaiter(cmdID string) { h.waitersMux.Lock() delete(h.responseWaiters, cmdID) h.waitersMux.Unlock() } // GetTunnelManager gibt den TunnelManager zurück (für API-Handler) func (h *Hub) GetTunnelManager() *TunnelManager { return h.tunnelManager } // RegisterAgent registriert eine neue Agent-Verbindung func (h *Hub) RegisterAgent(agentID string, conn *Connection) { agent := &AgentConnection{ AgentID: agentID, Connection: conn, LastSeen: time.Now(), } h.register <- agent } // UnregisterAgent entfernt eine Agent-Verbindung func (h *Hub) UnregisterAgent(agentID string, conn *Connection) { agent := &AgentConnection{ AgentID: agentID, Connection: conn, } h.unregister <- agent } // ProcessAgentMessage verarbeitet eingehende Nachrichten von Agents func (h *Hub) ProcessAgentMessage(agentID string, messageType int, data []byte) error { h.agentsMux.RLock() agent, exists := h.agents[agentID] h.agentsMux.RUnlock() if exists { agent.LastSeen = time.Now() } // Binary Messages: PTY oder Tunnel if messageType == 2 { // websocket.BinaryMessage // PTY-Session erkennen: 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-" { h.ptySessionsMux.RLock() ch, ok := h.ptySessions[sessionID] h.ptySessionsMux.RUnlock() if ok { select { case ch <- payload: default: } } return nil } } } return h.tunnelManager.ProcessBinaryMessage(agentID, data) } // Text Messages sind JSON-Responses/Events vom Agent var msg struct { Type string `json:"type"` ID string `json:"id"` Status string `json:"status"` Data map[string]interface{} `json:"data"` } if err := json.Unmarshal(data, &msg); err != nil { return nil // Ignorieren wenn nicht parsebar } if msg.Type == "response" { // Tunnel-Connect Responses verarbeiten if msg.Status == "ok" && msg.Data != nil { if sessionID, ok := msg.Data["session_id"].(string); ok { h.tunnelManager.ConfirmSession(sessionID) } } // Response an wartende API-Handler weiterleiten if msg.ID != "" { h.waitersMux.RLock() ch, exists := h.responseWaiters[msg.ID] h.waitersMux.RUnlock() if exists { resp := map[string]interface{}{ "status": msg.Status, "data": msg.Data, } if msg.Status == "error" { var errMsg struct { Error string `json:"error"` } json.Unmarshal(data, &errMsg) resp["error"] = errMsg.Error } select { case ch <- resp: default: } } } } return nil } // RegisterPTYSession registriert einen Output-Channel fuer eine PTY-Session func (h *Hub) RegisterPTYSession(sessionID string) chan []byte { ch := make(chan []byte, 256) h.ptySessionsMux.Lock() h.ptySessions[sessionID] = ch h.ptySessionsMux.Unlock() return ch } // UnregisterPTYSession entfernt eine PTY-Session func (h *Hub) UnregisterPTYSession(sessionID string) { h.ptySessionsMux.Lock() delete(h.ptySessions, sessionID) h.ptySessionsMux.Unlock() }