From 9239bee6f22452435838957419ba3cdf78a1ccec Mon Sep 17 00:00:00 2001 From: Pascal Brogle Date: Mon, 9 Oct 2023 21:30:35 +0200 Subject: [PATCH] feat: support login for non-root user --- proxmox/websocket.go | 135 ++++++++++++++++++++++++++++++++++++++++++- rest/node.go | 14 +++++ 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/proxmox/websocket.go b/proxmox/websocket.go index 0b2ebd4..94c1fea 100644 --- a/proxmox/websocket.go +++ b/proxmox/websocket.go @@ -1,9 +1,11 @@ package proxmox import ( + "bufio" "context" "encoding/base64" "fmt" + "io" "regexp" "strconv" "strings" @@ -45,7 +47,18 @@ func (s *Service) NewNodeVNCWebSocketConnection(ctx context.Context, nodeName st } }() - return &VNCWebSocketClient{conn: conn, ticker: ticker}, nil + client := &VNCWebSocketClient{conn: conn, ticker: ticker} + + credentials := s.restclient.Credentials() + // login is required for non-root user: https://git.proxmox.com/?p=pve-manager.git;a=blob;f=PVE/API2/Nodes.pm;h=0843c3a3c6cee7c763bf4ac9d8b75ab298f1373e;hb=HEAD#l913 + if credentials.Username != "root@pam" { + err := client.Login(strings.Replace(credentials.Username, "@pam", "", 1), credentials.Password) + if err != nil { + return nil, fmt.Errorf("login failed: %v", err) + } + } + + return client, nil } func (c *VNCWebSocketClient) Close() { @@ -63,6 +76,126 @@ func (c *VNCWebSocketClient) Write(cmd string) error { return c.sendFinMessage() } +func (c *VNCWebSocketClient) ExpectMessage(expected string) error { + _, msg, err := c.conn.ReadMessage() + if err != nil { + return fmt.Errorf("failed to read message: %v", err) + } + + if !strings.Contains(string(msg), expected) { + return fmt.Errorf("read expected '%s' did not contain expected '%s'", string(msg), expected) + } + return nil +} + +func (c *VNCWebSocketClient) ExpectMessageMultiline(expected string) error { + dataStream := make(chan string, 1) + readErr := make(chan error) + stop := make(chan bool) + go func() { + loop: + for { + select { + case <-stop: + break loop + default: + var r io.Reader + _, r, err := c.conn.NextReader() + if err != nil { + readErr <- err + break loop + } + + br := bufio.NewReader(r) + s, e := ReadLine(br) + for e == nil { + dataStream <- s + s, e = ReadLine(br) + } + if e == io.EOF { + continue loop + } else { + readErr <- e + } + break loop + + } + } + close(dataStream) + close(readErr) + }() + defer close(stop) + + for { + select { + case data := <-dataStream: + if strings.Contains(data, expected) { + stop <- true + return nil + } + case <-time.After(time.Duration(10) * time.Second): + return fmt.Errorf("timeout while reading '%s' from multiline response", expected) + case err := <-readErr: + return err + } + } +} + +func ReadLine(r *bufio.Reader) (string, error) { + var ( + isPrefix bool = true + err error = nil + line, ln []byte + ) + for isPrefix && err == nil { + line, isPrefix, err = r.ReadLine() + ln = append(ln, line...) + } + return string(ln), err +} + +func (c *VNCWebSocketClient) WriteMessage(message string) error { + b := []byte(message) + bheader := []byte(fmt.Sprintf("0:%d:", len(b))) + bmsg := append(bheader, b...) + if err := c.conn.WriteMessage(websocket.BinaryMessage, bmsg); err != nil { + return fmt.Errorf("failed writing message '%s': %v", message, err) + } + return nil +} + +func (c *VNCWebSocketClient) Login(username string, password string) error { + // proxmox task result + if err := c.ExpectMessage("OK"); err != nil { + return err + } + // linux login prompt + if err := c.ExpectMessage("login"); err != nil { + return err + } + + if err := c.WriteMessage(fmt.Sprintf("%s\n", username)); err != nil { + return err + } + + // password prompt for user + if err := c.ExpectMessage(username); err != nil { + return err + } + if err := c.ExpectMessage("Password"); err != nil { + return err + } + if err := c.WriteMessage(fmt.Sprintf("%s\n", password)); err != nil { + return err + } + + // successful login + if err := c.ExpectMessageMultiline("Last login"); err != nil { + return err + } + return nil +} + func (c *VNCWebSocketClient) WriteFile(ctx context.Context, content, path string) error { c.Exec(ctx, fmt.Sprintf("rm %s", path)) if out, _, err := c.Exec(ctx, fmt.Sprintf("touch %s", path)); err != nil { diff --git a/rest/node.go b/rest/node.go index 4f47d74..d50db71 100644 --- a/rest/node.go +++ b/rest/node.go @@ -2,8 +2,10 @@ package rest import ( "context" + "errors" "fmt" "net/url" + "strings" "github.com/sp-yduck/proxmox-go/api" ) @@ -30,6 +32,10 @@ func (c *RESTClient) GetNode(ctx context.Context, name string) (*api.Node, error } func (c *RESTClient) CreateNodeTermProxy(ctx context.Context, nodeName string, option api.TermProxyOption) (*api.TermProxy, error) { + if !strings.HasSuffix(c.credentials.Username, "@pam") { + return nil, errors.New("term proxy is only possible with pam users") + } + path := fmt.Sprintf("/nodes/%s/termproxy", nodeName) var termProxy *api.TermProxy if err := c.Post(ctx, path, option, &termProxy); err != nil { @@ -39,6 +45,10 @@ func (c *RESTClient) CreateNodeTermProxy(ctx context.Context, nodeName string, o } func (c *RESTClient) CreateNodeVNCShell(ctx context.Context, nodeName string, option api.VNCShellOption) (*api.TermProxy, error) { + if !strings.HasSuffix(c.credentials.Username, "@pam") { + return nil, errors.New("vnc shell is only possible with pam users") + } + path := fmt.Sprintf("/nodes/%s/vncshell", nodeName) var termProxy *api.TermProxy if err := c.Post(ctx, path, option, &termProxy); err != nil { @@ -55,3 +65,7 @@ func (c *RESTClient) GetNodeVNCWebSocket(ctx context.Context, nodeName, port, vn } return websocket, nil } + +func (c *RESTClient) Credentials() *TicketRequest { + return c.credentials +}