@@ -2,8 +2,13 @@ package cli_test
2
2
3
3
import (
4
4
"context"
5
+ "crypto/ecdsa"
6
+ "crypto/elliptic"
7
+ "crypto/rand"
8
+ "errors"
5
9
"io"
6
10
"net"
11
+ "path/filepath"
7
12
"runtime"
8
13
"testing"
9
14
"time"
@@ -12,6 +17,7 @@ import (
12
17
"github.com/stretchr/testify/assert"
13
18
"github.com/stretchr/testify/require"
14
19
"golang.org/x/crypto/ssh"
20
+ gosshagent "golang.org/x/crypto/ssh/agent"
15
21
16
22
"cdr.dev/slog"
17
23
"cdr.dev/slog/sloggers/slogtest"
@@ -135,6 +141,84 @@ func TestSSH(t *testing.T) {
135
141
require .NoError (t , err )
136
142
_ = clientOutput .Close ()
137
143
})
144
+ //nolint:paralleltest // Disabled due to use of t.Setenv.
145
+ t .Run ("ForwardAgent" , func (t * testing.T ) {
146
+ if runtime .GOOS == "windows" {
147
+ t .Skip ("Test not supported on windows" )
148
+ }
149
+
150
+ client , workspace , agentToken := setupWorkspaceForSSH (t )
151
+
152
+ tGoContext (t , func (ctx context.Context ) {
153
+ // Run this async so the SSH command has to wait for
154
+ // the build and agent to connect!
155
+ coderdtest .AwaitWorkspaceBuildJob (t , client , workspace .LatestBuild .ID )
156
+ agentClient := codersdk .New (client .URL )
157
+ agentClient .SessionToken = agentToken
158
+ agentCloser := agent .New (agentClient .ListenWorkspaceAgent , & agent.Options {
159
+ Logger : slogtest .Make (t , nil ).Leveled (slog .LevelDebug ),
160
+ })
161
+ <- ctx .Done ()
162
+ _ = agentCloser .Close ()
163
+ })
164
+
165
+ // Generate private key.
166
+ privateKey , err := ecdsa .GenerateKey (elliptic .P256 (), rand .Reader )
167
+ require .NoError (t , err )
168
+ kr := gosshagent .NewKeyring ()
169
+ kr .Add (gosshagent.AddedKey {
170
+ PrivateKey : privateKey ,
171
+ })
172
+
173
+ // Start up ssh agent listening on unix socket.
174
+ tmpdir := t .TempDir ()
175
+ agentSock := filepath .Join (tmpdir , "agent.sock" )
176
+ l , err := net .Listen ("unix" , agentSock )
177
+ require .NoError (t , err )
178
+ defer l .Close ()
179
+ tGo (t , func () {
180
+ for {
181
+ fd , err := l .Accept ()
182
+ if err != nil {
183
+ t .Logf ("accept error: %v" , err )
184
+ return
185
+ }
186
+
187
+ err = gosshagent .ServeAgent (kr , fd )
188
+ if ! errors .Is (err , io .EOF ) {
189
+ assert .NoError (t , err )
190
+ }
191
+ }
192
+ })
193
+
194
+ t .Setenv ("SSH_AUTH_SOCK" , agentSock )
195
+ cmd , root := clitest .New (t ,
196
+ "ssh" ,
197
+ workspace .Name ,
198
+ "--forward-agent" ,
199
+ )
200
+ clitest .SetupConfig (t , client , root )
201
+ pty := ptytest .New (t )
202
+ cmd .SetIn (pty .Input ())
203
+ cmd .SetOut (pty .Output ())
204
+ cmd .SetErr (io .Discard )
205
+ tGo (t , func () {
206
+ err := cmd .Execute ()
207
+ assert .NoError (t , err )
208
+ })
209
+
210
+ // Ensure that SSH_AUTH_SOCK is set.
211
+ pty .WriteLine ("env" )
212
+ pty .ExpectMatch ("SSH_AUTH_SOCK=/tmp" ) // E.g. /tmp/auth-agent3167016167/listener.sock
213
+ // Ensure that ssh-add lists our key.
214
+ pty .WriteLine ("ssh-add -L" )
215
+ keys , err := kr .List ()
216
+ require .NoError (t , err )
217
+ pty .ExpectMatch (keys [0 ].String ())
218
+
219
+ // kthxbye
220
+ pty .WriteLine ("exit" )
221
+ })
138
222
}
139
223
140
224
// tGoContext runs fn in a goroutine passing a context that will be
0 commit comments