feat: add server stats

This commit is contained in:
2024-11-13 22:45:03 +00:00
parent 2d4c81e15d
commit 7a00992ff9
12 changed files with 452 additions and 32 deletions
+105
View File
@@ -0,0 +1,105 @@
package term
import (
"crypto/tls"
"encoding/json"
"fmt"
"log"
"strings"
fastWs "github.com/fasthttp/websocket"
"github.com/gofiber/contrib/websocket"
"rul.sh/vaulterm/lib"
)
type IncusWebsocketSession struct {
Type string `json:"type"` // "qemu" | "lxc"
Instance string `json:"instance"`
User *int `json:"user"`
Shell string `json:"shell"`
}
func (i *IncusWebsocketSession) NewTerminal(c *websocket.Conn, incus *lib.IncusServer) error {
if i.Shell == "" {
i.Shell = "/bin/sh"
}
exec, err := incus.InstanceExec(i.Instance, []string{i.Shell}, &lib.IncusInstanceExecOptions{
Interactive: true,
User: i.User,
})
if err != nil {
return err
}
clientCert, err := incus.GetCertificate()
if err != nil {
return err
}
dialer := fastWs.Dialer{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{*clientCert},
},
}
controlUrl := fmt.Sprintf("wss://%s:%d%s/websocket?secret=%s", incus.HostName, incus.Port, exec.Operation, exec.Control)
controlWs, _, err := dialer.Dial(controlUrl, nil)
if err != nil {
return err
}
defer controlWs.Close()
ttyUrl := fmt.Sprintf("wss://%s:%d%s/websocket?secret=%s", incus.HostName, incus.Port, exec.Operation, exec.Secret)
ttyWs, _, err := dialer.Dial(ttyUrl, nil)
if err != nil {
return err
}
defer ttyWs.Close()
go func() {
for {
_, msg, err := c.ReadMessage()
if err != nil {
log.Println("Error reading from client:", err)
break
}
if strings.HasPrefix(string(msg), "\x01") {
parts := strings.Split(string(msg[1:]), ",")
if len(parts) == 2 {
resizeCmd, _ := json.Marshal(map[string]interface{}{
"command": "window-resize",
"args": map[string]string{
"width": parts[0],
"height": parts[1],
},
})
controlWs.WriteMessage(websocket.BinaryMessage, resizeCmd)
}
continue
}
if err = ttyWs.WriteMessage(websocket.BinaryMessage, msg); err != nil {
log.Println("Error writing to Incus:", err)
break
}
}
}()
for {
t, msg, err := ttyWs.ReadMessage()
if err != nil {
log.Println("Error reading from Incus:", err)
break
}
if err = c.WriteMessage(t, msg); err != nil {
log.Println("Error writing to client:", err)
break
}
}
return nil
}
+163
View File
@@ -0,0 +1,163 @@
package term
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"net/url"
"strconv"
"strings"
fastWs "github.com/fasthttp/websocket"
"github.com/gofiber/contrib/websocket"
"rul.sh/vaulterm/lib"
)
// https://github.com/proxmox/pve-xtermjs/blob/master/README
func NewTerminalSession(c *websocket.Conn, pve *lib.PVEServer, instance *lib.PVEInstance) error {
access, err := pve.GetAccessTicket()
if err != nil {
log.Println("Error getting access ticket:", err)
return err
}
ticket, err := pve.GetVNCTicket(access, instance, false)
if err != nil {
log.Println("Error getting vnc ticket:", err)
return err
}
url := fmt.Sprintf("wss://%s:%d/api2/json/nodes/%s/%s/%s/vncwebsocket?port=%s&vncticket=%s",
pve.HostName, pve.Port, instance.Node, instance.Type, instance.VMID, ticket.Port, url.QueryEscape(ticket.Ticket))
headers := http.Header{}
headers.Add("Authorization", "PVEAPIToken="+access.Username)
headers.Add("Cookie", "PVEAuthCookie="+access.Ticket)
dialer := fastWs.Dialer{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
ws, _, err := dialer.Dial(url, headers)
if err != nil {
log.Println("Error connecting to Proxmox WebSocket:", err)
return err
}
defer ws.Close()
// Send first ticket line
ws.WriteMessage(fastWs.TextMessage, []byte(fmt.Sprintf("%s:%s\n", access.Username, access.Ticket)))
go func() {
for {
t, msg, err := c.ReadMessage()
if err != nil {
log.Println("Error reading from client:", err)
break
}
if strings.HasPrefix(string(msg), "\x01") {
parts := strings.Split(string(msg[1:]), ",")
if len(parts) == 2 {
width, _ := strconv.Atoi(parts[0])
height, _ := strconv.Atoi(parts[1])
ws.WriteMessage(fastWs.TextMessage, []byte(fmt.Sprintf("1:%d:%d:", width, height)))
}
continue
}
msg = []byte(fmt.Sprintf("0:%d:%s\n", len(msg), string(msg)))
if err = ws.WriteMessage(t, msg); err != nil {
log.Println("Error writing to Proxmox:", err)
break
}
}
}()
for {
t, msg, err := ws.ReadMessage()
if err != nil {
log.Println("Error reading from Proxmox:", err)
break
}
if string(msg) == "OK" {
continue
}
if err = c.WriteMessage(t, msg); err != nil {
log.Println("Error writing to client:", err)
break
}
}
return nil
}
func NewVNCSession(c *websocket.Conn, pve *lib.PVEServer, instance *lib.PVEInstance) error {
access, err := pve.GetAccessTicket()
if err != nil {
log.Println("Error getting access ticket:", err)
return err
}
ticket, err := pve.GetVNCTicket(access, instance, true)
if err != nil {
log.Println("Error getting vnc ticket:", err)
return err
}
url := fmt.Sprintf("wss://%s:%d/api2/json/nodes/%s/%s/%s/vncwebsocket?port=%s&vncticket=%s",
pve.HostName, pve.Port, instance.Node, instance.Type, instance.VMID, ticket.Port, url.QueryEscape(ticket.Ticket))
headers := http.Header{}
headers.Add("Authorization", "PVEAPIToken="+access.Username)
headers.Add("Cookie", "PVEAuthCookie="+access.Ticket)
dialer := fastWs.Dialer{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
ws, _, err := dialer.Dial(url, headers)
if err != nil {
log.Println("Error connecting to Proxmox WebSocket:", err)
return err
}
defer ws.Close()
// Send vnc password
c.WriteMessage(fastWs.TextMessage, []byte(fmt.Sprintf("\x01%s", ticket.Ticket)))
go func() {
for {
t, msg, err := c.ReadMessage()
if err != nil {
log.Println("Error reading from client:", err)
break
}
if err = ws.WriteMessage(t, msg); err != nil {
log.Println("Error writing to Proxmox:", err)
break
}
}
}()
for {
t, msg, err := ws.ReadMessage()
if err != nil {
log.Println("Error reading from Proxmox:", err)
break
}
if err = c.WriteMessage(t, msg); err != nil {
log.Println("Error writing to client:", err)
break
}
}
return nil
}
+100
View File
@@ -0,0 +1,100 @@
package term
import (
"io"
"log"
"strconv"
"strings"
"github.com/gofiber/contrib/websocket"
"rul.sh/vaulterm/lib"
)
func NewSSHWebsocketSession(c *websocket.Conn, client *lib.SSHClient) error {
if err := client.Connect(); err != nil {
log.Printf("error connecting to SSH: %v", err)
return err
}
defer client.Close()
shell, err := client.StartPtyShell()
if err != nil {
log.Printf("error starting SSH shell: %v", err)
return err
}
session := shell.Session
defer session.Close()
// Goroutine to send SSH stdout to WebSocket
go func() {
buf := make([]byte, 1024)
for {
n, err := shell.Stdout.Read(buf)
if err != nil {
if err != io.EOF {
log.Printf("error reading from SSH stdout: %v", err)
}
break
}
if err := c.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
log.Printf("error writing to websocket: %v", err)
break
}
}
}()
// Goroutine to handle SSH stderr
go func() {
buf := make([]byte, 1024)
for {
n, err := shell.Stderr.Read(buf)
if err != nil {
if err != io.EOF {
log.Printf("error reading from SSH stderr: %v", err)
}
break
}
if err := c.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
log.Printf("error writing to websocket: %v", err)
break
}
}
}()
// Handle WebSocket to SSH data streaming
go func() {
defer session.Close()
for {
_, msg, err := c.ReadMessage()
if err != nil {
log.Printf("error reading from websocket: %v", err)
break
}
if strings.HasPrefix(string(msg), "\x01") {
parts := strings.Split(string(msg[1:]), ",")
if len(parts) == 2 {
width, _ := strconv.Atoi(parts[0])
height, _ := strconv.Atoi(parts[1])
session.WindowChange(height, width)
}
continue
}
shell.Stdin.Write(msg)
}
log.Println("SSH session closed")
}()
// Wait for the SSH session to close
if err := session.Wait(); err != nil {
log.Printf("SSH session ended with error: %v", err)
return err
}
return nil
}
+119
View File
@@ -0,0 +1,119 @@
package term
import (
"log"
"github.com/gofiber/contrib/websocket"
"rul.sh/vaulterm/app/hosts"
"rul.sh/vaulterm/lib"
"rul.sh/vaulterm/models"
"rul.sh/vaulterm/utils"
)
func HandleTerm(c *websocket.Conn) {
hostId := c.Query("hostId")
user := utils.GetUserWs(c)
hostRepo := hosts.NewRepository(&hosts.Hosts{User: user})
data, err := hostRepo.Get(hostId)
if data == nil || !data.HasAccess(&user.User) {
log.Printf("Cannot find host! %v\n", err)
c.WriteMessage(websocket.TextMessage, []byte("Host not found"))
return
}
switch data.Host.Type {
case "ssh":
sshHandler(c, data)
case "pve":
pveHandler(c, data)
case "incus":
incusHandler(c, data)
default:
c.WriteMessage(websocket.TextMessage, []byte("Invalid host type"))
}
}
func sshHandler(c *websocket.Conn, data *models.HostDecrypted) {
cfg := lib.NewSSHClient(&lib.SSHClientConfig{
HostName: data.Host.Host,
Port: data.Port,
Key: data.Key,
AltKey: data.AltKey,
})
if err := NewSSHWebsocketSession(c, cfg); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
}
func pveHandler(c *websocket.Conn, data *models.HostDecrypted) {
client := c.Query("client")
username, _ := data.Key["username"].(string)
realm, _ := data.Key["realm"].(string)
password, _ := data.Key["password"].(string)
pve := &lib.PVEServer{
HostName: data.Host.Host,
Port: data.Port,
Username: username,
Realm: realm,
Password: password,
}
var i *lib.PVEInstance
if err := utils.ParseMapInterface(data.Host.Metadata, &i); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
return
}
if i == nil || i.Type == "" || i.Node == "" || i.VMID == "" {
c.WriteMessage(websocket.TextMessage, []byte("Invalid pve instance metadata"))
return
}
var err error
if client == "vnc" {
err = NewVNCSession(c, pve, i)
} else {
err = NewTerminalSession(c, pve, i)
}
if err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
}
func incusHandler(c *websocket.Conn, data *models.HostDecrypted) {
shell := c.Query("shell")
cert, _ := data.Key["cert"].(string)
key, _ := data.Key["key"].(string)
if cert == "" || key == "" {
c.WriteMessage(websocket.TextMessage, []byte("Missing certificate or key"))
return
}
incus := &lib.IncusServer{
HostName: data.Host.Host,
Port: data.Port,
ClientCert: cert,
ClientKey: key,
}
session := &IncusWebsocketSession{}
if err := utils.ParseMapInterface(data.Host.Metadata, session); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
return
}
if shell != "" {
session.Shell = shell
}
if err := session.NewTerminal(c, incus); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
}