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

Skip to content

Commit 30136fc

Browse files
committed
Merge branch 'main' into bpmct/add-docker-builds
1 parent a747cec commit 30136fc

File tree

136 files changed

+4282
-1141
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

136 files changed

+4282
-1141
lines changed

.goreleaser.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
archives:
22
- id: coder-linux
33
builds: [coder-linux]
4-
format: tar
4+
format: tar.gz
55
files:
66
- src: docs/README.md
77
dst: README.md

.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@
7373
{
7474
"match": "database/queries/*.sql",
7575
"cmd": "make gen"
76+
},
77+
{
78+
"match": "provisionerd/proto/provisionerd.proto",
79+
"cmd": "make provisionerd/proto/provisionerd.pb.go",
7680
}
7781
]
7882
},

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ coderd/database/dump.sql: $(wildcard coderd/database/migrations/*.sql)
2020
coderd/database/querier.go: coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql)
2121
coderd/database/generate.sh
2222

23+
dev:
24+
./scripts/develop.sh
25+
.PHONY: dev
26+
2327
dist/artifacts.json: site/out/index.html $(shell find . -not -path './vendor/*' -type f -name '*.go') go.mod go.sum
2428
goreleaser release --snapshot --rm-dist --skip-sign
2529

agent/agent.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"io"
1111
"net"
12+
"net/url"
1213
"os"
1314
"os/exec"
1415
"os/user"
@@ -211,6 +212,8 @@ func (a *agent) handlePeerConn(ctx context.Context, conn *peer.Conn) {
211212
go a.sshServer.HandleConn(channel.NetConn())
212213
case "reconnecting-pty":
213214
go a.handleReconnectingPTY(ctx, channel.Label(), channel.NetConn())
215+
case "dial":
216+
go a.handleDial(ctx, channel.Label(), channel.NetConn())
214217
default:
215218
a.logger.Warn(ctx, "unhandled protocol from channel",
216219
slog.F("protocol", channel.Protocol()),
@@ -617,6 +620,70 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, rawID string, conn ne
617620
}
618621
}
619622

623+
// dialResponse is written to datachannels with protocol "dial" by the agent as
624+
// the first packet to signify whether the dial succeeded or failed.
625+
type dialResponse struct {
626+
Error string `json:"error,omitempty"`
627+
}
628+
629+
func (a *agent) handleDial(ctx context.Context, label string, conn net.Conn) {
630+
defer conn.Close()
631+
632+
writeError := func(responseError error) error {
633+
msg := ""
634+
if responseError != nil {
635+
msg = responseError.Error()
636+
if !xerrors.Is(responseError, io.EOF) {
637+
a.logger.Warn(ctx, "handle dial", slog.F("label", label), slog.Error(responseError))
638+
}
639+
}
640+
b, err := json.Marshal(dialResponse{
641+
Error: msg,
642+
})
643+
if err != nil {
644+
a.logger.Warn(ctx, "write dial response", slog.F("label", label), slog.Error(err))
645+
return xerrors.Errorf("marshal agent webrtc dial response: %w", err)
646+
}
647+
648+
_, err = conn.Write(b)
649+
return err
650+
}
651+
652+
u, err := url.Parse(label)
653+
if err != nil {
654+
_ = writeError(xerrors.Errorf("parse URL %q: %w", label, err))
655+
return
656+
}
657+
658+
network := u.Scheme
659+
addr := u.Host + u.Path
660+
if strings.HasPrefix(network, "unix") {
661+
if runtime.GOOS == "windows" {
662+
_ = writeError(xerrors.New("Unix forwarding is not supported from Windows workspaces"))
663+
return
664+
}
665+
addr, err = ExpandRelativeHomePath(addr)
666+
if err != nil {
667+
_ = writeError(xerrors.Errorf("expand path %q: %w", addr, err))
668+
return
669+
}
670+
}
671+
672+
d := net.Dialer{Timeout: 3 * time.Second}
673+
nconn, err := d.DialContext(ctx, network, addr)
674+
if err != nil {
675+
_ = writeError(xerrors.Errorf("dial '%v://%v': %w", network, addr, err))
676+
return
677+
}
678+
679+
err = writeError(nil)
680+
if err != nil {
681+
return
682+
}
683+
684+
Bicopy(ctx, conn, nconn)
685+
}
686+
620687
// isClosed returns whether the API is closed or not.
621688
func (a *agent) isClosed() bool {
622689
select {
@@ -662,3 +729,50 @@ func (r *reconnectingPTY) Close() {
662729
r.circularBuffer.Reset()
663730
r.timeout.Stop()
664731
}
732+
733+
// Bicopy copies all of the data between the two connections and will close them
734+
// after one or both of them are done writing. If the context is canceled, both
735+
// of the connections will be closed.
736+
func Bicopy(ctx context.Context, c1, c2 io.ReadWriteCloser) {
737+
defer c1.Close()
738+
defer c2.Close()
739+
740+
var wg sync.WaitGroup
741+
copyFunc := func(dst io.WriteCloser, src io.Reader) {
742+
defer wg.Done()
743+
_, _ = io.Copy(dst, src)
744+
}
745+
746+
wg.Add(2)
747+
go copyFunc(c1, c2)
748+
go copyFunc(c2, c1)
749+
750+
// Convert waitgroup to a channel so we can also wait on the context.
751+
done := make(chan struct{})
752+
go func() {
753+
defer close(done)
754+
wg.Wait()
755+
}()
756+
757+
select {
758+
case <-ctx.Done():
759+
case <-done:
760+
}
761+
}
762+
763+
// ExpandRelativeHomePath expands the tilde at the beginning of a path to the
764+
// current user's home directory and returns a full absolute path.
765+
func ExpandRelativeHomePath(in string) (string, error) {
766+
usr, err := user.Current()
767+
if err != nil {
768+
return "", xerrors.Errorf("get current user details: %w", err)
769+
}
770+
771+
if in == "~" {
772+
in = usr.HomeDir
773+
} else if strings.HasPrefix(in, "~/") {
774+
in = filepath.Join(usr.HomeDir, in[2:])
775+
}
776+
777+
return filepath.Abs(in)
778+
}

agent/agent_test.go

Lines changed: 158 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package agent_test
22

33
import (
4+
"bufio"
45
"context"
56
"encoding/json"
67
"fmt"
@@ -16,6 +17,7 @@ import (
1617
"time"
1718

1819
"github.com/google/uuid"
20+
"github.com/pion/udp"
1921
"github.com/pion/webrtc/v3"
2022
"github.com/pkg/sftp"
2123
"github.com/stretchr/testify/require"
@@ -203,6 +205,11 @@ func TestAgent(t *testing.T) {
203205
id := uuid.NewString()
204206
netConn, err := conn.ReconnectingPTY(id, 100, 100)
205207
require.NoError(t, err)
208+
bufRead := bufio.NewReader(netConn)
209+
210+
// Brief pause to reduce the likelihood that we send keystrokes while
211+
// the shell is simultaneously sending a prompt.
212+
time.Sleep(100 * time.Millisecond)
206213

207214
data, err := json.Marshal(agent.ReconnectingPTYRequest{
208215
Data: "echo test\r\n",
@@ -211,28 +218,141 @@ func TestAgent(t *testing.T) {
211218
_, err = netConn.Write(data)
212219
require.NoError(t, err)
213220

214-
findEcho := func() {
221+
expectLine := func(matcher func(string) bool) {
215222
for {
216-
read, err := netConn.Read(data)
223+
line, err := bufRead.ReadString('\n')
217224
require.NoError(t, err)
218-
if strings.Contains(string(data[:read]), "test") {
225+
if matcher(line) {
219226
break
220227
}
221228
}
222229
}
230+
matchEchoCommand := func(line string) bool {
231+
return strings.Contains(line, "echo test")
232+
}
233+
matchEchoOutput := func(line string) bool {
234+
return strings.Contains(line, "test") && !strings.Contains(line, "echo")
235+
}
223236

224237
// Once for typing the command...
225-
findEcho()
238+
expectLine(matchEchoCommand)
226239
// And another time for the actual output.
227-
findEcho()
240+
expectLine(matchEchoOutput)
228241

229242
_ = netConn.Close()
230243
netConn, err = conn.ReconnectingPTY(id, 100, 100)
231244
require.NoError(t, err)
245+
bufRead = bufio.NewReader(netConn)
232246

233247
// Same output again!
234-
findEcho()
235-
findEcho()
248+
expectLine(matchEchoCommand)
249+
expectLine(matchEchoOutput)
250+
})
251+
252+
t.Run("Dial", func(t *testing.T) {
253+
t.Parallel()
254+
255+
cases := []struct {
256+
name string
257+
setup func(t *testing.T) net.Listener
258+
}{
259+
{
260+
name: "TCP",
261+
setup: func(t *testing.T) net.Listener {
262+
l, err := net.Listen("tcp", "127.0.0.1:0")
263+
require.NoError(t, err, "create TCP listener")
264+
return l
265+
},
266+
},
267+
{
268+
name: "UDP",
269+
setup: func(t *testing.T) net.Listener {
270+
addr := net.UDPAddr{
271+
IP: net.ParseIP("127.0.0.1"),
272+
Port: 0,
273+
}
274+
l, err := udp.Listen("udp", &addr)
275+
require.NoError(t, err, "create UDP listener")
276+
return l
277+
},
278+
},
279+
{
280+
name: "Unix",
281+
setup: func(t *testing.T) net.Listener {
282+
if runtime.GOOS == "windows" {
283+
t.Skip("Unix socket forwarding isn't supported on Windows")
284+
}
285+
286+
tmpDir, err := os.MkdirTemp("", "coderd_agent_test_")
287+
require.NoError(t, err, "create temp dir for unix listener")
288+
t.Cleanup(func() {
289+
_ = os.RemoveAll(tmpDir)
290+
})
291+
292+
l, err := net.Listen("unix", filepath.Join(tmpDir, "test.sock"))
293+
require.NoError(t, err, "create UDP listener")
294+
return l
295+
},
296+
},
297+
}
298+
299+
for _, c := range cases {
300+
c := c
301+
t.Run(c.name, func(t *testing.T) {
302+
t.Parallel()
303+
304+
// Setup listener
305+
l := c.setup(t)
306+
defer l.Close()
307+
go func() {
308+
for {
309+
c, err := l.Accept()
310+
if err != nil {
311+
return
312+
}
313+
314+
go testAccept(t, c)
315+
}
316+
}()
317+
318+
// Dial the listener over WebRTC twice and test out of order
319+
conn := setupAgent(t, agent.Metadata{}, 0)
320+
conn1, err := conn.DialContext(context.Background(), l.Addr().Network(), l.Addr().String())
321+
require.NoError(t, err)
322+
defer conn1.Close()
323+
conn2, err := conn.DialContext(context.Background(), l.Addr().Network(), l.Addr().String())
324+
require.NoError(t, err)
325+
defer conn2.Close()
326+
testDial(t, conn2)
327+
testDial(t, conn1)
328+
})
329+
}
330+
})
331+
332+
t.Run("DialError", func(t *testing.T) {
333+
t.Parallel()
334+
335+
if runtime.GOOS == "windows" {
336+
// This test uses Unix listeners so we can very easily ensure that
337+
// no other tests decide to listen on the same random port we
338+
// picked.
339+
t.Skip("this test is unsupported on Windows")
340+
return
341+
}
342+
343+
tmpDir, err := os.MkdirTemp("", "coderd_agent_test_")
344+
require.NoError(t, err, "create temp dir")
345+
t.Cleanup(func() {
346+
_ = os.RemoveAll(tmpDir)
347+
})
348+
349+
// Try to dial the non-existent Unix socket over WebRTC
350+
conn := setupAgent(t, agent.Metadata{}, 0)
351+
netConn, err := conn.DialContext(context.Background(), "unix", filepath.Join(tmpDir, "test.sock"))
352+
require.Error(t, err)
353+
require.ErrorContains(t, err, "remote dial error")
354+
require.ErrorContains(t, err, "no such file")
355+
require.Nil(t, netConn)
236356
})
237357
}
238358

@@ -303,3 +423,34 @@ func setupAgent(t *testing.T, metadata agent.Metadata, ptyTimeout time.Duration)
303423
Conn: conn,
304424
}
305425
}
426+
427+
var dialTestPayload = []byte("dean-was-here123")
428+
429+
func testDial(t *testing.T, c net.Conn) {
430+
t.Helper()
431+
432+
assertWritePayload(t, c, dialTestPayload)
433+
assertReadPayload(t, c, dialTestPayload)
434+
}
435+
436+
func testAccept(t *testing.T, c net.Conn) {
437+
t.Helper()
438+
defer c.Close()
439+
440+
assertReadPayload(t, c, dialTestPayload)
441+
assertWritePayload(t, c, dialTestPayload)
442+
}
443+
444+
func assertReadPayload(t *testing.T, r io.Reader, payload []byte) {
445+
b := make([]byte, len(payload)+16)
446+
n, err := r.Read(b)
447+
require.NoError(t, err, "read payload")
448+
require.Equal(t, len(payload), n, "read payload length does not match")
449+
require.Equal(t, payload, b[:n])
450+
}
451+
452+
func assertWritePayload(t *testing.T, w io.Writer, payload []byte) {
453+
n, err := w.Write(payload)
454+
require.NoError(t, err, "write payload")
455+
require.Equal(t, len(payload), n, "payload length does not match")
456+
}

0 commit comments

Comments
 (0)