// Handler pour le terminal SSH interactif via WebSocket + PTY. // Utilise golang.org/x/crypto/ssh pour la connexion et gorilla/websocket pour le transport. package api import ( "encoding/json" "fmt" "net/http" "time" "git.geronzi.fr/proxmoxPanel/core/backend/internal/audit" "git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto" "git.geronzi.fr/proxmoxPanel/core/backend/internal/db" gorillaws "github.com/gorilla/websocket" gossh "golang.org/x/crypto/ssh" ) // TerminalHandler gère les sessions de terminal SSH interactif. type TerminalHandler struct { db *db.DB auditLogger *audit.Logger encryptor *crypto.Encryptor } // NewTerminalHandler crée un TerminalHandler. func NewTerminalHandler(database *db.DB, auditLog *audit.Logger, enc *crypto.Encryptor) *TerminalHandler { return &TerminalHandler{db: database, auditLogger: auditLog, encryptor: enc} } // terminalCmd représente un message de contrôle envoyé via WebSocket. type terminalCmd struct { Type string `json:"type"` // "resize" | "data" Cols int `json:"cols,omitempty"` Rows int `json:"rows,omitempty"` } // WebSocket ouvre un terminal SSH interactif via WebSocket. // GET /ws/terminal // Query params: host (optionnel, défaut = ssh_host depuis config) func (h *TerminalHandler) WebSocket(w http.ResponseWriter, r *http.Request) { claims := GetClaims(r) // Connexion WebSocket conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close() // Récupérer les params SSH sshHost := r.URL.Query().Get("host") if sshHost == "" { sshHost, _, _ = h.db.GetSetting("ssh_host") } sshUser, _, _ := h.db.GetSetting("ssh_username") encryptedPass, _, _ := h.db.GetSetting("ssh_password") sshPass, _ := h.encryptor.Decrypt(encryptedPass) if sshHost == "" { conn.WriteMessage(gorillaws.TextMessage, []byte("\r\nErreur : SSH non configuré\r\n")) return } h.auditLogger.Log(&claims.UserID, claims.Username, "terminal_open", sshHost, nil, clientIP(r)) // Établir la connexion SSH sshConfig := &gossh.ClientConfig{ User: sshUser, Auth: []gossh.AuthMethod{ gossh.Password(sshPass), }, Timeout: 15 * time.Second, HostKeyCallback: gossh.InsecureIgnoreHostKey(), } sshClient, err := gossh.Dial("tcp", sshHost, sshConfig) if err != nil { conn.WriteMessage(gorillaws.TextMessage, []byte(fmt.Sprintf("\r\nErreur SSH : %v\r\n", err))) return } defer sshClient.Close() // Créer une session SSH avec pseudo-terminal session, err := sshClient.NewSession() if err != nil { conn.WriteMessage(gorillaws.TextMessage, []byte(fmt.Sprintf("\r\nErreur session SSH : %v\r\n", err))) return } defer session.Close() // Configurer le PTY (terminal 80x24 par défaut) modes := gossh.TerminalModes{ gossh.ECHO: 1, gossh.TTY_OP_ISPEED: 14400, gossh.TTY_OP_OSPEED: 14400, } if err := session.RequestPty("xterm-256color", 24, 80, modes); err != nil { conn.WriteMessage(gorillaws.TextMessage, []byte(fmt.Sprintf("\r\nErreur PTY : %v\r\n", err))) return } // Pipes stdin/stdout entre WebSocket et SSH stdinPipe, err := session.StdinPipe() if err != nil { return } stdoutPipe, err := session.StdoutPipe() if err != nil { return } if err := session.Shell(); err != nil { conn.WriteMessage(gorillaws.TextMessage, []byte(fmt.Sprintf("\r\nErreur shell : %v\r\n", err))) return } // Goroutine : SSH stdout → WebSocket go func() { buf := make([]byte, 4096) for { n, err := stdoutPipe.Read(buf) if err != nil { break } conn.WriteMessage(gorillaws.BinaryMessage, buf[:n]) } conn.Close() }() // Boucle principale : WebSocket → SSH stdin for { _, msg, err := conn.ReadMessage() if err != nil { break } // Détecter les messages de contrôle JSON (ex: resize) if len(msg) > 0 && msg[0] == '{' { var cmd terminalCmd if json.Unmarshal(msg, &cmd) == nil && cmd.Type == "resize" && cmd.Cols > 0 && cmd.Rows > 0 { session.WindowChange(cmd.Rows, cmd.Cols) continue } } // Données brutes → stdin SSH stdinPipe.Write(msg) } h.auditLogger.Log(&claims.UserID, claims.Username, "terminal_close", sshHost, nil, clientIP(r)) }