package api import ( "encoding/json" "log" "net/http" "os" "os/exec" "sync" "time" "github.com/creack/pty/v2" "github.com/gorilla/websocket" ) var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } type wsMessage struct { Type string `json:"type"` Data string `json:"data"` Rows uint16 `json:"rows,omitempty"` Cols uint16 `json:"cols,omitempty"` } func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Printf("ws upgrade: %v", err) return } defer conn.Close() shell := "/bin/sh" if s, err := exec.LookPath("bash"); err == nil { shell = s } cmd := exec.Command(shell) cmd.Env = append(os.Environ(), "TERM=xterm-256color") ptmx, err := pty.Start(cmd) if err != nil { log.Printf("pty start: %v", err) conn.WriteJSON(wsMessage{Type: "error", Data: err.Error()}) return } defer func() { ptmx.Close() if cmd.Process != nil { cmd.Process.Kill() cmd.Wait() } }() var once sync.Once cleanup := func() { once.Do(func() { ptmx.Close() if cmd.Process != nil { cmd.Process.Kill() cmd.Wait() } }) } // PTY -> WebSocket go func() { buf := make([]byte, 4096) for { n, err := ptmx.Read(buf) if err != nil { cleanup() conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) return } if err := conn.WriteJSON(wsMessage{ Type: "output", Data: string(buf[:n]), }); err != nil { cleanup() return } } }() // WebSocket -> PTY conn.SetReadLimit(1 << 20) // 1MB conn.SetReadDeadline(time.Time{}) for { _, raw, err := conn.ReadMessage() if err != nil { cleanup() return } var msg wsMessage if err := json.Unmarshal(raw, &msg); err != nil { continue } switch msg.Type { case "input": if _, err := ptmx.Write([]byte(msg.Data)); err != nil { cleanup() return } case "resize": if msg.Rows > 0 && msg.Cols > 0 { pty.Setsize(ptmx, &pty.Winsize{ Rows: msg.Rows, Cols: msg.Cols, }) } } } }