-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
332 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package proxmox | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/pkg/errors" | ||
|
||
"github.com/gorilla/websocket" | ||
"github.com/sp-yduck/proxmox-go/api" | ||
) | ||
|
||
const ( | ||
finMessage = "done with status: " | ||
finMessageFormat = finMessage + `[0-9]+` | ||
) | ||
|
||
type VNCWebSocketClient struct { | ||
conn *websocket.Conn | ||
} | ||
|
||
func (s *Service) NewNodeVNCWebSocketConnection(ctx context.Context, nodeName string) (*VNCWebSocketClient, error) { | ||
termProxy, err := s.restclient.CreateNodeTermProxy(ctx, nodeName, api.TermProxyOption{}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
conn, err := s.restclient.DialNodeVNCWebSocket(ctx, nodeName, *termProxy) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &VNCWebSocketClient{conn: conn}, nil | ||
} | ||
|
||
func (c *VNCWebSocketClient) Close() { | ||
c.conn.Close() | ||
} | ||
|
||
func (c *VNCWebSocketClient) Write(cmd string) error { | ||
b := []byte(fmt.Sprintf("%s\n", cmd)) | ||
bheader := []byte(fmt.Sprintf("0:%d:", len(b))) | ||
bmsg := append(bheader, b...) | ||
if err := c.conn.WriteMessage(websocket.BinaryMessage, bmsg); err != nil { | ||
return err | ||
} | ||
return c.sendFinMessage() | ||
} | ||
|
||
func (c *VNCWebSocketClient) sendFinMessage() error { | ||
b := []byte(fmt.Sprintf(`echo "%s$?"%s`, finMessage, "\n")) | ||
bheader := []byte(fmt.Sprintf("0:%d:", len(b))) | ||
bmsg := append(bheader, b...) | ||
if err := c.conn.WriteMessage(websocket.BinaryMessage, bmsg); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// Read() reads message until find fin message | ||
// then returns whole message and status code | ||
func (c *VNCWebSocketClient) Read(ctx context.Context) (outputs string, code int, err error) { | ||
done := make(chan error, 1) | ||
go func() { | ||
defer close(done) | ||
for { | ||
_, msg, err := c.conn.ReadMessage() | ||
if err != nil { | ||
done <- err | ||
return | ||
} | ||
outputs += string(msg) | ||
finMsg := parseFinMessage(string(msg)) | ||
if finMsg != "" { | ||
code, err = parseStatusFromFinMessage(finMsg) | ||
done <- err | ||
return | ||
} | ||
} | ||
}() | ||
select { | ||
case err = <-done: | ||
return outputs, code, err | ||
case <-ctx.Done(): | ||
return outputs, -1, errors.New("context deadline exceeded") | ||
} | ||
} | ||
|
||
// Exec executes a command and return error if code is not 0 | ||
// usually out contains many extra messages that is just useless | ||
func (c *VNCWebSocketClient) Exec(ctx context.Context, cmd string) (out string, code int, err error) { | ||
if err := c.Write(cmd); err != nil { | ||
return "", 0, err | ||
} | ||
out, code, err = c.Read(ctx) | ||
if err != nil { | ||
return out, code, err | ||
} | ||
if code != 0 { | ||
return out, code, errors.Errorf("exit with non zero code: %d", code) | ||
} | ||
return out, 0, nil | ||
} | ||
|
||
func parseFinMessage(message string) string { | ||
re := regexp.MustCompile(finMessageFormat) | ||
return re.FindString(message) | ||
} | ||
|
||
func parseStatusFromFinMessage(message string) (int, error) { | ||
re := regexp.MustCompile(finMessageFormat) | ||
match := re.FindString(message) | ||
if match == "" { | ||
return 0, errors.Errorf("failed to find status code from %s", message) | ||
} | ||
statusCode := strings.Split(match, ": ")[1] | ||
return strconv.Atoi(statusCode) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package proxmox | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func (s *TestSuite) TestVNCWebSocketClient() { | ||
testNode := s.getTestNode() | ||
client, err := s.service.NewNodeVNCWebSocketConnection(context.TODO(), testNode.Node) | ||
if err != nil { | ||
s.T().Fatalf("failed to create new vnc client: %v", err) | ||
} | ||
defer client.Close() | ||
|
||
if err := client.Write("pwd"); err != nil { | ||
s.T().Fatalf("write error: %v", err) | ||
} | ||
|
||
ctx, _ := context.WithTimeout(context.TODO(), 10*time.Second) | ||
out, _, err := client.Read(ctx) | ||
if err != nil { | ||
s.T().Fatalf("failed read message: %v", err) | ||
} | ||
|
||
s.T().Logf("read message: %s", out) | ||
} | ||
|
||
func (s *TestSuite) TestExec() { | ||
testNode := s.getTestNode() | ||
client, err := s.service.NewNodeVNCWebSocketConnection(context.TODO(), testNode.Node) | ||
if err != nil { | ||
s.T().Fatalf("failed to create new vnc client: %v", err) | ||
} | ||
defer client.Close() | ||
|
||
ctx, _ := context.WithTimeout(context.TODO(), 5*time.Second) | ||
out, code, err := client.Exec(ctx, "whoami | base64 | base64 -d") | ||
if err != nil { | ||
s.T().Fatalf("failed to exec command: %s : %d : %v", out, code, err) | ||
} | ||
s.T().Logf("exec command : %s : %d", out, code) | ||
} | ||
|
||
func TestParseFinMessage(t *testing.T) { | ||
testMsg := " daf" + finMessage + "123\n" | ||
if parseFinMessage(testMsg) == "" { | ||
t.Fatalf("wrong") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package rest | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/sp-yduck/proxmox-go/api" | ||
) | ||
|
||
func (s *TestSuite) TestCreateTermProxy() { | ||
testNode := s.GetTestNode() | ||
termProxy, err := s.restclient.CreateNodeTermProxy(context.TODO(), testNode.Node, api.TermProxyOption{}) | ||
if err != nil { | ||
s.T().Fatalf("failed to create termproxy: %v", err) | ||
} | ||
s.T().Logf("create termproxy: %v", termProxy) | ||
} | ||
|
||
func (s *TestSuite) TestGetVNCWebSocket() { | ||
testNode := s.GetTestNode() | ||
termProxy, err := s.restclient.CreateNodeTermProxy(context.TODO(), testNode.Node, api.TermProxyOption{}) | ||
if err != nil { | ||
s.T().Fatalf("failed to create termproxy: %v", err) | ||
} | ||
s.T().Logf("create termproxy: %v", termProxy) | ||
|
||
websocket, err := s.restclient.GetNodeVNCWebSocket(context.TODO(), testNode.Node, termProxy.Port, termProxy.Ticket) | ||
if err != nil { | ||
s.T().Fatalf("failed to get vncwebsocket: %v", err) | ||
} | ||
s.T().Logf("get vncwebsocket: %v", websocket) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package rest | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
"time" | ||
|
||
"github.com/gorilla/websocket" | ||
"github.com/pkg/errors" | ||
|
||
"github.com/sp-yduck/proxmox-go/api" | ||
) | ||
|
||
func (c *RESTClient) DialNodeVNCWebSocket(ctx context.Context, nodeName string, vnc api.TermProxy) (*websocket.Conn, error) { | ||
baseUrl := strings.Replace(c.endpoint, "https://", "wss://", 1) | ||
baseUrl = strings.Replace(baseUrl, "http://", "wss://", 1) | ||
websocketUrl := fmt.Sprintf("%s/nodes/%s/vncwebsocket?port=%s&vncticket=%s", baseUrl, nodeName, vnc.Port, url.QueryEscape(vnc.Ticket)) | ||
|
||
conn, resp, err := c.websocketDialer().DialContext(ctx, websocketUrl, c.makeAuthHeaders()) | ||
if err != nil { | ||
if resp != nil { | ||
return nil, errors.Errorf("failed to dial websocket: %v : %v", checkResponse(resp), err) | ||
} | ||
return nil, errors.Errorf("failed to dial websocket: %v", err) | ||
} | ||
|
||
if err := conn.WriteMessage(websocket.BinaryMessage, []byte(fmt.Sprintf("%s:%s\n", vnc.User, vnc.Ticket))); err != nil { | ||
return nil, errors.Errorf("failed to start session: %v", err) | ||
} | ||
|
||
return conn, nil | ||
} | ||
|
||
func (c *RESTClient) websocketDialer() *websocket.Dialer { | ||
var tlsConfig *tls.Config | ||
transport := c.httpClient.Transport.(*http.Transport) | ||
if transport != nil { | ||
tlsConfig = transport.TLSClientConfig | ||
} | ||
return &websocket.Dialer{ | ||
Proxy: http.ProxyFromEnvironment, | ||
HandshakeTimeout: 30 * time.Second, | ||
TLSClientConfig: tlsConfig, | ||
} | ||
} |
Oops, something went wrong.