feat: add pve lxc xtermjs console

This commit is contained in:
Khairul Hidayat 2024-11-06 13:49:41 +00:00
parent b9d879330a
commit bde42ca729
7 changed files with 289 additions and 28 deletions

1
server/.gitignore vendored
View File

@ -1 +1,2 @@
tmp/
.env

View File

@ -5,23 +5,24 @@ go 1.21.1
require (
github.com/gofiber/contrib/websocket v1.3.2
github.com/gofiber/fiber/v2 v2.52.5
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.28.0
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/fasthttp/websocket v1.5.8 // indirect
github.com/fasthttp/websocket v1.5.10
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.7 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.52.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/net v0.26.0 // indirect
)
require golang.org/x/sys v0.26.0 // indirect

View File

@ -2,16 +2,18 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fasthttp/websocket v1.5.8 h1:k5DpirKkftIF/w1R8ZzjSgARJrs54Je9YJK37DL/Ah8=
github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0=
github.com/fasthttp/websocket v1.5.10 h1:bc7NIGyrg1L6sd5pRzCIbXpro54SZLEluZCu0rOpcN4=
github.com/fasthttp/websocket v1.5.10/go.mod h1:BwHeuXGWzCW1/BIKUKD3+qfCl+cTdsHu/f243NcAI/Q=
github.com/gofiber/contrib/websocket v1.3.2 h1:AUq5PYeKwK50s0nQrnluuINYeep1c4nRCJ0NWsV3cvg=
github.com/gofiber/contrib/websocket v1.3.2/go.mod h1:07u6QGMsvX+sx7iGNCl5xhzuUVArWwLQ3tBIH24i+S8=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@ -23,20 +25,20 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8=
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=

127
server/lib/pve.go Normal file
View File

@ -0,0 +1,127 @@
package lib
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
)
type PVEServer struct {
HostName string
Port int
Username string
Password string
}
type PVERequestInit struct {
Body map[string]string
Ticket string
CSRF string
}
func fetch(method string, url string, cfg *PVERequestInit) ([]byte, error) {
var body io.Reader
if cfg.Body != nil {
json, _ := json.Marshal(cfg.Body)
body = bytes.NewBuffer(json)
}
req, _ := http.NewRequest(method, url, body)
if cfg.Ticket != "" {
req.Header.Add("Cookie", "PVEAuthCookie="+cfg.Ticket)
}
if cfg.CSRF != "" {
req.Header.Add("CSRFPreventionToken", cfg.CSRF)
}
if body != nil {
req.Header.Add("Content-Type", "application/json")
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{
Transport: tr,
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("request failed with status code %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
type PVEAccessTicket struct {
CSRFPreventionToken string `json:"CSRFPreventionToken"`
Ticket string `json:"ticket"`
Username string `json:"username"`
}
func (pve *PVEServer) GetAccessTicket() (*PVEAccessTicket, error) {
url := fmt.Sprintf("https://%s:%d/api2/json/access/ticket", pve.HostName, pve.Port)
body, err := fetch("POST", url, &PVERequestInit{Body: map[string]string{
"username": pve.Username,
"password": pve.Password,
}})
if err != nil {
return nil, err
}
var res struct {
Data PVEAccessTicket `json:"data"`
}
if err := json.Unmarshal(body, &res); err != nil {
return nil, err
}
return &res.Data, nil
}
type PVEInstance struct {
Type string
Node string
VMID string
}
type PVEVNCTicketData struct {
Port string `json:"port"`
User string `json:"user"`
Ticket string `json:"ticket"`
CERT string `json:"cert"`
Upid string `json:"upid"`
}
func (pve *PVEServer) GetVNCTicket(access *PVEAccessTicket, instance *PVEInstance, isVNC bool) (*PVEVNCTicketData, error) {
proxyType := "termproxy"
if isVNC {
proxyType = "vncproxy"
}
url := fmt.Sprintf("https://%s:%d/api2/json/nodes/%s/%s/%s/%s",
pve.HostName, pve.Port, instance.Node, instance.Type, instance.VMID, proxyType)
body, err := fetch("POST", url, &PVERequestInit{Ticket: access.Ticket, CSRF: access.CSRFPreventionToken})
if err != nil {
return nil, err
}
var res struct {
Data PVEVNCTicketData `json:"data"`
}
if err := json.Unmarshal(body, &res); err != nil {
return nil, err
}
return &res.Data, nil
}

96
server/lib/pve_session.go Normal file
View File

@ -0,0 +1,96 @@
package lib
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"net/url"
"strconv"
"strings"
fastWs "github.com/fasthttp/websocket"
"github.com/gofiber/contrib/websocket"
)
type PVEConfig struct {
HostName string
User string
Password string
Port int
PrivateKey string
PrivateKeyPassphrase string
}
func (pve *PVEServer) NewTerminalSession(c *websocket.Conn, access *PVEAccessTicket, instance *PVEInstance, ticket *PVEVNCTicketData) error {
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)))
// https://github.com/proxmox/pve-xtermjs/blob/master/README
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)))
err = ws.WriteMessage(t, msg)
if 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
}
err = c.WriteMessage(t, msg)
if err != nil {
log.Println("Error writing to client:", err)
break
}
}
return nil
}

View File

@ -145,9 +145,10 @@ func NewSSHWebsocketSession(c *websocket.Conn, cfg *SSHConfig) error {
height, _ := strconv.Atoi(parts[1])
session.WindowChange(height, width)
}
} else {
stdinPipe.Write(msg)
continue
}
stdinPipe.Write(msg)
}
}()

View File

@ -1,16 +1,25 @@
package main
import (
"fmt"
"os"
"github.com/gofiber/contrib/websocket"
"github.com/gofiber/fiber/v2"
"github.com/joho/godotenv"
"rul.sh/vaulterm/lib"
)
func main() {
godotenv.Load()
app := fiber.New()
var pve = &lib.PVEServer{
HostName: "pve",
Port: 8006,
Username: os.Getenv("PVE_USERNAME"),
Password: os.Getenv("PVE_PASSWORD"),
}
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
@ -22,18 +31,42 @@ func main() {
return fiber.ErrUpgradeRequired
})
// app.Get("/ws/ssh", websocket.New(func(c *websocket.Conn) {
// err := lib.NewSSHWebsocketSession(c, &lib.SSHConfig{
// HostName: "10.0.0.102",
// User: "root",
// Password: "ausya2",
// })
// if err != nil {
// msg := fmt.Sprintf("\r\n%s\r\n", err.Error())
// c.WriteMessage(websocket.TextMessage, []byte(msg))
// }
// }))
app.Get("/ws/ssh", websocket.New(func(c *websocket.Conn) {
err := lib.NewSSHWebsocketSession(c, &lib.SSHConfig{
HostName: "10.0.0.102",
User: "root",
Password: "ausya2",
})
if err != nil {
msg := fmt.Sprintf("\r\n%s\r\n", err.Error())
c.WriteMessage(websocket.TextMessage, []byte(msg))
node := &lib.PVEInstance{
Type: "lxc",
Node: "pve",
VMID: "102",
}
}))
access, err := pve.GetAccessTicket()
if err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
return
}
ticket, err := pve.GetVNCTicket(access, node, false)
if err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
return
}
if err := pve.NewTerminalSession(c, access, node, ticket); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
}))
app.Listen(":3000")
}