Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ac0d6e0

Browse files
authored
Merge branch 'main' into bryphe/refactor/project-with-params-example
2 parents f10ff6c + 3b57619 commit ac0d6e0

30 files changed

+684
-93
lines changed

.github/workflows/coder.yaml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ jobs:
6060
key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
6161

6262
- name: Install node_modules
63-
run: yarn install
64-
working-directory: site
63+
run: ./scripts/yarn_install.sh
6564

6665
- name: "yarn lint"
6766
run: yarn lint
@@ -108,8 +107,7 @@ jobs:
108107
key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
109108

110109
- name: Install node_modules
111-
run: yarn install
112-
working-directory: site
110+
run: ./scripts/yarn_install.sh
113111

114112
- name: "make fmt"
115113
run: "make --output-sync -j fmt"
@@ -214,8 +212,8 @@ jobs:
214212
with:
215213
node-version: "14"
216214

217-
- run: yarn install
218-
working-directory: site
215+
- name: Install node_modules
216+
run: ./scripts/yarn_install.sh
219217

220218
- uses: actions/setup-go@v2
221219
with:
@@ -252,13 +250,15 @@ jobs:
252250
with:
253251
node-version: "14"
254252

255-
- run: yarn install
256-
working-directory: site
253+
- name: Install node_modules
254+
run: ./scripts/yarn_install.sh
257255

258-
- run: yarn build
256+
- name: Build frontend
257+
run: yarn build
259258
working-directory: site
260259

261-
- run: yarn storybook:build
260+
- name: Build Storybook
261+
run: yarn storybook:build
262262
working-directory: site
263263

264264
- run: yarn test:coverage

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ provisionersdk/proto: provisionersdk/proto/provisioner.proto
8787
.PHONY: provisionersdk/proto
8888

8989
site/out:
90-
cd site && yarn install
90+
./scripts/yarn_install.sh
9191
cd site && yarn build
9292
cd site && yarn export
93-
.PHONY: site/out
93+
.PHONY: site/out

