@@ -4,45 +4,54 @@ import (
44 "context"
55 "errors"
66 "fmt"
7- "io"
8- "math/rand"
7+ "net"
98 "os"
109 "os/exec"
1110 "strconv"
1211 "strings"
13- "time"
1412
1513 "github.com/github/go-liveshare"
1614)
1715
18- func MakeSSHTunnel (ctx context.Context , lsclient * liveshare.Client , localSSHPort int , remoteSSHPort int ) (int , <- chan error , error ) {
19- tunnelClosed := make (chan error )
16+ // UnusedPort returns the number of a local TCP port that is currently
17+ // unbound, or an error if none was available.
18+ //
19+ // Use of this function carries an inherent risk of a time-of-check to
20+ // time-of-use race against other processes.
21+ func UnusedPort () (int , error ) {
22+ addr , err := net .ResolveTCPAddr ("tcp" , "localhost:0" )
23+ if err != nil {
24+ return 0 , fmt .Errorf ("internal error while choosing port: %v" , err )
25+ }
2026
21- server , err := liveshare . NewServer ( lsclient )
27+ l , err := net . ListenTCP ( "tcp" , addr )
2228 if err != nil {
23- return 0 , nil , fmt .Errorf ("new Live Share server : %v" , err )
29+ return 0 , fmt .Errorf ("choosing available port : %v" , err )
2430 }
31+ defer l .Close ()
32+ return l .Addr ().(* net.TCPAddr ).Port , nil
33+ }
2534
26- rand .Seed (time .Now ().Unix ())
27- port := rand .Intn (9999 - 2000 ) + 2000 // improve this obviously
28- if localSSHPort != 0 {
29- port = localSSHPort
35+ // NewPortForwarder returns a new port forwarder for traffic between
36+ // the Live Share client and the specified local and remote ports.
37+ //
38+ // The session name is used (along with the port) to generate
39+ // names for streams, and may appear in error messages.
40+ func NewPortForwarder (ctx context.Context , client * liveshare.Client , sessionName string , localSSHPort , remoteSSHPort int ) (* liveshare.PortForwarder , error ) {
41+ if localSSHPort == 0 {
42+ return nil , fmt .Errorf ("a local port must be provided" )
3043 }
3144
32- if err := server .StartSharing (ctx , "sshd" , remoteSSHPort ); err != nil {
33- return 0 , nil , fmt .Errorf ("sharing sshd port: %v" , err )
45+ server , err := liveshare .NewServer (client )
46+ if err != nil {
47+ return nil , fmt .Errorf ("new liveshare server: %v" , err )
3448 }
3549
36- go func () {
37- portForwarder := liveshare .NewPortForwarder (lsclient , server , port )
38- if err := portForwarder .Start (ctx ); err != nil {
39- tunnelClosed <- fmt .Errorf ("forwarding port: %v" , err )
40- return
41- }
42- tunnelClosed <- nil
43- }()
50+ if err := server .StartSharing (ctx , "sshd" , remoteSSHPort ); err != nil {
51+ return nil , fmt .Errorf ("sharing sshd port: %v" , err )
52+ }
4453
45- return port , tunnelClosed , nil
54+ return liveshare . NewPortForwarder ( client , server , localSSHPort ) , nil
4655}
4756
4857// StartSSHServer installs (if necessary) and starts the SSH in the codespace.
@@ -72,72 +81,41 @@ func StartSSHServer(ctx context.Context, client *liveshare.Client, log logger) (
7281 return portInt , sshServerStartResult .User , nil
7382}
7483
75- func makeSSHArgs (port int , dst , cmd string ) ([]string , []string ) {
76- connArgs := []string {"-p" , strconv .Itoa (port ), "-o" , "NoHostAuthenticationForLocalhost=yes" }
77- cmdArgs := append ([]string {dst , "-X" , "-Y" , "-C" }, connArgs ... ) // X11, X11Trust, Compression
78-
79- if cmd != "" {
80- cmdArgs = append (cmdArgs , cmd )
81- }
82-
83- return cmdArgs , connArgs
84- }
85-
86- func ConnectToTunnel (ctx context.Context , log logger , port int , destination string , usingCustomPort bool ) <- chan error {
87- connClosed := make (chan error )
88- args , connArgs := makeSSHArgs (port , destination , "" )
84+ // Shell runs an interactive secure shell over an existing
85+ // port-forwarding session. It runs until the shell is terminated
86+ // (including by cancellation of the context).
87+ func Shell (ctx context.Context , log logger , port int , destination string , usingCustomPort bool ) error {
88+ cmd , connArgs := newSSHCommand (ctx , port , destination , "" )
8989
9090 if usingCustomPort {
9191 log .Println ("Connection Details: ssh " + destination + " " + strings .Join (connArgs , " " ))
9292 }
9393
94- cmd := exec .CommandContext (ctx , "ssh" , args ... )
95- cmd .Stdout = os .Stdout
96- cmd .Stdin = os .Stdin
97- cmd .Stderr = os .Stderr
98-
99- go func () {
100- connClosed <- cmd .Run ()
101- }()
102-
103- return connClosed
104- }
105-
106- type command struct {
107- Cmd * exec.Cmd
108- StdoutPipe io.ReadCloser
94+ return cmd .Run ()
10995}
11096
111- func newCommand (cmd * exec.Cmd ) (* command , error ) {
112- stdoutPipe , err := cmd .StdoutPipe ()
113- if err != nil {
114- return nil , fmt .Errorf ("create stdout pipe: %v" , err )
115- }
116-
117- if err := cmd .Start (); err != nil {
118- return nil , fmt .Errorf ("cmd start: %v" , err )
119- }
120-
121- return & command {
122- Cmd : cmd ,
123- StdoutPipe : stdoutPipe ,
124- }, nil
97+ // NewRemoteCommand returns an exec.Cmd that will securely run a shell
98+ // command on the remote machine.
99+ func NewRemoteCommand (ctx context.Context , tunnelPort int , destination , command string ) * exec.Cmd {
100+ cmd , _ := newSSHCommand (ctx , tunnelPort , destination , command )
101+ return cmd
125102}
126103
127- func (c * command ) Read (p []byte ) (int , error ) {
128- return c .StdoutPipe .Read (p )
129- }
104+ // newSSHCommand populates an exec.Cmd to run a command (or if blank,
105+ // an interactive shell) over ssh.
106+ func newSSHCommand (ctx context.Context , port int , dst , command string ) (* exec.Cmd , []string ) {
107+ connArgs := []string {"-p" , strconv .Itoa (port ), "-o" , "NoHostAuthenticationForLocalhost=yes" }
108+ // TODO(adonovan): eliminate X11 and X11Trust flags where unneeded.
109+ cmdArgs := append ([]string {dst , "-X" , "-Y" , "-C" }, connArgs ... ) // X11, X11Trust, Compression
130110
131- func (c * command ) Close () error {
132- if err := c .StdoutPipe .Close (); err != nil {
133- return fmt .Errorf ("close stdout: %v" , err )
111+ if command != "" {
112+ cmdArgs = append (cmdArgs , command )
134113 }
135114
136- return c .Cmd .Wait ()
137- }
115+ cmd := exec .CommandContext (ctx , "ssh" , cmdArgs ... )
116+ cmd .Stdout = os .Stdout
117+ cmd .Stdin = os .Stdin
118+ cmd .Stderr = os .Stderr
138119
139- func RunCommand (ctx context.Context , tunnelPort int , destination , cmdString string ) (io.ReadCloser , error ) {
140- args , _ := makeSSHArgs (tunnelPort , destination , cmdString )
141- cmd := exec .CommandContext (ctx , "ssh" , args ... )
142- return newCommand (cmd )
120+ return cmd , connArgs
143121}
0 commit comments