Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 134 additions & 1 deletion proxmox/websocket.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package proxmox

import (
"bufio"
"context"
"encoding/base64"
"fmt"
"io"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -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() {
Expand All @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions rest/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package rest

import (
"context"
"errors"
"fmt"
"net/url"
"strings"

"github.com/sp-yduck/proxmox-go/api"
)
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
}