@@ -12,12 +12,15 @@ import (
12
12
13
13
"cdr.dev/slog"
14
14
"cdr.dev/slog/sloggers/sloghuman"
15
+ "github.com/fatih/color"
15
16
"github.com/pion/webrtc/v3"
16
17
"github.com/spf13/cobra"
18
+ "golang.org/x/crypto/ssh"
17
19
"golang.org/x/xerrors"
18
20
19
21
"cdr.dev/coder-cli/coder-sdk"
20
22
"cdr.dev/coder-cli/internal/x/xcobra"
23
+ "cdr.dev/coder-cli/pkg/clog"
21
24
"cdr.dev/coder-cli/wsnet"
22
25
)
23
26
@@ -59,20 +62,34 @@ coder tunnel my-dev 3000 3000
59
62
}
60
63
baseURL := sdk .BaseURL ()
61
64
62
- workspaces ,err := getWorkspaces (ctx ,sdk ,coder .Me )
65
+ workspace ,err := findWorkspace (ctx ,sdk , args [ 0 ] ,coder .Me )
63
66
if err != nil {
64
67
return xerrors .Errorf ("get workspaces: %w" ,err )
65
68
}
66
69
67
- var workspaceID string
68
- for _ ,workspace := range workspaces {
69
- if workspace .Name == args [0 ] {
70
- workspaceID = workspace .ID
71
- break
70
+ if workspace .LatestStat .ContainerStatus != coder .WorkspaceOn {
71
+ color .NoColor = false
72
+ notAvailableError := clog .Error ("workspace not available" ,
73
+ fmt .Sprintf ("current status: %q" ,workspace .LatestStat .ContainerStatus ),
74
+ clog .BlankLine ,
75
+ clog .Tipf ("use\" coder workspaces rebuild %s\" to rebuild this workspace" ,workspace .Name ),
76
+ )
77
+ // If we're attempting to forward our remote SSH port,
78
+ // we want to communicate with the OpenSSH protocol so
79
+ // SSH clients can properly display output to our users.
80
+ if remotePort == 12213 {
81
+ rawKey ,err := sdk .SSHKey (ctx )
82
+ if err != nil {
83
+ return xerrors .Errorf ("get ssh key: %w" ,err )
84
+ }
85
+ err = discardSSHConnection (& stdioConn {},rawKey .PrivateKey ,notAvailableError .String ())
86
+ if err != nil {
87
+ return err
88
+ }
89
+ return nil
72
90
}
73
- }
74
- if workspaceID == "" {
75
- return xerrors .Errorf ("No workspace found by name '%s'" ,args [0 ])
91
+
92
+ return notAvailableError
76
93
}
77
94
78
95
iceServers ,err := sdk .ICEServers (ctx )
@@ -82,14 +99,14 @@ coder tunnel my-dev 3000 3000
82
99
log .Debug (ctx ,"got ICE servers" ,slog .F ("ice" ,iceServers ))
83
100
84
101
c := & tunnneler {
85
- log :log ,
86
- brokerAddr :& baseURL ,
87
- token :sdk .Token (),
88
- workspaceID : workspaceID ,
89
- iceServers :iceServers ,
90
- stdio :args [2 ]== "stdio" ,
91
- localPort :uint16 (localPort ),
92
- remotePort :uint16 (remotePort ),
102
+ log :log ,
103
+ brokerAddr :& baseURL ,
104
+ token :sdk .Token (),
105
+ workspace : workspace ,
106
+ iceServers :iceServers ,
107
+ stdio :args [2 ]== "stdio" ,
108
+ localPort :uint16 (localPort ),
109
+ remotePort :uint16 (remotePort ),
93
110
}
94
111
95
112
err = c .start (ctx )
@@ -105,14 +122,14 @@ coder tunnel my-dev 3000 3000
105
122
}
106
123
107
124
type tunnneler struct {
108
- log slog.Logger
109
- brokerAddr * url.URL
110
- token string
111
- workspaceID string
112
- iceServers []webrtc.ICEServer
113
- remotePort uint16
114
- localPort uint16
115
- stdio bool
125
+ log slog.Logger
126
+ brokerAddr * url.URL
127
+ token string
128
+ workspace * coder. Workspace
129
+ iceServers []webrtc.ICEServer
130
+ remotePort uint16
131
+ localPort uint16
132
+ stdio bool
116
133
}
117
134
118
135
func (c * tunnneler )start (ctx context.Context )error {
@@ -121,7 +138,7 @@ func (c *tunnneler) start(ctx context.Context) error {
121
138
dialLog := c .log .Named ("wsnet" )
122
139
wd ,err := wsnet .DialWebsocket (
123
140
ctx ,
124
- wsnet .ConnectEndpoint (c .brokerAddr ,c .workspaceID ,c .token ),
141
+ wsnet .ConnectEndpoint (c .brokerAddr ,c .workspace . ID ,c .token ),
125
142
& wsnet.DialOptions {
126
143
Log :& dialLog ,
127
144
TURNProxyAuthToken :c .token ,
@@ -156,7 +173,7 @@ func (c *tunnneler) start(ctx context.Context) error {
156
173
return
157
174
case <- ticker .C :
158
175
// silently ignore failures so we don't spam the console
159
- _ = sdk .UpdateLastConnectionAt (ctx ,c .workspaceID )
176
+ _ = sdk .UpdateLastConnectionAt (ctx ,c .workspace . ID )
160
177
}
161
178
}
162
179
}()
@@ -203,3 +220,78 @@ func (c *tunnneler) start(ctx context.Context) error {
203
220
}()
204
221
}
205
222
}
223
+
224
+ // Used to treat stdio like a connection for proxying SSH.
225
+ type stdioConn struct {}
226
+
227
+ func (s * stdioConn )Read (b []byte ) (n int ,err error ) {
228
+ return os .Stdin .Read (b )
229
+ }
230
+
231
+ func (s * stdioConn )Write (b []byte ) (n int ,err error ) {
232
+ return os .Stdout .Write (b )
233
+ }
234
+
235
+ func (s * stdioConn )Close ()error {
236
+ return nil
237
+ }
238
+
239
+ func (s * stdioConn )LocalAddr () net.Addr {
240
+ return nil
241
+ }
242
+
243
+ func (s * stdioConn )RemoteAddr () net.Addr {
244
+ return nil
245
+ }
246
+
247
+ func (s * stdioConn )SetDeadline (t time.Time )error {
248
+ return nil
249
+ }
250
+
251
+ func (s * stdioConn )SetReadDeadline (t time.Time )error {
252
+ return nil
253
+ }
254
+
255
+ func (s * stdioConn )SetWriteDeadline (t time.Time )error {
256
+ return nil
257
+ }
258
+
259
+ // discardSSHConnection accepts a connection then outputs the message provided
260
+ // to any channel opened, immediately closing the connection afterwards.
261
+ //
262
+ // Used to provide status to connecting clients while still aligning with the
263
+ // native SSH protocol.
264
+ func discardSSHConnection (nc net.Conn ,privateKey string ,msg string )error {
265
+ config := & ssh.ServerConfig {
266
+ NoClientAuth :true ,
267
+ }
268
+ key ,err := ssh .ParseRawPrivateKey ([]byte (privateKey ))
269
+ if err != nil {
270
+ return fmt .Errorf ("parse private key: %w" ,err )
271
+ }
272
+ signer ,err := ssh .NewSignerFromKey (key )
273
+ if err != nil {
274
+ return fmt .Errorf ("signer from private key: %w" ,err )
275
+ }
276
+ config .AddHostKey (signer )
277
+ conn ,chans ,reqs ,err := ssh .NewServerConn (nc ,config )
278
+ if err != nil {
279
+ return fmt .Errorf ("create server conn: %w" ,err )
280
+ }
281
+ go ssh .DiscardRequests (reqs )
282
+ ch ,req ,err := (<- chans ).Accept ()
283
+ if err != nil {
284
+ return fmt .Errorf ("accept channel: %w" ,err )
285
+ }
286
+ go ssh .DiscardRequests (req )
287
+
288
+ _ ,err = ch .Write ([]byte (msg ))
289
+ if err != nil {
290
+ return fmt .Errorf ("write channel: %w" ,err )
291
+ }
292
+ err = ch .Close ()
293
+ if err != nil {
294
+ return fmt .Errorf ("close channel: %w" ,err )
295
+ }
296
+ return conn .Close ()
297
+ }