Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 3536869

Browse files
kylecarbsjawnsy
andauthored
feat: Display error to user when SSHing into offline workspace (#411)
* feat: Display error to user when SSH'ing into offline workspace * Remove new \r * Update internal/cmd/tunnel.go Co-authored-by: Jonathan Yu <jonathan@coder.com> Co-authored-by: Jonathan Yu <jonathan@coder.com>
1 parent 3af8385 commit 3536869

File tree

3 files changed

+125
-30
lines changed

3 files changed

+125
-30
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ require (
2525
github.com/rjeczalik/notify v0.9.2
2626
github.com/spf13/cobra v1.2.1
2727
github.com/stretchr/testify v1.7.0
28+
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
2829
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
2930
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
30-
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015
31+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
3132
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
3233
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
3334
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,9 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
430430
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
431431
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
432432
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
433-
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
434433
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
434+
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
435+
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
435436
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
436437
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
437438
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -590,8 +591,9 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
590591
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
591592
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
592593
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
593-
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
594594
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
595+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
596+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
595597
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
596598
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
597599
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

internal/cmd/tunnel.go

Lines changed: 119 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import (
1212

1313
"cdr.dev/slog"
1414
"cdr.dev/slog/sloggers/sloghuman"
15+
"github.com/fatih/color"
1516
"github.com/pion/webrtc/v3"
1617
"github.com/spf13/cobra"
18+
"golang.org/x/crypto/ssh"
1719
"golang.org/x/xerrors"
1820

1921
"cdr.dev/coder-cli/coder-sdk"
2022
"cdr.dev/coder-cli/internal/x/xcobra"
23+
"cdr.dev/coder-cli/pkg/clog"
2124
"cdr.dev/coder-cli/wsnet"
2225
)
2326

@@ -59,20 +62,34 @@ coder tunnel my-dev 3000 3000
5962
}
6063
baseURL := sdk.BaseURL()
6164

62-
workspaces, err := getWorkspaces(ctx, sdk, coder.Me)
65+
workspace, err := findWorkspace(ctx, sdk, args[0], coder.Me)
6366
if err != nil {
6467
return xerrors.Errorf("get workspaces: %w", err)
6568
}
6669

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
7290
}
73-
}
74-
if workspaceID == "" {
75-
return xerrors.Errorf("No workspace found by name '%s'", args[0])
91+
92+
return notAvailableError
7693
}
7794

7895
iceServers, err := sdk.ICEServers(ctx)
@@ -82,14 +99,14 @@ coder tunnel my-dev 3000 3000
8299
log.Debug(ctx, "got ICE servers", slog.F("ice", iceServers))
83100

84101
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),
93110
}
94111

95112
err = c.start(ctx)
@@ -105,14 +122,14 @@ coder tunnel my-dev 3000 3000
105122
}
106123

107124
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
116133
}
117134

118135
func (c *tunnneler) start(ctx context.Context) error {
@@ -121,7 +138,7 @@ func (c *tunnneler) start(ctx context.Context) error {
121138
dialLog := c.log.Named("wsnet")
122139
wd, err := wsnet.DialWebsocket(
123140
ctx,
124-
wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token),
141+
wsnet.ConnectEndpoint(c.brokerAddr, c.workspace.ID, c.token),
125142
&wsnet.DialOptions{
126143
Log: &dialLog,
127144
TURNProxyAuthToken: c.token,
@@ -156,7 +173,7 @@ func (c *tunnneler) start(ctx context.Context) error {
156173
return
157174
case <-ticker.C:
158175
// silently ignore failures so we don't spam the console
159-
_ = sdk.UpdateLastConnectionAt(ctx, c.workspaceID)
176+
_ = sdk.UpdateLastConnectionAt(ctx, c.workspace.ID)
160177
}
161178
}
162179
}()
@@ -203,3 +220,78 @@ func (c *tunnneler) start(ctx context.Context) error {
203220
}()
204221
}
205222
}
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+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy