cli/internal/codespaces/ssh.go

95 lines
3 KiB
Go

package codespaces
import (
"context"
"fmt"
"net"
"os"
"os/exec"
"strconv"
"strings"
"github.com/github/go-liveshare"
)
// UnusedPort returns the number of a local TCP port that is currently
// unbound, or an error if none was available.
//
// Use of this function carries an inherent risk of a time-of-check to
// time-of-use race against other processes.
func UnusedPort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, fmt.Errorf("internal error while choosing port: %v", err)
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, fmt.Errorf("choosing available port: %v", err)
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port, nil
}
// NewPortForwarder returns a new port forwarder for traffic between
// the Live Share client and the specified local port (which must be
// available).
//
// The session name is used (along with the port) to generate
// names for streams, and may appear in error messages.
func NewPortForwarder(ctx context.Context, client *liveshare.Client, sessionName string, localPort int) (*liveshare.PortForwarder, error) {
if localPort == 0 {
return nil, fmt.Errorf("a local port must be provided")
}
server, err := liveshare.NewServer(client)
if err != nil {
return nil, fmt.Errorf("new liveshare server: %v", err)
}
// TODO(josebalius): This port won't always be 2222
if err := server.StartSharing(ctx, sessionName, 2222); err != nil {
return nil, fmt.Errorf("sharing sshd port: %v", err)
}
return liveshare.NewPortForwarder(client, server, localPort), nil
}
// Shell runs an interactive secure shell over an existing
// port-forwarding session. It runs until the shell is terminated
// (including by cancellation of the context).
func Shell(ctx context.Context, log logger, port int, destination string, usingCustomPort bool) error {
cmd, connArgs := newSSHCommand(ctx, port, destination, "")
if usingCustomPort {
log.Println("Connection Details: ssh " + destination + " " + strings.Join(connArgs, " "))
}
return cmd.Run()
}
// NewRemoteCommand returns an exec.Cmd that will securely run a shell
// command on the remote machine.
func NewRemoteCommand(ctx context.Context, tunnelPort int, destination, command string) *exec.Cmd {
cmd, _ := newSSHCommand(ctx, tunnelPort, destination, command)
return cmd
}
// newSSHCommand populates an exec.Cmd to run a command (or if blank,
// an interactive shell) over ssh.
func newSSHCommand(ctx context.Context, port int, dst, command string) (*exec.Cmd, []string) {
connArgs := []string{"-p", strconv.Itoa(port), "-o", "NoHostAuthenticationForLocalhost=yes"}
// TODO(adonovan): eliminate X11 and X11Trust flags where unneeded.
cmdArgs := append([]string{dst, "-X", "-Y", "-C"}, connArgs...) // X11, X11Trust, Compression
if command != "" {
cmdArgs = append(cmdArgs, command)
}
cmd := exec.CommandContext(ctx, "ssh", cmdArgs...)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
return cmd, connArgs
}