agent/agent.go

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
package agent
2+
3+
import (
4+
"context"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"errors"
8+
"fmt"
9+
"io"
10+
"net"
11+
"os/exec"
12+
"os/user"
13+
"sync"
14+
"time"
15+
16+
"cdr.dev/slog"
17+
"github.com/coder/coder/agent/usershell"
18+
"github.com/coder/coder/peer"
19+
"github.com/coder/coder/peerbroker"
20+
"github.com/coder/coder/pty"
21+
"github.com/coder/retry"
22+
23+
"github.com/gliderlabs/ssh"
24+
gossh "golang.org/x/crypto/ssh"
25+
"golang.org/x/xerrors"
26+
)
27+
28+
func DialSSH(conn *peer.Conn) (net.Conn, error) {
29+
channel, err := conn.Dial(context.Background(), "ssh", &peer.ChannelOptions{
30+
Protocol: "ssh",
31+
})
32+
if err != nil {
33+
return nil, err
34+
}
35+
return channel.NetConn(), nil
36+
}
37+
38+
func DialSSHClient(conn *peer.Conn) (*gossh.Client, error) {
39+
netConn, err := DialSSH(conn)
40+
if err != nil {
41+
return nil, err
42+
}
43+
sshConn, channels, requests, err := gossh.NewClientConn(netConn, "localhost:22", &gossh.ClientConfig{
44+
Config: gossh.Config{
45+
Ciphers: []string{"arcfour"},
46+
},
47+
// SSH host validation isn't helpful, because obtaining a peer
48+
// connection already signifies user-intent to dial a workspace.
49+
// #nosec
50+
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
51+
})
52+
if err != nil {
53+
return nil, err
54+
}
55+
return gossh.NewClient(sshConn, channels, requests), nil
56+
}
57+
58+
type Options struct {
59+
Logger slog.Logger
60+
}
61+
62+
type Dialer func(ctx context.Context) (*peerbroker.Listener, error)
63+
64+
func New(dialer Dialer, options *Options) io.Closer {
65+
ctx, cancelFunc := context.WithCancel(context.Background())
66+
server := &server{
67+
clientDialer: dialer,
68+
options: options,
69+
closeCancel: cancelFunc,
70+
closed: make(chan struct{}),
71+
}
72+
server.init(ctx)
73+
return server
74+
}
75+
76+
type server struct {
77+
clientDialer Dialer
78+
options *Options
79+
80+
closeCancel context.CancelFunc
81+
closeMutex sync.Mutex
82+
closed chan struct{}
83+
84+
sshServer *ssh.Server
85+
}
86+
87+
func (s *server) init(ctx context.Context) {
88+
// Clients' should ignore the host key when connecting.
89+
// The agent needs to authenticate with coderd to SSH,
90+
// so SSH authentication doesn't improve security.
91+
randomHostKey, err := rsa.GenerateKey(rand.Reader, 2048)
92+
if err != nil {
93+
panic(err)
94+
}
95+
randomSigner, err := gossh.NewSignerFromKey(randomHostKey)
96+
if err != nil {
97+
panic(err)
98+
}
99+
sshLogger := s.options.Logger.Named("ssh-server")
100+
forwardHandler := &ssh.ForwardedTCPHandler{}
101+
s.sshServer = &ssh.Server{
102+
ChannelHandlers: ssh.DefaultChannelHandlers,
103+
ConnectionFailedCallback: func(conn net.Conn, err error) {
104+
sshLogger.Info(ctx, "ssh connection ended", slog.Error(err))
105+
},
106+
Handler: func(session ssh.Session) {
107+
err := s.handleSSHSession(session)
108+
if err != nil {
109+
s.options.Logger.Debug(ctx, "ssh session failed", slog.Error(err))
110+
_ = session.Exit(1)
111+
return
112+
}
113+
},
114+
HostSigners: []ssh.Signer{randomSigner},
115+
LocalPortForwardingCallback: func(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
116+
// Allow local port forwarding all!
117+
sshLogger.Debug(ctx, "local port forward",
118+
slog.F("destination-host", destinationHost),
119+
slog.F("destination-port", destinationPort))
120+
return true
121+
},
122+
PtyCallback: func(ctx ssh.Context, pty ssh.Pty) bool {
123+
return true
124+
},
125+
ReversePortForwardingCallback: func(ctx ssh.Context, bindHost string, bindPort uint32) bool {
126+
// Allow reverse port forwarding all!
127+
sshLogger.Debug(ctx, "local port forward",
128+
slog.F("bind-host", bindHost),
129+
slog.F("bind-port", bindPort))
130+
return true
131+
},
132+
RequestHandlers: map[string]ssh.RequestHandler{
133+
"tcpip-forward": forwardHandler.HandleSSHRequest,
134+
"cancel-tcpip-forward": forwardHandler.HandleSSHRequest,
135+
},
136+
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
137+
return &gossh.ServerConfig{
138+
Config: gossh.Config{
139+
// "arcfour" is the fastest SSH cipher. We prioritize throughput
140+
// over encryption here, because the WebRTC connection is already
141+
// encrypted. If possible, we'd disable encryption entirely here.
142+
Ciphers: []string{"arcfour"},
143+
},
144+
NoClientAuth: true,
145+
}
146+
},
147+
}
148+
149+
go s.run(ctx)
150+
}
151+
152+
func (*server) handleSSHSession(session ssh.Session) error {
153+
var (
154+
command string
155+
args = []string{}
156+
err error
157+
)
158+
159+
username := session.User()
160+
if username == "" {
161+
currentUser, err := user.Current()
162+
if err != nil {
163+
return xerrors.Errorf("get current user: %w", err)
164+
}
165+
username = currentUser.Username
166+
}
167+
168+
// gliderlabs/ssh returns a command slice of zero
169+
// when a shell is requested.
170+
if len(session.Command()) == 0 {
171+
command, err = usershell.Get(username)
172+
if err != nil {
173+
return xerrors.Errorf("get user shell: %w", err)
174+
}
175+
} else {
176+
command = session.Command()[0]
177+
if len(session.Command()) > 1 {
178+
args = session.Command()[1:]
179+
}
180+
}
181+
182+
signals := make(chan ssh.Signal)
183+
breaks := make(chan bool)
184+
defer close(signals)
185+
defer close(breaks)
186+
go func() {
187+
for {
188+
select {
189+
case <-session.Context().Done():
190+
return
191+
// Ignore signals and breaks for now!
192+
case <-signals:
193+
case <-breaks:
194+
}
195+
}
196+
}()
197+
198+
cmd := exec.CommandContext(session.Context(), command, args...)
199+
cmd.Env = session.Environ()
200+
201+
sshPty, windowSize, isPty := session.Pty()
202+
if isPty {
203+
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", sshPty.Term))
204+
ptty, process, err := pty.Start(cmd)
205+
if err != nil {
206+
return xerrors.Errorf("start command: %w", err)
207+
}
208+
go func() {
209+
for win := range windowSize {
210+
err := ptty.Resize(uint16(win.Width), uint16(win.Height))
211+
if err != nil {
212+
panic(err)
213+
}
214+
}
215+
}()
216+
go func() {
217+
_, _ = io.Copy(ptty.Input(), session)
218+
}()
219+
go func() {
220+
_, _ = io.Copy(session, ptty.Output())
221+
}()
222+
_, _ = process.Wait()
223+
_ = ptty.Close()
224+
return nil
225+
}
226+
227+
cmd.Stdout = session
228+
cmd.Stderr = session
229+
// This blocks forever until stdin is received if we don't
230+
// use StdinPipe. It's unknown what causes this.
231+
stdinPipe, err := cmd.StdinPipe()
232+
if err != nil {
233+
return xerrors.Errorf("create stdin pipe: %w", err)
234+
}
235+
go func() {
236+
_, _ = io.Copy(stdinPipe, session)
237+
}()
238+
err = cmd.Start()
239+
if err != nil {
240+
return xerrors.Errorf("start: %w", err)
241+
}
242+
_ = cmd.Wait()
243+
return nil
244+
}
245+
246+
func (s *server) run(ctx context.Context) {
247+
var peerListener *peerbroker.Listener
248+
var err error
249+
// An exponential back-off occurs when the connection is failing to dial.
250+
// This is to prevent server spam in case of a coderd outage.
251+
for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(ctx); {
252+
peerListener, err = s.clientDialer(ctx)
253+
if err != nil {
254+
if errors.Is(err, context.Canceled) {
255+
return
256+
}
257+
if s.isClosed() {
258+
return
259+
}
260+
s.options.Logger.Warn(context.Background(), "failed to dial", slog.Error(err))
261+
continue
262+
}
263+
s.options.Logger.Debug(context.Background(), "connected")
264+
break
265+
}
266+
select {
267+
case <-ctx.Done():
268+
return
269+
default:
270+
}
271+
272+
for {
273+
conn, err := peerListener.Accept()
274+
if err != nil {
275+
if s.isClosed() {
276+
return
277+
}
278+
s.options.Logger.Debug(ctx, "peer listener accept exited; restarting connection", slog.Error(err))
279+
s.run(ctx)
280+
return
281+
}
282+
go s.handlePeerConn(ctx, conn)
283+
}
284+
}
285+
286+
func (s *server) handlePeerConn(ctx context.Context, conn *peer.Conn) {
287+
for {
288+
channel, err := conn.Accept(ctx)
289+
if err != nil {
290+
if errors.Is(err, peer.ErrClosed) || s.isClosed() {
291+
return
292+
}
293+
s.options.Logger.Debug(ctx, "accept channel from peer connection", slog.Error(err))
294+
return
295+
}
296+
297+
switch channel.Protocol() {
298+
case "ssh":
299+
s.sshServer.HandleConn(channel.NetConn())
300+
default:
301+
s.options.Logger.Warn(ctx, "unhandled protocol from channel",
302+
slog.F("protocol", channel.Protocol()),
303+
slog.F("label", channel.Label()),
304+
)
305+
}
306+
}
307+
}
308+
309+
// isClosed returns whether the API is closed or not.
310+
func (s *server) isClosed() bool {
311+
select {
312+
case <-s.closed:
313+
return true
314+
default:
315+
return false
316+
}
317+
}
318+
319+
func (s *server) Close() error {
320+
s.closeMutex.Lock()
321+
defer s.closeMutex.Unlock()
322+
if s.isClosed() {
323+
return nil
324+
}
325+
close(s.closed)
326+
s.closeCancel()
327+
_ = s.sshServer.Close()
328+
return nil
329+
}

0 commit comments

Comments
 (0)