126 lines
3.5 KiB
Go
126 lines
3.5 KiB
Go
package liveshare
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// A Client capable of joining a Live Share workspace.
|
|
type Client struct {
|
|
connection Connection
|
|
tlsConfig *tls.Config
|
|
}
|
|
|
|
// A ClientOption is a function that modifies a client
|
|
type ClientOption func(*Client) error
|
|
|
|
// NewClient accepts a range of options, applies them and returns a client
|
|
func NewClient(opts ...ClientOption) (*Client, error) {
|
|
client := new(Client)
|
|
|
|
for _, o := range opts {
|
|
if err := o(client); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
// WithConnection is a ClientOption that accepts a Connection
|
|
func WithConnection(connection Connection) ClientOption {
|
|
return func(c *Client) error {
|
|
if err := connection.validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.connection = connection
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithTLSConfig(tlsConfig *tls.Config) ClientOption {
|
|
return func(c *Client) error {
|
|
c.tlsConfig = tlsConfig
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// JoinWorkspace connects the client to the server's Live Share
|
|
// workspace and returns a session representing their connection.
|
|
func (c *Client) JoinWorkspace(ctx context.Context) (*Session, error) {
|
|
clientSocket := newSocket(c.connection, c.tlsConfig)
|
|
if err := clientSocket.connect(ctx); err != nil {
|
|
return nil, fmt.Errorf("error connecting websocket: %v", err)
|
|
}
|
|
|
|
ssh := newSSHSession(c.connection.SessionToken, clientSocket)
|
|
if err := ssh.connect(ctx); err != nil {
|
|
return nil, fmt.Errorf("error connecting to ssh session: %v", err)
|
|
}
|
|
|
|
rpc := newRPCClient(ssh)
|
|
rpc.connect(ctx)
|
|
if _, err := c.joinWorkspace(ctx, rpc); err != nil {
|
|
return nil, fmt.Errorf("error joining Live Share workspace: %v", err)
|
|
}
|
|
|
|
return &Session{ssh: ssh, rpc: rpc}, nil
|
|
}
|
|
|
|
type clientCapabilities struct {
|
|
IsNonInteractive bool `json:"isNonInteractive"`
|
|
}
|
|
|
|
type joinWorkspaceArgs struct {
|
|
ID string `json:"id"`
|
|
ConnectionMode string `json:"connectionMode"`
|
|
JoiningUserSessionToken string `json:"joiningUserSessionToken"`
|
|
ClientCapabilities clientCapabilities `json:"clientCapabilities"`
|
|
}
|
|
|
|
type joinWorkspaceResult struct {
|
|
SessionNumber int `json:"sessionNumber"`
|
|
}
|
|
|
|
func (c *Client) joinWorkspace(ctx context.Context, rpc *rpcClient) (*joinWorkspaceResult, error) {
|
|
args := joinWorkspaceArgs{
|
|
ID: c.connection.SessionID,
|
|
ConnectionMode: "local",
|
|
JoiningUserSessionToken: c.connection.SessionToken,
|
|
ClientCapabilities: clientCapabilities{
|
|
IsNonInteractive: false,
|
|
},
|
|
}
|
|
|
|
var result joinWorkspaceResult
|
|
if err := rpc.do(ctx, "workspace.joinWorkspace", &args, &result); err != nil {
|
|
return nil, fmt.Errorf("error making workspace.joinWorkspace call: %v", err)
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *Session) openStreamingChannel(ctx context.Context, streamName, condition string) (ssh.Channel, error) {
|
|
args := getStreamArgs{streamName, condition}
|
|
var streamID string
|
|
if err := s.rpc.do(ctx, "streamManager.getStream", args, &streamID); err != nil {
|
|
return nil, fmt.Errorf("error getting stream id: %v", err)
|
|
}
|
|
|
|
channel, reqs, err := s.ssh.conn.OpenChannel("session", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error opening ssh channel for transport: %v", err)
|
|
}
|
|
go ssh.DiscardRequests(reqs)
|
|
|
|
requestType := fmt.Sprintf("stream-transport-%s", streamID)
|
|
if _, err = channel.SendRequest(requestType, true, nil); err != nil {
|
|
return nil, fmt.Errorf("error sending channel request: %v", err)
|
|
}
|
|
|
|
return channel, nil
|
|
}
|