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)
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