diff --git a/agent-bsd/go.mod b/agent-bsd/go.mod index 81924f2..0d0336d 100644 --- a/agent-bsd/go.mod +++ b/agent-bsd/go.mod @@ -5,3 +5,5 @@ go 1.22 require gopkg.in/yaml.v3 v3.0.1 require github.com/gorilla/websocket v1.5.3 + +require github.com/creack/pty v1.1.24 // indirect diff --git a/agent-bsd/go.sum b/agent-bsd/go.sum index 6a2c68d..38fee7d 100644 --- a/agent-bsd/go.sum +++ b/agent-bsd/go.sum @@ -1,3 +1,5 @@ +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/agent-bsd/ws/client.go b/agent-bsd/ws/client.go index 2a564ee..634f650 100644 --- a/agent-bsd/ws/client.go +++ b/agent-bsd/ws/client.go @@ -31,6 +31,7 @@ type Client struct { messageHandler *Handler tunnelManager *TunnelManager + ptyManager *PTYManager } type Message struct { @@ -60,6 +61,7 @@ func NewClient(backendURL, agentID, apiKey string, insecure bool) *Client { client.messageHandler = NewHandler(client) client.tunnelManager = NewTunnelManager(client) + client.ptyManager = NewPTYManager(client) return client } @@ -250,7 +252,21 @@ func (c *Client) readMessages(conn *websocket.Conn, msgChan chan<- Message, errC } if messageType == websocket.BinaryMessage { - // Binary-Message für Tunnel-Daten verarbeiten + // 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) } diff --git a/agent-bsd/ws/handler.go b/agent-bsd/ws/handler.go index dee7dea..315aaad 100644 --- a/agent-bsd/ws/handler.go +++ b/agent-bsd/ws/handler.go @@ -58,6 +58,14 @@ func (h *Handler) HandleCommand(msg Message) { response.Data = map[string]interface{}{"message": "tunnel_open deprecated, use tunnel_connect"} case "tunnel_close": response.Status = "ok" + case "pty_start": + response = h.handlePTYStart(msg) + case "pty_stop": + h.handlePTYStop(msg) + return + case "pty_resize": + h.handlePTYResize(msg) + return case "agent_update": response = h.handleAgentUpdate(msg) default: @@ -659,3 +667,57 @@ func (h *Handler) SendSessionData(sessionID string, data []byte) error { return h.client.SendBinaryData(message) } + +// --- PTY Handlers --- + +func (h *Handler) handlePTYStart(msg Message) Message { + response := Message{Type: "response", ID: msg.ID} + + sessionID, _ := msg.Params["session_id"].(string) + if sessionID == "" { + response.Status = "error" + response.Error = "session_id erforderlich" + return response + } + + cols := uint16(80) + rows := uint16(24) + if c, ok := msg.Params["cols"].(float64); ok && c > 0 { + cols = uint16(c) + } + if r, ok := msg.Params["rows"].(float64); ok && r > 0 { + rows = uint16(r) + } + + if err := h.client.ptyManager.StartPTY(sessionID, cols, rows); err != nil { + response.Status = "error" + response.Error = err.Error() + return response + } + + response.Status = "ok" + response.Data = map[string]interface{}{"session_id": sessionID} + return response +} + +func (h *Handler) handlePTYStop(msg Message) { + sessionID, _ := msg.Params["session_id"].(string) + if sessionID != "" { + h.client.ptyManager.StopPTY(sessionID) + } +} + +func (h *Handler) handlePTYResize(msg Message) { + sessionID, _ := msg.Params["session_id"].(string) + cols := uint16(80) + rows := uint16(24) + if c, ok := msg.Params["cols"].(float64); ok && c > 0 { + cols = uint16(c) + } + if r, ok := msg.Params["rows"].(float64); ok && r > 0 { + rows = uint16(r) + } + if sessionID != "" { + h.client.ptyManager.ResizePTY(sessionID, cols, rows) + } +} diff --git a/agent-bsd/ws/pty_manager.go b/agent-bsd/ws/pty_manager.go new file mode 100644 index 0000000..12988cf --- /dev/null +++ b/agent-bsd/ws/pty_manager.go @@ -0,0 +1,170 @@ +package ws + +import ( + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + "sync" + "syscall" + "unsafe" + + "github.com/creack/pty" +) + +type PTYSession struct { + ID string + ptmx *os.File + cmd *exec.Cmd + done chan struct{} +} + +type PTYManager struct { + sessions map[string]*PTYSession + mu sync.Mutex + client *Client +} + +func NewPTYManager(c *Client) *PTYManager { + return &PTYManager{ + sessions: make(map[string]*PTYSession), + client: c, + } +} + +// StartPTY startet eine neue PTY-Session mit /bin/sh +func (pm *PTYManager) StartPTY(sessionID string, cols, rows uint16) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + if _, exists := pm.sessions[sessionID]; exists { + return fmt.Errorf("PTY-Session %s existiert bereits", sessionID) + } + + cmd := exec.Command("/bin/sh", "-l") + cmd.Env = append(os.Environ(), + "TERM=xterm-256color", + fmt.Sprintf("COLUMNS=%d", cols), + fmt.Sprintf("LINES=%d", rows), + ) + + ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: rows, Cols: cols}) + if err != nil { + return fmt.Errorf("PTY starten fehlgeschlagen: %w", err) + } + + session := &PTYSession{ + ID: sessionID, + ptmx: ptmx, + cmd: cmd, + done: make(chan struct{}), + } + + pm.sessions[sessionID] = session + + // PTY-Ausgabe lesen und an Backend senden + go pm.readPTYOutput(session) + + log.Printf("PTY-Session gestartet: %s (%dx%d)", sessionID, cols, rows) + return nil +} + +// StopPTY beendet eine PTY-Session +func (pm *PTYManager) StopPTY(sessionID string) { + pm.mu.Lock() + session, exists := pm.sessions[sessionID] + if exists { + delete(pm.sessions, sessionID) + } + pm.mu.Unlock() + + if !exists { + return + } + + close(session.done) + session.cmd.Process.Kill() + session.ptmx.Close() + log.Printf("PTY-Session beendet: %s", sessionID) +} + +// WriteToPTY schreibt Daten in die PTY-Stdin +func (pm *PTYManager) WriteToPTY(sessionID string, data []byte) error { + pm.mu.Lock() + session, exists := pm.sessions[sessionID] + pm.mu.Unlock() + + if !exists { + return fmt.Errorf("PTY-Session %s nicht gefunden", sessionID) + } + + _, err := session.ptmx.Write(data) + return err +} + +// ResizePTY aendert die PTY-Groesse +func (pm *PTYManager) ResizePTY(sessionID string, cols, rows uint16) error { + pm.mu.Lock() + session, exists := pm.sessions[sessionID] + pm.mu.Unlock() + + if !exists { + return fmt.Errorf("PTY-Session %s nicht gefunden", sessionID) + } + + ws := &struct { + Rows, Cols, X, Y uint16 + }{Rows: rows, Cols: cols} + + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, + session.ptmx.Fd(), + syscall.TIOCSWINSZ, + uintptr(unsafe.Pointer(ws)), + ) + if errno != 0 { + return errno + } + return nil +} + +func (pm *PTYManager) readPTYOutput(session *PTYSession) { + buf := make([]byte, 4096) + for { + select { + case <-session.done: + return + default: + } + + n, err := session.ptmx.Read(buf) + if err != nil { + // PTY geschlossen — Session beenden und Backend informieren + pm.StopPTY(session.ID) + // Exit-Nachricht senden + msg := map[string]interface{}{ + "type": "pty_exit", + "session_id": session.ID, + } + if data, err := json.Marshal(msg); err == nil { + pm.client.SendMessage(Message{ + Type: "event", + Event: "pty_exit", + Params: map[string]interface{}{ + "session_id": session.ID, + }, + }) + _ = data + } + return + } + + if n > 0 { + // Daten als Binary-Message senden (session_id Prefix) + if err := pm.client.messageHandler.SendSessionData(session.ID, buf[:n]); err != nil { + log.Printf("PTY-Daten senden fehlgeschlagen: %v", err) + return + } + } + } +}