diff --git a/server/.gitignore b/server/.gitignore index 3fec32c..47760cd 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1 +1,2 @@ tmp/ +.env diff --git a/server/go.mod b/server/go.mod index d4db9e3..e5cf4c4 100644 --- a/server/go.mod +++ b/server/go.mod @@ -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 diff --git a/server/go.sum b/server/go.sum index 7b4694d..ab8e194 100644 --- a/server/go.sum +++ b/server/go.sum @@ -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= diff --git a/server/lib/pve.go b/server/lib/pve.go new file mode 100644 index 0000000..60360b7 --- /dev/null +++ b/server/lib/pve.go @@ -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 +} diff --git a/server/lib/pve_session.go b/server/lib/pve_session.go new file mode 100644 index 0000000..b038686 --- /dev/null +++ b/server/lib/pve_session.go @@ -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 +} diff --git a/server/lib/ssh-session.go b/server/lib/ssh_session.go similarity index 98% rename from server/lib/ssh-session.go rename to server/lib/ssh_session.go index 040d2f1..dffca97 100644 --- a/server/lib/ssh-session.go +++ b/server/lib/ssh_session.go @@ -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) } }() diff --git a/server/main.go b/server/main.go index 1812b0e..c3a5d2e 100644 --- a/server/main.go +++ b/server/main.go @@ -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") }