package api import ( "encoding/json" "fmt" "log" "net/http" "time" "github.com/cynfo/rmm-backend/ws" "github.com/gorilla/websocket" ) var termUpgrader = websocket.Upgrader{ ReadBufferSize: 4096, WriteBufferSize: 4096, CheckOrigin: func(r *http.Request) bool { return true }, } // TerminalHandler bridget Browser-WebSocket mit Agent-PTY (JWT-only, kein API-Key) func (h *Handler) TerminalHandler(hub *ws.Hub) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { agentID := r.PathValue("id") if agentID == "" { http.Error(w, "agent_id erforderlich", http.StatusBadRequest) return } // Cols/Rows aus Query cols := uint16(220) rows := uint16(50) if c := r.URL.Query().Get("cols"); c != "" { var v uint16 fmt.Sscanf(c, "%d", &v) if v > 0 { cols = v } } if r := r.URL.Query().Get("rows"); r != "" { var v uint16 fmt.Sscanf(r, "%d", &v) if v > 0 { rows = v } } // WebSocket-Upgrade browserConn, err := termUpgrader.Upgrade(w, r, nil) if err != nil { log.Printf("Terminal WS Upgrade fehlgeschlagen: %v", err) return } defer browserConn.Close() // PTY-Session-ID generieren sessionID := fmt.Sprintf("pty-%s-%d", agentID[:8], time.Now().UnixNano()) // Output-Channel registrieren outputCh := hub.RegisterPTYSession(sessionID) defer hub.UnregisterPTYSession(sessionID) // pty_start an Agent senden err = h.sendCommandAndWaitPTY(hub, agentID, sessionID, cols, rows) if err != nil { log.Printf("PTY-Start fehlgeschlagen fuer %s: %v", agentID, err) browserConn.WriteMessage(websocket.TextMessage, []byte("\r\nFehler: Agent nicht erreichbar oder PTY-Start fehlgeschlagen\r\n")) return } log.Printf("PTY-Session gestartet: %s fuer Agent %s", sessionID, agentID) // Goroutine: Agent-Ausgabe -> Browser done := make(chan struct{}) go func() { defer close(done) for { select { case data, ok := <-outputCh: if !ok { return } browserConn.SetWriteDeadline(time.Now().Add(10 * time.Second)) if err := browserConn.WriteMessage(websocket.BinaryMessage, data); err != nil { return } case <-time.After(30 * time.Second): // Keep-alive ping if err := browserConn.WriteMessage(websocket.PingMessage, nil); err != nil { return } } } }() // Browser-Input -> Agent browserConn.SetReadDeadline(time.Time{}) // kein Timeout fuer interaktive Sessions for { messageType, data, err := browserConn.ReadMessage() if err != nil { break } if messageType == websocket.BinaryMessage { // Eingabe an Agent-PTY weiterleiten idBytes := []byte(sessionID) msg := make([]byte, 1+len(idBytes)+len(data)) msg[0] = byte(len(idBytes)) copy(msg[1:], idBytes) copy(msg[1+len(idBytes):], data) hub.SendBinaryToAgent(agentID, msg) } else if messageType == websocket.TextMessage { // JSON-Control-Messages (resize) var ctrl struct { Type string `json:"type"` Cols uint16 `json:"cols"` Rows uint16 `json:"rows"` } if err := json.Unmarshal(data, &ctrl); err == nil && ctrl.Type == "resize" { SendCommandToAgent(hub, agentID, "pty_resize", map[string]interface{}{ "session_id": sessionID, "cols": ctrl.Cols, "rows": ctrl.Rows, }) } } } // Browser disconnected -> PTY stoppen SendCommandToAgent(hub, agentID, "pty_stop", map[string]interface{}{ "session_id": sessionID, }) log.Printf("PTY-Session beendet: %s", sessionID) <-done } } func (h *Handler) sendCommandAndWaitPTY(hub *ws.Hub, agentID, sessionID string, cols, rows uint16) error { _, err := h.sendCommandAndWaitResult(hub, agentID, "pty_start", map[string]interface{}{ "session_id": sessionID, "cols": cols, "rows": rows, }, 10) return err }