@@ -2,6 +2,7 @@ package codespaces
22
33import (
44 "context"
5+ "fmt"
56 "os"
67 "os/exec"
78 "strconv"
@@ -11,8 +12,11 @@ import (
1112// Shell runs an interactive secure shell over an existing
1213// port-forwarding session. It runs until the shell is terminated
1314// (including by cancellation of the context).
14- func Shell (ctx context.Context , log logger , port int , destination string , usingCustomPort bool ) error {
15- cmd , connArgs := newSSHCommand (ctx , port , destination , "" )
15+ func Shell (ctx context.Context , log logger , sshArgs []string , port int , destination string , usingCustomPort bool ) error {
16+ cmd , connArgs , err := newSSHCommand (ctx , port , destination , sshArgs )
17+ if err != nil {
18+ return fmt .Errorf ("failed to create ssh command: %w" , err )
19+ }
1620
1721 if usingCustomPort {
1822 log .Println ("Connection Details: ssh " + destination + " " + strings .Join (connArgs , " " ))
@@ -23,31 +27,64 @@ func Shell(ctx context.Context, log logger, port int, destination string, usingC
2327
2428// NewRemoteCommand returns an exec.Cmd that will securely run a shell
2529// command on the remote machine.
26- func NewRemoteCommand (ctx context.Context , tunnelPort int , destination , command string ) * exec.Cmd {
27- cmd , _ := newSSHCommand (ctx , tunnelPort , destination , command )
28- return cmd
30+ func NewRemoteCommand (ctx context.Context , tunnelPort int , destination string , sshArgs ... string ) ( * exec.Cmd , error ) {
31+ cmd , _ , err := newSSHCommand (ctx , tunnelPort , destination , sshArgs )
32+ return cmd , err
2933}
3034
3135// newSSHCommand populates an exec.Cmd to run a command (or if blank,
3236// an interactive shell) over ssh.
33- func newSSHCommand (ctx context.Context , port int , dst , command string ) (* exec.Cmd , []string ) {
37+ func newSSHCommand (ctx context.Context , port int , dst string , cmdArgs [] string ) (* exec.Cmd , []string , error ) {
3438 connArgs := []string {"-p" , strconv .Itoa (port ), "-o" , "NoHostAuthenticationForLocalhost=yes" }
3539
36- cmdArgs := []string {dst , "-C" } // Always use Compression
37- if command == "" {
38- // if we are in a shell send X11 and X11Trust
39- cmdArgs = append (cmdArgs , "-X" , "-Y" )
40+ // The ssh command syntax is: ssh [flags] user@host command [args...]
41+ // There is no way to specify the user@host destination as a flag.
42+ // Unfortunately, that means we need to know which user-provided words are
43+ // SSH flags and which are command arguments so that we can place
44+ // them before or after the destination, and that means we need to know all
45+ // the flags and their arities.
46+ cmdArgs , command , err := parseSSHArgs (cmdArgs )
47+ if err != nil {
48+ return nil , nil , err
4049 }
4150
4251 cmdArgs = append (cmdArgs , connArgs ... )
43- if command != "" {
44- cmdArgs = append (cmdArgs , command )
52+ cmdArgs = append (cmdArgs , "-C" ) // Compression
53+ cmdArgs = append (cmdArgs , dst ) // user@host
54+
55+ if command != nil {
56+ cmdArgs = append (cmdArgs , command ... )
4557 }
4658
4759 cmd := exec .CommandContext (ctx , "ssh" , cmdArgs ... )
4860 cmd .Stdout = os .Stdout
4961 cmd .Stdin = os .Stdin
5062 cmd .Stderr = os .Stderr
5163
52- return cmd , connArgs
64+ return cmd , connArgs , nil
65+ }
66+
67+ // parseSSHArgs parses SSH arguments into two distinct slices of flags and command.
68+ // It returns an error if a unary flag is provided without an argument.
69+ func parseSSHArgs (args []string ) (cmdArgs , command []string , err error ) {
70+ for i := 0 ; i < len (args ); i ++ {
71+ arg := args [i ]
72+
73+ // if we've started parsing the command, set it to the rest of the args
74+ if ! strings .HasPrefix (arg , "-" ) {
75+ command = args [i :]
76+ break
77+ }
78+
79+ cmdArgs = append (cmdArgs , arg )
80+ if len (arg ) == 2 && strings .Contains ("bcDeFIiLlmOopRSWw" , arg [1 :2 ]) {
81+ if i ++ ; i == len (args ) {
82+ return nil , nil , fmt .Errorf ("ssh flag: %s requires an argument" , arg )
83+ }
84+
85+ cmdArgs = append (cmdArgs , args [i ])
86+ }
87+ }
88+
89+ return cmdArgs , command , nil
5390}
0 commit comments