Bug principal : l'SSHAuthenticator est créé au démarrage avec host="" (DB vide avant installation). Après configure, il gardait host vide. Le login lisait maintenant le ssh_host depuis la DB à chaque requête. Logs ajoutés : - ssh_auth.go : dial SSH, succès, échec avec détail d'erreur - auth.go : host SSH utilisé, résultat auth à chaque login - updates.go : credentials SSH, démarrage/fin de job - terminal.go : ouverture/échec session SSH Frontend : - auth.store.ts : gère les réponses non-JSON sur erreur HTTP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
156 lines
4.4 KiB
Go
156 lines
4.4 KiB
Go
// 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"
|
|
"log"
|
|
"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 == "" {
|
|
log.Printf("[terminal] SSH non configuré — user=%s", claims.Username)
|
|
conn.WriteMessage(gorillaws.TextMessage, []byte("\r\nErreur : SSH non configuré\r\n"))
|
|
return
|
|
}
|
|
|
|
log.Printf("[terminal] Ouverture session — user=%s ssh_host=%s", claims.Username, sshHost)
|
|
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 {
|
|
log.Printf("[terminal] Échec connexion SSH %s@%s : %v", sshUser, sshHost, err)
|
|
conn.WriteMessage(gorillaws.TextMessage, []byte(fmt.Sprintf("\r\nErreur SSH : %v\r\n", err)))
|
|
return
|
|
}
|
|
log.Printf("[terminal] Connecté — %s@%s", sshUser, sshHost)
|
|
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))
|
|
}
|