feat: initialisation complète du CORE ProxmoxPanel
Backend Go 1.23+ : - API REST + WebSocket (chi, gorilla/websocket) - Authentification PAM via SSH + JWT RS256 - Chiffrement AES-256-GCM pour secrets SQLite - Pool SSH, client Proxmox REST, hub WebSocket pub/sub - Système de modules compilés à initialisation conditionnelle - Audit log, migrations SQLite versionnées Frontend Vue 3 + Vite + TypeScript : - Thème Neumorphism sombre/clair (CSS custom properties) - Wizard d'installation, Dashboard drag-drop, Terminal xterm.js - Toutes les vues CORE + stubs modules optionnels - i18n EN/FR (vue-i18n v11) Infrastructure : - Docker multi-stage (Go → alpine, Node → nginx) - docker-compose.yml, .gitattributes, LICENSE MIT, README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
5dbcb1df07
66 changed files with 10370 additions and 0 deletions
151
backend/internal/api/terminal.go
Normal file
151
backend/internal/api/terminal.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// 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))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue