1
1
package cli
2
2
3
3
import (
4
+ "bufio"
5
+ "bytes"
6
+ "context"
4
7
"fmt"
8
+ "io"
5
9
"os"
6
10
"os/exec"
11
+ "os/signal"
12
+ "path/filepath"
7
13
"strings"
8
14
9
15
"github.com/spf13/cobra"
@@ -13,16 +19,30 @@ import (
13
19
)
14
20
15
21
func gitssh () * cobra.Command {
16
- return & cobra.Command {
22
+ cmd := & cobra.Command {
17
23
Use : "gitssh" ,
18
24
Hidden : true ,
19
25
Short : `Wraps the "ssh" command and uses the coder gitssh key for authentication` ,
20
26
RunE : func (cmd * cobra.Command , args []string ) error {
27
+ ctx := cmd .Context ()
28
+ env := os .Environ ()
29
+
30
+ // Catch interrupt signals to ensure the temporary private
31
+ // key file is cleaned up on most cases.
32
+ ctx , stop := signal .NotifyContext (ctx , interruptSignals ... )
33
+ defer stop ()
34
+
35
+ // Early check so errors are reported immediately.
36
+ identityFiles , err := parseIdentityFilesForHost (ctx , args , env )
37
+ if err != nil {
38
+ return err
39
+ }
40
+
21
41
client , err := createAgentClient (cmd )
22
42
if err != nil {
23
43
return xerrors .Errorf ("create agent client: %w" , err )
24
44
}
25
- key , err := client .AgentGitSSHKey (cmd . Context () )
45
+ key , err := client .AgentGitSSHKey (ctx )
26
46
if err != nil {
27
47
return xerrors .Errorf ("get agent git ssh token: %w" , err )
28
48
}
@@ -44,8 +64,23 @@ func gitssh() *cobra.Command {
44
64
return xerrors .Errorf ("close temp gitsshkey file: %w" , err )
45
65
}
46
66
47
- args = append ([]string {"-i" , privateKeyFile .Name ()}, args ... )
48
- c := exec .CommandContext (cmd .Context (), "ssh" , args ... )
67
+ // Append our key, giving precedence to user keys. Note that
68
+ // OpenSSH server are typically configured with MaxAuthTries
69
+ // set to the default value of 6. This means that only the 6
70
+ // first keys can be tried. However, we will assume that if
71
+ // a user has configured 6+ keys for a host, they know what
72
+ // they're doing. This behavior is critical if a server has
73
+ // been configured with MaxAuthTries set to 1.
74
+ identityFiles = append (identityFiles , privateKeyFile .Name ())
75
+
76
+ var identityArgs []string
77
+ for _ , id := range identityFiles {
78
+ identityArgs = append (identityArgs , "-i" , id )
79
+ }
80
+
81
+ args = append (identityArgs , args ... )
82
+ c := exec .CommandContext (ctx , "ssh" , args ... )
83
+ c .Env = append (c .Env , env ... )
49
84
c .Stderr = cmd .ErrOrStderr ()
50
85
c .Stdout = cmd .OutOrStdout ()
51
86
c .Stdin = cmd .InOrStdin ()
@@ -69,4 +104,86 @@ func gitssh() *cobra.Command {
69
104
return nil
70
105
},
71
106
}
107
+
108
+ return cmd
109
+ }
110
+
111
+ // fallbackIdentityFiles is the list of identity files SSH tries when
112
+ // none have been defined for a host.
113
+ var fallbackIdentityFiles = strings .Join ([]string {
114
+ "identityfile ~/.ssh/id_rsa" ,
115
+ "identityfile ~/.ssh/id_dsa" ,
116
+ "identityfile ~/.ssh/id_ecdsa" ,
117
+ "identityfile ~/.ssh/id_ecdsa_sk" ,
118
+ "identityfile ~/.ssh/id_ed25519" ,
119
+ "identityfile ~/.ssh/id_ed25519_sk" ,
120
+ "identityfile ~/.ssh/id_xmss" ,
121
+ }, "\n " )
122
+
123
+ // parseIdentityFilesForHost uses ssh -G to discern what SSH keys have
124
+ // been enabled for the host (via the users SSH config) and returns a
125
+ // list of existing identity files.
126
+ //
127
+ // We do this because when no keys are defined for a host, SSH uses
128
+ // fallback keys (see above). However, by passing `-i` to attach our
129
+ // private key, we're effectively disabling the fallback keys.
130
+ //
131
+ // Example invocation:
132
+ //
133
+ // ssh -G -o SendEnv=GIT_PROTOCOL [email protected] git-upload-pack 'coder/coder'
134
+ //
135
+ // The extra arguments work without issue and lets us run the command
136
+ // as-is without stripping out the excess (git-upload-pack 'coder/coder').
137
+ func parseIdentityFilesForHost (ctx context.Context , args , env []string ) (identityFiles []string , error error ) {
138
+ home , err := os .UserHomeDir ()
139
+ if err != nil {
140
+ return nil , xerrors .Errorf ("get user home dir failed: %w" , err )
141
+ }
142
+
143
+ var outBuf bytes.Buffer
144
+ var r io.Reader = & outBuf
145
+
146
+ args = append ([]string {"-G" }, args ... )
147
+ cmd := exec .CommandContext (ctx , "ssh" , args ... )
148
+ cmd .Env = append (cmd .Env , env ... )
149
+ cmd .Stdout = & outBuf
150
+ cmd .Stderr = io .Discard
151
+ err = cmd .Run ()
152
+ if err != nil {
153
+ // If ssh -G failed, the SSH version is likely too old, fallback
154
+ // to using the default identity files.
155
+ r = strings .NewReader (fallbackIdentityFiles )
156
+ }
157
+
158
+ s := bufio .NewScanner (r )
159
+ for s .Scan () {
160
+ line := s .Text ()
161
+ if strings .HasPrefix (line , "identityfile " ) {
162
+ id := strings .TrimPrefix (line , "identityfile " )
163
+ if strings .HasPrefix (id , "~/" ) {
164
+ id = home + id [1 :]
165
+ }
166
+ // OpenSSH on Windows is weird, it supports using (and does
167
+ // use) mixed \ and / in paths.
168
+ //
169
+ // Example: C:\Users\ZeroCool/.ssh/known_hosts
170
+ //
171
+ // To check the file existence in Go, though, we want to use
172
+ // proper Windows paths.
173
+ // OpenSSH is amazing, this will work on Windows too:
174
+ // C:\Users\ZeroCool/.ssh/id_rsa
175
+ id = filepath .FromSlash (id )
176
+
177
+ // Only include the identity file if it exists.
178
+ if _ , err := os .Stat (id ); err == nil {
179
+ identityFiles = append (identityFiles , id )
180
+ }
181
+ }
182
+ }
183
+ if err := s .Err (); err != nil {
184
+ // This should never happen, the check is for completeness.
185
+ return nil , xerrors .Errorf ("scan ssh output: %w" , err )
186
+ }
187
+
188
+ return identityFiles , nil
72
189
}
0 commit comments