diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0d449cc..6dda08e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,22 +16,22 @@ jobs: goreleaser: runs-on: ubuntu-latest-8-cores steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Allow goreleaser to access older tag information. fetch-depth: 0 - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version-file: "go.mod" cache: true - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 + uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0 id: import_gpg with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 + uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 with: args: release --clean env: diff --git a/.gitignore b/.gitignore index ea90573..e15b546 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,11 @@ test main.wasm test.txt +.next/ +.env +site/wasm/*.js +site/wasm/*.wasm +site/node_modules/ +.env.local + +.vercel diff --git a/README.md b/README.md index bf8610a..dfd02b6 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,34 @@ shells over a peer-to-peer WireGuard connection. It's similar to 1. Automatic peer-to-peer connections over UDP. 1. Endless possibilities; rsync, ssh, etc. +## Basic Usage + +On the host machine: + +```bash +$ wush serve +Picked DERP region Toronto as overlay home +Your auth key is: + > 112v1RyL5KPzsbMbhT7fkEGrcfpygxtnvwjR5kMLGxDHGeLTK1BvoPqsUcjo7xyMkFn46KLTdedKuPCG5trP84mz9kx +Use this key to authenticate other wush commands to this instance. +``` + +On the client machine: + +```bash +# Copy a file to the host +$ wush cp 1gb.txt +Uploading "1gb.txt" 100% |██████████████████████████████████████████████| (2.1/2.1 GB, 376 MB/s) + +# Open a shell to the host +$ wush ssh +┃ Enter the Auth key: +┃ > 112v1RyL5KPzsbMbhT7fkEGrcfpygxtnvwjR5kMLGxDHGeLTK1BvoPqsUcjo7xyMkFn46KLTdedKuPCG5trP84mz9kx +coder@colin:~$ +``` + +[![asciicast](https://asciinema.org/a/ZrCNiRRkeHUi5Lj3fqC3ovLqi.svg)](https://asciinema.org/a/ZrCNiRRkeHUi5Lj3fqC3ovLqi) + > [!NOTE] > `wush` uses Tailscale's [tsnet](https://tailscale.com/kb/1244/tsnet) package > under the hood, managed by an in-memory control server on each CLI. We utilize @@ -20,11 +48,13 @@ shells over a peer-to-peer WireGuard connection. It's similar to ## Install Using install script + ```bash -curl -fsSL https://wush.dev/install.sh | sh +curl -fsSL https://github.com/coder/wush/raw/refs/heads/main/install.sh | sh ``` Using Homebrew + ```bash brew install wush ``` @@ -41,56 +71,6 @@ For a manual installation, see the [latest release](https://github.com/coder/wus > sudo setcap cap_net_admin=eip $(which wush) > ``` -## Basic Usage - -[![asciicast](https://asciinema.org/a/ZrCNiRRkeHUi5Lj3fqC3ovLqi.svg)](https://asciinema.org/a/ZrCNiRRkeHUi5Lj3fqC3ovLqi) - -On the host machine: - -```bash -$ wush host -Picked DERP region Toronto as overlay home -Your auth key is: - > 112v1RyL5KPzsbMbhT7fkEGrcfpygxtnvwjR5kMLGxDHGeLTK1BvoPqsUcjo7xyMkFn46KLTdedKuPCG5trP84mz9kx -Use this key to authenticate other wush commands to this instance. -05:18:59 WireGuard is ready -05:18:59 SSH server listening -``` - -On the client machine: - -```bash -# Copy a file to the receiver -$ wush cp 1gb.txt -Auth information: - > Server overlay STUN address: Disabled - > Server overlay DERP home: Toronto - > Server overlay public key: [NFWN0] - > Server overlay auth key: [mTbpN] -Bringing WireGuard up.. -WireGuard is ready! -Received peer -Peer active with relay nyc -Peer active over p2p 172.20.0.8:53768 -Uploading "1gb.txt" 100% |██████████████████████████████████████████████| (2.1/2.1 GB, 376 MB/s) - -# Open a shell to the receiver -$ wush ssh -┃ Enter the receiver's Auth key: -┃ > 112v1RyL5KPzsbMbhT7fkEGrcfpygxtnvwjR5kMLGxDHGeLTK1BvoPqsUcjo7xyMkFn46KLTdedKuPCG5trP84mz9kx -Auth information: - > Server overlay STUN address: Disabled - > Server overlay DERP home: Toronto - > Server overlay public key: [sEIS1] - > Server overlay auth key: [w/sYF] -Bringing WireGuard up.. -WireGuard is ready! -Received peer -Peer active with relay nyc -Peer active over p2p 172.20.0.8:44483 -coder@colin:~$ -``` - ## Technical Details `wush` doesn't require you to trust any 3rd party authentication or relay diff --git a/cliui/cliui.go b/cliui/cliui.go index 8a7ae46..9baa25c 100644 --- a/cliui/cliui.go +++ b/cliui/cliui.go @@ -50,7 +50,7 @@ var ( func Color(s string) termenv.Color { colorOnce.Do(func() { color = termenv.NewOutput(os.Stdout).ColorProfile() - if flag.Lookup("test.v") != nil { + if _, exists := os.LookupEnv("NO_COLOR"); exists || flag.Lookup("test.v") != nil { // Use a consistent colorless profile in tests so that results // are deterministic. color = termenv.Ascii diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go deleted file mode 100644 index b0560e6..0000000 --- a/cmd/wasm/main.go +++ /dev/null @@ -1,308 +0,0 @@ -//go:build js && wasm - -package main - -import ( - "bytes" - "context" - "fmt" - "log" - "log/slog" - "net" - "syscall/js" - "time" - - "github.com/coder/wush/overlay" - "github.com/coder/wush/tsserver" - "golang.org/x/crypto/ssh" - "golang.org/x/xerrors" - "tailscale.com/ipn/store" - "tailscale.com/net/netns" - "tailscale.com/tsnet" -) - -func main() { - fmt.Println("WebAssembly module initialized") - defer fmt.Println("WebAssembly module exited") - - js.Global().Set("newWush", js.FuncOf(func(this js.Value, args []js.Value) any { - handler := js.FuncOf(func(this js.Value, promiseArgs []js.Value) any { - if len(args) != 1 { - log.Fatal("Usage: newWush(config)") - return nil - } - - go func() { - w := newWush(args[0]) - promiseArgs[0].Invoke(w) - }() - - return nil - }) - - promiseConstructor := js.Global().Get("Promise") - return promiseConstructor.New(handler) - })) - js.Global().Set("exitWush", js.FuncOf(func(this js.Value, args []js.Value) any { - // close(ch) - return nil - })) - - // Keep the main function running - <-make(chan struct{}, 0) -} - -func newWush(jsConfig js.Value) map[string]any { - ctx := context.Background() - var authKey string - if jsAuthKey := jsConfig.Get("authKey"); jsAuthKey.Type() == js.TypeString { - authKey = jsAuthKey.String() - } - - logger := slog.New(slog.NewTextHandler(jsConsoleWriter{}, nil)) - hlog := func(format string, args ...any) { - fmt.Printf(format+"\n", args...) - } - dm, err := tsserver.DERPMapTailscale(ctx) - if err != nil { - panic(err) - } - - send := overlay.NewSendOverlay(logger, dm) - err = send.Auth.Parse(authKey) - if err != nil { - panic(err) - } - - s, err := tsserver.NewServer(ctx, logger, send) - if err != nil { - panic(err) - } - - go send.ListenOverlayDERP(ctx) - go s.ListenAndServe(ctx) - netns.SetDialerOverride(s.Dialer()) - - ts, err := newTSNet("send") - if err != nil { - panic(err) - } - - _, err = ts.Up(ctx) - if err != nil { - panic(err) - } - hlog("WireGuard is ready") - - return map[string]any{ - "stop": js.FuncOf(func(this js.Value, args []js.Value) any { - if len(args) != 0 { - log.Printf("Usage: stop()") - return nil - } - ts.Close() - return nil - }), - "ssh": js.FuncOf(func(this js.Value, args []js.Value) any { - if len(args) != 1 { - log.Printf("Usage: ssh({})") - return nil - } - - sess := &sshSession{ - ts: ts, - cfg: args[0], - } - - go sess.Run() - - return map[string]any{ - "close": js.FuncOf(func(this js.Value, args []js.Value) any { - return sess.Close() != nil - }), - "resize": js.FuncOf(func(this js.Value, args []js.Value) any { - rows := args[0].Int() - cols := args[1].Int() - return sess.Resize(rows, cols) != nil - }), - } - }), - } -} - -type sshSession struct { - ts *tsnet.Server - cfg js.Value - - session *ssh.Session - pendingResizeRows int - pendingResizeCols int -} - -func (s *sshSession) Close() error { - if s.session == nil { - // We never had a chance to open the session, ignore the close request. - return nil - } - return s.session.Close() -} - -func (s *sshSession) Resize(rows, cols int) error { - if s.session == nil { - s.pendingResizeRows = rows - s.pendingResizeCols = cols - return nil - } - return s.session.WindowChange(rows, cols) -} - -func (s *sshSession) Run() { - writeFn := s.cfg.Get("writeFn") - writeErrorFn := s.cfg.Get("writeErrorFn") - setReadFn := s.cfg.Get("setReadFn") - rows := s.cfg.Get("rows").Int() - cols := s.cfg.Get("cols").Int() - timeoutSeconds := 5.0 - if jsTimeoutSeconds := s.cfg.Get("timeoutSeconds"); jsTimeoutSeconds.Type() == js.TypeNumber { - timeoutSeconds = jsTimeoutSeconds.Float() - } - onConnectionProgress := s.cfg.Get("onConnectionProgress") - onConnected := s.cfg.Get("onConnected") - onDone := s.cfg.Get("onDone") - defer onDone.Invoke() - - writeError := func(label string, err error) { - writeErrorFn.Invoke(fmt.Sprintf("%s Error: %v\r\n", label, err)) - } - reportProgress := func(message string) { - onConnectionProgress.Invoke(message) - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds*float64(time.Second))) - defer cancel() - reportProgress(fmt.Sprintf("Connecting...")) - c, err := s.ts.Dial(ctx, "tcp", net.JoinHostPort("100.64.0.0", "3")) - if err != nil { - writeError("Dial", err) - return - } - defer c.Close() - - config := &ssh.ClientConfig{ - HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { - // Host keys are not used with Tailscale SSH, but we can use this - // callback to know that the connection has been established. - reportProgress("SSH connection established…") - return nil - }, - } - - reportProgress("Starting SSH client…") - sshConn, _, _, err := ssh.NewClientConn(c, "100.64.0.0:3", config) - if err != nil { - writeError("SSH Connection", err) - return - } - defer sshConn.Close() - - sshClient := ssh.NewClient(sshConn, nil, nil) - defer sshClient.Close() - - session, err := sshClient.NewSession() - if err != nil { - writeError("SSH Session", err) - return - } - s.session = session - defer session.Close() - - stdin, err := session.StdinPipe() - if err != nil { - writeError("SSH Stdin", err) - return - } - - session.Stdout = termWriter{writeFn} - session.Stderr = termWriter{writeFn} - - setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) any { - input := args[0].String() - _, err := stdin.Write([]byte(input)) - if err != nil { - writeError("Write Input", err) - } - return nil - })) - - // We might have gotten a resize notification since we started opening the - // session, pick up the latest size. - if s.pendingResizeRows != 0 { - rows = s.pendingResizeRows - } - if s.pendingResizeCols != 0 { - cols = s.pendingResizeCols - } - err = session.RequestPty("xterm", rows, cols, ssh.TerminalModes{}) - - if err != nil { - writeError("Pseudo Terminal", err) - return - } - - err = session.Shell() - if err != nil { - writeError("Shell", err) - return - } - - onConnected.Invoke() - err = session.Wait() - if err != nil { - writeError("Wait", err) - return - } -} - -type termWriter struct { - f js.Value -} - -func (w termWriter) Write(p []byte) (n int, err error) { - r := bytes.Replace(p, []byte("\n"), []byte("\n\r"), -1) - w.f.Invoke(string(r)) - return len(p), nil -} - -type jsConsoleWriter struct{} - -func (w jsConsoleWriter) Write(p []byte) (n int, err error) { - js.Global().Get("console").Call("log", string(p)) - return len(p), nil -} - -func newTSNet(direction string) (*tsnet.Server, error) { - var err error - // tmp := os.TempDir() - srv := new(tsnet.Server) - // srv.Dir = tmp - srv.Hostname = "wush-" + direction - srv.Ephemeral = true - srv.AuthKey = direction - srv.ControlURL = "http://127.0.0.1:8080" - // srv.Logf = func(format string, args ...any) {} - srv.Logf = func(format string, args ...any) { - fmt.Printf(format+"\n", args...) - } - // srv.UserLogf = func(format string, args ...any) {} - srv.UserLogf = func(format string, args ...any) { - fmt.Printf(format+"\n", args...) - } - // netns.SetEnabled(false) - - srv.Store, err = store.New(func(format string, args ...any) {}, "mem:wush") - if err != nil { - return nil, xerrors.Errorf("create state store: %w", err) - } - - return srv, nil -} diff --git a/cmd/wasm/main_js.go b/cmd/wasm/main_js.go new file mode 100644 index 0000000..d2bab5e --- /dev/null +++ b/cmd/wasm/main_js.go @@ -0,0 +1,703 @@ +//go:build js && wasm + +package main + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "log" + "log/slog" + "net" + "net/http" + "strings" + "syscall/js" + "time" + + "github.com/coder/coder/v2/coderd/util/ptr" + "github.com/coder/wush/overlay" + "github.com/coder/wush/tsserver" + "github.com/pion/webrtc/v4" + "golang.org/x/crypto/ssh" + "golang.org/x/xerrors" + "tailscale.com/ipn/store" + "tailscale.com/net/netns" + "tailscale.com/tsnet" +) + +func main() { + fmt.Println("WebAssembly module initialized") + defer fmt.Println("WebAssembly module exited") + + js.Global().Set("newWush", js.FuncOf(func(this js.Value, args []js.Value) any { + handler := js.FuncOf(func(this js.Value, promiseArgs []js.Value) any { + if len(args) != 1 { + log.Fatal("Usage: newWush(config)") + return nil + } + + go func() { + w := newWush(args[0]) + promiseArgs[0].Invoke(w) + }() + + return nil + }) + + promiseConstructor := js.Global().Get("Promise") + return promiseConstructor.New(handler) + })) + + // Keep the main function running + <-make(chan struct{}, 0) +} + +func newWush(cfg js.Value) map[string]any { + ctx := context.Background() + logger := slog.New(slog.NewTextHandler(jsConsoleWriter{}, nil)) + hlog := func(format string, args ...any) { + fmt.Printf(format+"\n", args...) + } + dm, err := tsserver.DERPMapTailscale(ctx) + if err != nil { + panic(err) + } + + ov := overlay.NewWasmOverlay(log.Printf, dm, + cfg.Get("onNewPeer"), + cfg.Get("onWebrtcOffer"), + cfg.Get("onWebrtcAnswer"), + cfg.Get("onWebrtcCandidate"), + ) + + // Try to get stored DERP home from localStorage + localStorage := js.Global().Get("localStorage") + storedDerpHome := localStorage.Call("getItem", "derpHome") + if !storedDerpHome.IsNull() { + // Parse stored DERP home and use it + derpID := uint16(js.Global().Get("parseInt").Invoke(storedDerpHome).Int()) + if region := dm.Regions[int(derpID)]; region != nil { + ov.DerpRegionID = derpID + hlog("Using stored DERP home: %s", region.RegionName) + } else { + // If stored DERP home is invalid, pick a new one + err = ov.PickDERPHome(ctx) + if err != nil { + panic(err) + } + // Store the newly picked DERP home + localStorage.Call("setItem", "derpHome", fmt.Sprint(ov.DerpRegionID)) + } + } else { + // No stored DERP home, pick a new one + err = ov.PickDERPHome(ctx) + if err != nil { + panic(err) + } + // Store the picked DERP home + localStorage.Call("setItem", "derpHome", fmt.Sprint(ov.DerpRegionID)) + } + + s, err := tsserver.NewServer(ctx, logger, ov, dm) + if err != nil { + panic(err) + } + + go ov.ListenOverlayDERP(ctx) + go s.ListenAndServe(ctx) + netns.SetDialerOverride(s.Dialer()) + + ts, err := newTSNet("send") + if err != nil { + panic(err) + } + + go func() { + _, err = ts.Up(ctx) + if err != nil { + panic(err) + } + hlog("WireGuard is ready") + }() + + cpListener, err := ts.Listen("tcp", ":4444") + if err != nil { + panic(err) + } + + go func() { + err := http.Serve(cpListener, http.HandlerFunc(cpH( + cfg.Get("onIncomingFile"), + cfg.Get("downloadFile"), + ))) + if err != nil { + hlog("File transfer server exited: " + err.Error()) + } + }() + + return map[string]any{ + "auth_info": js.FuncOf(func(this js.Value, args []js.Value) any { + if len(args) != 0 { + log.Printf("Usage: auth_info()") + return nil + } + + return map[string]any{ + "derp_id": ov.DerpRegionID, + "derp_name": ov.DerpMap.Regions[int(ov.DerpRegionID)].RegionName, + "derp_latency": ov.DerpLatency.Milliseconds(), + "auth_key": ov.ClientAuth().AuthKey(), + } + }), + "stop": js.FuncOf(func(this js.Value, args []js.Value) any { + if len(args) != 0 { + log.Printf("Usage: stop()") + return nil + } + cpListener.Close() + ts.Close() + return nil + }), + "ssh": js.FuncOf(func(this js.Value, args []js.Value) any { + if len(args) != 2 { + log.Printf("Usage: ssh(peer, config)") + return nil + } + + sess := &sshSession{ + ts: ts, + cfg: args[1], + } + + go sess.Run() + + return map[string]any{ + "close": js.FuncOf(func(this js.Value, args []js.Value) any { + return sess.Close() != nil + }), + "resize": js.FuncOf(func(this js.Value, args []js.Value) any { + rows := args[0].Int() + cols := args[1].Int() + return sess.Resize(rows, cols) != nil + }), + } + }), + "connect": js.FuncOf(func(this js.Value, args []js.Value) any { + handler := js.FuncOf(func(this js.Value, promiseArgs []js.Value) any { + resolve := promiseArgs[0] + reject := promiseArgs[1] + + go func() { + if len(args) != 2 { + errorConstructor := js.Global().Get("Error") + errorObject := errorConstructor.New("Usage: connect(authKey, offer)") + reject.Invoke(errorObject) + return + } + + var authKey string + if args[0].Type() == js.TypeString { + authKey = args[0].String() + } else { + errorConstructor := js.Global().Get("Error") + errorObject := errorConstructor.New("Usage: connect(authKey, offer)") + reject.Invoke(errorObject) + return + } + + var offer webrtc.SessionDescription + if jsOffer := args[1]; jsOffer.Type() == js.TypeObject { + offer.SDP = jsOffer.Get("sdp").String() + offer.Type = webrtc.NewSDPType(jsOffer.Get("type").String()) + } else { + errorConstructor := js.Global().Get("Error") + errorObject := errorConstructor.New("Usage: connect(authKey, offer)") + reject.Invoke(errorObject) + return + } + + var ca overlay.ClientAuth + err := ca.Parse(authKey) + if err != nil { + errorConstructor := js.Global().Get("Error") + errorObject := errorConstructor.New(fmt.Errorf("parse authkey: %w", err).Error()) + reject.Invoke(errorObject) + return + } + + ctx, cancel := context.WithCancel(context.Background()) + peer, err := ov.Connect(ctx, ca, offer) + if err != nil { + cancel() + errorConstructor := js.Global().Get("Error") + errorObject := errorConstructor.New(fmt.Errorf("connect to peer: %w", err).Error()) + reject.Invoke(errorObject) + return + } + + resolve.Invoke(map[string]any{ + "id": js.ValueOf(peer.ID), + "name": js.ValueOf(peer.Name), + "ip": js.ValueOf(peer.IP.String()), + "type": js.ValueOf(peer.Type), + "cancel": js.FuncOf(func(this js.Value, args []js.Value) any { + cancel() + return nil + }), + }) + }() + + return nil + }) + + promiseConstructor := js.Global().Get("Promise") + return promiseConstructor.New(handler) + }), + "transfer": js.FuncOf(func(this js.Value, args []js.Value) any { + handler := js.FuncOf(func(this js.Value, promiseArgs []js.Value) any { + resolve := promiseArgs[0] + reject := promiseArgs[1] + + if len(args) != 5 { + errorConstructor := js.Global().Get("Error") + errorObject := errorConstructor.New("Usage: transfer(peer, fileName, sizeBytes, stream, onProgress)") + reject.Invoke(errorObject) + return nil + } + + peer := args[0] + ip := peer.Get("ip").String() + fileName := args[1].String() + sizeBytes := int64(args[2].Int()) + stream := args[3] + onProgress := args[4] + + go func() { + startTime := time.Now() + reader := &jsStreamReader{ + reader: stream.Call("getReader"), + onProgress: onProgress, + totalSize: sizeBytes, + } + bufferSize := 1024 * 1024 + hc := &http.Client{ + Transport: &http.Transport{ + DialContext: ts.Dial, + ReadBufferSize: bufferSize, + WriteBufferSize: bufferSize, + }, + } + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s:4444/%s", ip, fileName), bufio.NewReaderSize(reader, bufferSize)) + if err != nil { + errorConstructor := js.Global().Get("Error") + errorObject := errorConstructor.New(err.Error()) + reject.Invoke(errorObject) + return + } + req.ContentLength = int64(sizeBytes) + + fmt.Printf("Starting transfer of %d bytes\n", sizeBytes) + res, err := hc.Do(req) + if err != nil { + errorConstructor := js.Global().Get("Error") + errorObject := errorConstructor.New(err.Error()) + reject.Invoke(errorObject) + return + } + defer res.Body.Close() + + bod := bytes.NewBuffer(nil) + _, _ = io.Copy(bod, res.Body) + + duration := time.Since(startTime) + speed := float64(sizeBytes) / duration.Seconds() / 1024 / 1024 // MB/s + fmt.Printf("Transfer completed in %v. Speed: %.2f MB/s\n", duration, speed) + + resolve.Invoke() + }() + + return nil + }) + + promiseConstructor := js.Global().Get("Promise") + return promiseConstructor.New(handler) + }), + + "sendWebrtcCandidate": js.FuncOf(func(this js.Value, args []js.Value) any { + peer := args[0].String() + candidate := args[1] + + ov.SendWebrtcCandidate(peer, webrtc.ICECandidateInit{ + Candidate: candidate.Get("candidate").String(), + SDPMLineIndex: ptr.Ref(uint16(candidate.Get("sdpMLineIndex").Int())), + SDPMid: ptr.Ref(candidate.Get("sdpMid").String()), + UsernameFragment: ptr.Ref(candidate.Get("sdpMid").String()), + }) + + return nil + }), + + "parseAuthKey": js.FuncOf(func(this js.Value, args []js.Value) any { + authKey := args[0].String() + + var ca overlay.ClientAuth + _ = ca.Parse(authKey) + typ := "cli" + if ca.Web { + typ = "web" + } + + return map[string]any{ + "id": js.ValueOf(ca.ReceiverPublicKey.String()), + "type": js.ValueOf(typ), + } + }), + } +} + +type sshSession struct { + ts *tsnet.Server + cfg js.Value + + session *ssh.Session + pendingResizeRows int + pendingResizeCols int +} + +func (s *sshSession) Close() error { + if s.session == nil { + // We never had a chance to open the session, ignore the close request. + return nil + } + return s.session.Close() +} + +func (s *sshSession) Resize(rows, cols int) error { + if s.session == nil { + s.pendingResizeRows = rows + s.pendingResizeCols = cols + return nil + } + return s.session.WindowChange(rows, cols) +} + +func (s *sshSession) Run() { + writeFn := s.cfg.Get("writeFn") + writeErrorFn := s.cfg.Get("writeErrorFn") + setReadFn := s.cfg.Get("setReadFn") + rows := s.cfg.Get("rows").Int() + cols := s.cfg.Get("cols").Int() + timeoutSeconds := 5.0 + if jsTimeoutSeconds := s.cfg.Get("timeoutSeconds"); jsTimeoutSeconds.Type() == js.TypeNumber { + timeoutSeconds = jsTimeoutSeconds.Float() + } + onConnectionProgress := s.cfg.Get("onConnectionProgress") + onConnected := s.cfg.Get("onConnected") + onDone := s.cfg.Get("onDone") + defer onDone.Invoke() + + writeError := func(label string, err error) { + writeErrorFn.Invoke(fmt.Sprintf("%s Error: %v\r\n", label, err)) + } + reportProgress := func(message string) { + onConnectionProgress.Invoke(message) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds*float64(time.Second))) + defer cancel() + reportProgress(fmt.Sprintf("Connecting...")) + c, err := s.ts.Dial(ctx, "tcp", net.JoinHostPort("fd7a:115c:a1e0::1", "3")) + if err != nil { + writeError("Dial", err) + return + } + defer c.Close() + + config := &ssh.ClientConfig{ + HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { + // Host keys are not used with Tailscale SSH, but we can use this + // callback to know that the connection has been established. + reportProgress("SSH connection established…") + return nil + }, + } + + reportProgress("Starting SSH client…") + sshConn, _, _, err := ssh.NewClientConn(c, "100.64.0.0:3", config) + if err != nil { + writeError("SSH Connection", err) + return + } + defer sshConn.Close() + + sshClient := ssh.NewClient(sshConn, nil, nil) + defer sshClient.Close() + + session, err := sshClient.NewSession() + if err != nil { + writeError("SSH Session", err) + return + } + s.session = session + defer session.Close() + + stdin, err := session.StdinPipe() + if err != nil { + writeError("SSH Stdin", err) + return + } + + session.Stdout = termWriter{writeFn} + session.Stderr = termWriter{writeFn} + + setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) any { + input := args[0].String() + _, err := stdin.Write([]byte(input)) + if err != nil { + writeError("Write Input", err) + } + return nil + })) + + // We might have gotten a resize notification since we started opening the + // session, pick up the latest size. + if s.pendingResizeRows != 0 { + rows = s.pendingResizeRows + } + if s.pendingResizeCols != 0 { + cols = s.pendingResizeCols + } + err = session.RequestPty("xterm", rows, cols, ssh.TerminalModes{}) + + if err != nil { + writeError("Pseudo Terminal", err) + return + } + + err = session.Shell() + if err != nil { + writeError("Shell", err) + return + } + + onConnected.Invoke() + err = session.Wait() + if err != nil { + writeError("Wait", err) + return + } +} + +type termWriter struct { + f js.Value +} + +func (w termWriter) Write(p []byte) (n int, err error) { + r := bytes.Replace(p, []byte("\n"), []byte("\n\r"), -1) + w.f.Invoke(string(r)) + return len(p), nil +} + +type jsConsoleWriter struct{} + +func (w jsConsoleWriter) Write(p []byte) (n int, err error) { + js.Global().Get("console").Call("log", string(p)) + return len(p), nil +} + +func newTSNet(direction string) (*tsnet.Server, error) { + var err error + // tmp := os.TempDir() + srv := new(tsnet.Server) + // srv.Dir = tmp + srv.Hostname = "wush-" + direction + srv.Ephemeral = true + srv.AuthKey = direction + srv.ControlURL = "http://127.0.0.1:8080" + // srv.Logf = func(format string, args ...any) {} + srv.Logf = func(format string, args ...any) { + fmt.Printf(format+"\n", args...) + } + // srv.UserLogf = func(format string, args ...any) {} + srv.UserLogf = func(format string, args ...any) { + fmt.Printf(format+"\n", args...) + } + // netns.SetEnabled(false) + + srv.Store, err = store.New(func(format string, args ...any) {}, "mem:wush") + if err != nil { + return nil, xerrors.Errorf("create state store: %w", err) + } + + return srv, nil +} + +func cpH(onIncomingFile js.Value, downloadFile js.Value) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + return + } + + fiName := strings.TrimPrefix(r.URL.Path, "/") + + // TODO: impl + peer := map[string]any{ + "id": js.ValueOf(0), + "name": js.ValueOf(""), + "ip": js.ValueOf(""), + "cancel": js.FuncOf(func(this js.Value, args []js.Value) any { + return nil + }), + } + + allow := make(chan bool) + onIncomingFile.Invoke(peer, fiName, r.ContentLength). + Call("then", js.FuncOf(func(this js.Value, args []js.Value) any { + allow <- args[0].Bool() + return nil + })). + Call("catch", js.FuncOf(func(this js.Value, args []js.Value) any { + fmt.Println("onIncomingFile failed:", args[0].String()) + allow <- false + return nil + })) + if !<-allow { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("File transfer was denied")) + r.Body.Close() + return + } + + underlyingSource := map[string]interface{}{ + // start method + "start": js.FuncOf(func(this js.Value, args []js.Value) interface{} { + // The first and only arg is the controller object + controller := args[0] + + // Process the stream in yet another background goroutine, + // because we can't block on a goroutine invoked by JS in Wasm + // that is dealing with HTTP requests + go func() { + // Close the response body at the end of this method + defer r.Body.Close() + + // Read the entire stream and pass it to JavaScript + for { + // Read up to 1MB at a time + buf := make([]byte, 1024*1024) + n, err := r.Body.Read(buf) + if err != nil && err != io.EOF { + // Tell the controller we have an error + // We're ignoring "EOF" however, which means the stream was done + errorConstructor := js.Global().Get("Error") + errorObject := errorConstructor.New(err.Error()) + controller.Call("error", errorObject) + return + } + if n > 0 { + // If we read anything, send it to JavaScript using the "enqueue" method on the controller + // We need to convert it to a Uint8Array first + arrayConstructor := js.Global().Get("Uint8Array") + dataJS := arrayConstructor.New(n) + js.CopyBytesToJS(dataJS, buf[0:n]) + controller.Call("enqueue", dataJS) + } + if err == io.EOF { + // Stream is done, so call the "close" method on the controller + controller.Call("close") + return + } + } + }() + + return nil + }), + // cancel method + "cancel": js.FuncOf(func(this js.Value, args []js.Value) interface{} { + // If the request is canceled, just close the body + r.Body.Close() + + return nil + }), + } + + readableStreamConstructor := js.Global().Get("ReadableStream") + readableStream := readableStreamConstructor.New(underlyingSource) + + downloadFile.Invoke(peer, fiName, r.ContentLength, readableStream) + } +} + +// jsStreamReader implements io.Reader for JavaScript streams +type jsStreamReader struct { + reader js.Value + onProgress js.Value + bytesRead int64 + totalSize int64 + buffer bytes.Buffer +} + +func (r *jsStreamReader) Read(p []byte) (n int, err error) { + if r.bytesRead >= r.totalSize { + return 0, io.EOF + } + + fmt.Printf("Read %d bytes\n", len(p)) + + // If we have buffered data, use it first + if r.buffer.Len() > 0 { + n, _ = r.buffer.Read(p) + r.bytesRead += int64(n) + + if r.onProgress.Truthy() { + r.onProgress.Invoke(r.bytesRead) + } + return n, nil + } + + // Only read from stream if buffer is empty + promise := r.reader.Call("read") + result := await(promise) + + if result.Get("done").Bool() { + if r.bytesRead < r.totalSize { + return 0, fmt.Errorf("stream ended prematurely at %d/%d bytes", r.bytesRead, r.totalSize) + } + return 0, io.EOF + } + + // Get the chunk from JavaScript and write it to our buffer + value := result.Get("value") + chunk := make([]byte, value.Length()) + js.CopyBytesToGo(chunk, value) + r.buffer.Write(chunk) + + // Now read what we can into p + n, _ = r.buffer.Read(p) + r.bytesRead += int64(n) + + if r.onProgress.Truthy() { + r.onProgress.Invoke(r.bytesRead) + } + + return n, nil +} + +// Helper function to await a JavaScript promise +func await(promise js.Value) js.Value { + done := make(chan js.Value) + promise.Call("then", js.FuncOf(func(_ js.Value, args []js.Value) interface{} { + done <- args[0] + return nil + })) + return <-done +} + +func (r *jsStreamReader) Close() error { + r.reader.Call("releaseLock") + return nil +} diff --git a/cmd/wush/cp.go b/cmd/wush/cp.go index 41f1372..481e3d2 100644 --- a/cmd/wush/cp.go +++ b/cmd/wush/cp.go @@ -9,15 +9,18 @@ import ( "net/http" "net/http/httputil" "net/netip" + "net/url" "os" "path/filepath" "strings" + "time" "github.com/charmbracelet/huh" "github.com/coder/serpent" "github.com/coder/wush/cliui" "github.com/coder/wush/overlay" "github.com/coder/wush/tsserver" + "github.com/pion/webrtc/v4" "github.com/schollz/progressbar/v3" "tailscale.com/net/netns" "tailscale.com/tailcfg" @@ -57,7 +60,13 @@ func initAuth(authFlag *string, ca *overlay.ClientAuth) serpent.MiddlewareFunc { } } - err := ca.Parse(strings.TrimSpace(*authFlag)) + // If the user provided a URL, extract the auth key from the fragment. + authKey := *authFlag + if u, err := url.Parse(*authFlag); err == nil && u.Fragment != "" { + authKey = u.Fragment + } + + err := ca.Parse(strings.TrimSpace(authKey)) if err != nil { return fmt.Errorf("parse auth key: %w", err) } @@ -133,10 +142,10 @@ func cpCmd() *serpent.Command { ) return &serpent.Command{ Use: "cp ", - Short: "Transfer files.", - Long: "Transfer files to a " + cliui.Code("wush") + " peer.\n" + formatExamples( + Short: "Transfer files to a wush server.", + Long: formatExamples( example{ - Description: "Copy a local file to the remote", + Description: "Copy a local file to the server", Command: "wush cp local-file.txt", }, ), @@ -164,6 +173,94 @@ func cpCmd() *serpent.Command { } go s.ListenAndServe(ctx) + + fiPath := inv.Args[0] + fiName := filepath.Base(inv.Args[0]) + + fi, err := os.Open(fiPath) + if err != nil { + return err + } + defer fi.Close() + + fiStat, err := fi.Stat() + if err != nil { + return err + } + + if send.Auth.Web { + meta := overlay.RtcMetadata{ + Type: overlay.RtcMetadataTypeFileMetadata, + FileMetadata: overlay.RtcFileMetadata{ + FileName: fiName, + FileSize: int(fiStat.Size()), + }, + } + + raw, err := json.Marshal(meta) + if err != nil { + panic(err) + } + + logf("Waiting for data channel to open...") + for { + if send.RtcDc.ReadyState() == webrtc.DataChannelStateOpen { + break + } + time.Sleep(100 * time.Millisecond) + } + logf("Data channel is open!") + + if err := send.RtcDc.SendText(string(raw)); err != nil { + panic(err) + } + + bar := progressbar.DefaultBytes( + fiStat.Size(), + fmt.Sprintf("Uploading %q", fiPath), + ) + barReader := progressbar.NewReader(fi, bar) + + buf := make([]byte, 16384) + + for { + n, err := barReader.Read(buf) + if err != nil && err != io.EOF { + return err + } + + if n > 0 { + if err := send.RtcDc.Send(buf[:n]); err != nil { + fmt.Println("failed to send file data: ", err) + return err + } + } + + if err == io.EOF { + break + } + } + + meta = overlay.RtcMetadata{ + Type: overlay.RtcMetadataTypeFileComplete, + } + + raw, err = json.Marshal(meta) + if err != nil { + panic(err) + } + + if err := send.RtcDc.SendText(string(raw)); err != nil { + fmt.Println("failed to send file complete message", err) + } + + select { + case <-send.WaitTransferDone: + logger.Info("received file transfer acknowledgment") + return nil + } + } + netns.SetDialerOverride(s.Dialer()) ts, err := newTSNet("send", verbose) if err != nil { @@ -191,20 +288,6 @@ func cpCmd() *serpent.Command { } } - fiPath := inv.Args[0] - fiName := filepath.Base(inv.Args[0]) - - fi, err := os.Open(fiPath) - if err != nil { - return err - } - defer fi.Close() - - fiStat, err := fi.Stat() - if err != nil { - return err - } - bar := progressbar.DefaultBytes( fiStat.Size(), fmt.Sprintf("Uploading %q", fiPath), diff --git a/cmd/wush/main.go b/cmd/wush/main.go index cf935a6..90dd767 100644 --- a/cmd/wush/main.go +++ b/cmd/wush/main.go @@ -17,25 +17,25 @@ func main() { var ( showVersion bool - fmtLong = "wush %s - peer-to-peer file transfers and shells\n" + fmtLong = "wush %s - WireGuard-powered peer-to-peer file transfer and shell\n" ) cmd := &serpent.Command{ Use: "wush ", Long: fmt.Sprintf(fmtLong, getBuildInfo().version) + formatExamples( example{ - Description: "Start the wush server", + Description: "Start the wush server to accept incoming connections", Command: "wush serve", }, example{ - Description: "Open a shell to the wush host", + Description: "Open a shell to a wush server", Command: "wush ssh", }, example{ - Description: "Transfer files to the wush host using rsync", + Description: "Transfer files to a wush server with rsync", Command: "wush rsync local-file.txt :/path/to/remote/file", }, example{ - Description: "Copy a single file to the host", + Description: "Copy a single file to a wush server", Command: "wush cp local-file.txt", }, ), diff --git a/cmd/wush/portforward.go b/cmd/wush/portforward.go index 6bed4b6..994eaba 100644 --- a/cmd/wush/portforward.go +++ b/cmd/wush/portforward.go @@ -41,33 +41,33 @@ func portForwardCmd() *serpent.Command { ) return &serpent.Command{ Use: "port-forward", - Short: "Transfer files.", - Long: "Transfer files to a " + cliui.Code("wush") + " peer.\n" + formatExamples( + Short: "Forward ports from the wush server.", + Long: formatExamples( example{ - Description: "Port forward a single TCP port from 1234 in the peer to port 5678 on your local machine", + Description: "Forward a single TCP port from 1234 on the server to port 5678 on your local machine", Command: "wush port-forward --tcp 5678:1234", }, example{ - Description: "Port forward a single UDP port from port 9000 to port 9000 on your local machine", + Description: "Forward a single UDP port", Command: "wush port-forward --udp 9000", }, example{ - Description: "Port forward multiple TCP ports and a UDP port", + Description: "Forward multiple TCP ports and a UDP port", Command: "wush port-forward --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53", }, example{ - Description: "Port forward multiple ports (TCP or UDP) in condensed syntax", + Description: "Forward multiple ports (TCP or UDP) in condensed syntax", Command: "wush port-forward --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012", }, example{ - Description: "Port forward specifying the local address to bind to", + Description: "Forward specifying the local address to bind", Command: "wush port-forward --tcp 1.2.3.4:8080:8080", }, ), Middleware: serpent.Chain( initLogger(&verbose, ptr.To(false), logger, &logf), initAuth(&overlayOpts.authKey, &overlayOpts.clientAuth), - derpMap(nil, dm), + derpMap(&derpmapFi, dm), sendOverlayMW(overlayOpts, &send, logger, dm, &logf), ), Handler: func(inv *serpent.Invocation) error { diff --git a/cmd/wush/rsync.go b/cmd/wush/rsync.go index 9e876a7..3873601 100644 --- a/cmd/wush/rsync.go +++ b/cmd/wush/rsync.go @@ -24,17 +24,16 @@ func rsyncCmd() *serpent.Command { ) return &serpent.Command{ Use: "rsync [flags] -- [rsync args]", - Short: "Transfer files over rsync.", - Long: "Runs rsync to transfer files to a " + cliui.Code("wush") + " peer. " + - "Use " + cliui.Code("wush serve") + " on the computer you would like to connect to." + + Short: "Transfer files with rsync to/from a wush server.", + Long: "Use " + cliui.Code("wush serve") + " on the computer you would like to transfer files to." + "\n\n" + formatExamples( example{ - Description: "Sync a local file to the remote", + Description: "Upload a local file", Command: "wush rsync /local/path :/remote/path", }, example{ - Description: "Download a remote file to the local computer", + Description: "Download a remote file", Command: "wush rsync :/remote/path /local/path", }, example{ @@ -62,8 +61,7 @@ func rsyncCmd() *serpent.Command { progPath, overlayOpts.clientAuth.AuthKey(), strings.Join(inv.Args, " "), ), } - fmt.Println(args) - fmt.Println("Running: rsync", strings.Join(inv.Args, " ")) + fmt.Println("Running rsync", strings.Join(inv.Args, " ")) cmd := exec.CommandContext(ctx, "sh", args...) cmd.Stdin = inv.Stdin cmd.Stdout = inv.Stdout diff --git a/cmd/wush/serve.go b/cmd/wush/serve.go index f82dce7..14cb614 100644 --- a/cmd/wush/serve.go +++ b/cmd/wush/serve.go @@ -44,10 +44,8 @@ func serveCmd() *serpent.Command { dm = new(tailcfg.DERPMap) ) return &serpent.Command{ - Use: "serve", - Aliases: []string{"host"}, - Short: "Run the wush server.", - Long: "Runs the wush server. Allows other wush CLIs to connect to this computer.", + Use: "serve", + Short: "Run the wush server. Allow wush clients to connect.", Middleware: serpent.Chain( derpMap(&derpmapFi, dm), ), @@ -86,8 +84,10 @@ func serveCmd() *serpent.Command { // Ensure we always print the auth key on stdout if isatty.IsTerminal(os.Stdout.Fd()) { hlog("Your auth key is:") - fmt.Println("\t>", cliui.Code(r.ClientAuth().AuthKey())) + fmt.Println(" >", cliui.Code(r.ClientAuth().AuthKey())) hlog("Use this key to authenticate other " + cliui.Code("wush") + " commands to this instance.") + hlog("Visit the following link to connect via the browser:") + fmt.Println(" >", cliui.Code("https://wush.dev#"+r.ClientAuth().AuthKey())) } else { fmt.Println(cliui.Code(r.ClientAuth().AuthKey())) hlog("The auth key has been printed to stdout") @@ -105,10 +105,13 @@ func serveCmd() *serpent.Command { return err } - ts.Up(ctx) + _, err = ts.Up(ctx) + if err != nil { + return fmt.Errorf("bring wireguard up: %w", err) + } fs := afero.NewOsFs() - hlog("WireGuard is ready") + // hlog("WireGuard is ready") closers := []io.Closer{} @@ -131,7 +134,7 @@ func serveCmd() *serpent.Command { closers = append(closers, sshListener) // TODO: replace these logs with all of the options in the beginning. - hlog("SSH server " + pretty.Sprint(cliui.DefaultStyles.Enabled, "enabled")) + // hlog("SSH server " + pretty.Sprint(cliui.DefaultStyles.Enabled, "enabled")) go func() { err := sshSrv.Serve(sshListener) if err != nil { @@ -149,7 +152,7 @@ func serveCmd() *serpent.Command { } closers = append([]io.Closer{cpListener}, closers...) - hlog("File transfer server " + pretty.Sprint(cliui.DefaultStyles.Enabled, "enabled")) + // hlog("File transfer server " + pretty.Sprint(cliui.DefaultStyles.Enabled, "enabled")) go func() { err := http.Serve(cpListener, http.HandlerFunc(cpHandler)) if err != nil { @@ -173,7 +176,7 @@ func serveCmd() *serpent.Command { bicopy(ctx, src, dst) }, true }) - hlog("Port-forward server " + pretty.Sprint(cliui.DefaultStyles.Enabled, "enabled")) + // hlog("Port-forward server " + pretty.Sprint(cliui.DefaultStyles.Enabled, "enabled")) } else { hlog("Port-forward server " + pretty.Sprint(cliui.DefaultStyles.Disabled, "disabled")) } diff --git a/cmd/wush/ssh.go b/cmd/wush/ssh.go index f972c59..4e7b86a 100644 --- a/cmd/wush/ssh.go +++ b/cmd/wush/ssh.go @@ -34,9 +34,8 @@ func sshCmd() *serpent.Command { return &serpent.Command{ Use: "ssh", Aliases: []string{}, - Short: "Open a shell.", - Long: "Opens an SSH connection to a " + cliui.Code("wush") + " peer. " + - "Use " + cliui.Code("wush serve") + " on the computer you would like to connect to.", + Short: "Open a SSH connection to a wush server.", + Long: "Use " + cliui.Code("wush serve") + " on the computer you would like to connect to.", Middleware: serpent.Chain( initLogger(&verbose, &quiet, logger, &logf), initAuth(&overlayOpts.authKey, &overlayOpts.clientAuth), @@ -87,7 +86,7 @@ func sshCmd() *serpent.Command { } } - return xssh.TailnetSSH(ctx, inv, ts, ip.String()+":3", quiet) + return xssh.TailnetSSH(ctx, inv, ts, netip.AddrPortFrom(ip, 3).String(), quiet) }, Options: []serpent.Option{ { diff --git a/cmd/wush/version.go b/cmd/wush/version.go index 263aace..9632258 100644 --- a/cmd/wush/version.go +++ b/cmd/wush/version.go @@ -10,7 +10,7 @@ import ( func versionCmd() *serpent.Command { cmd := &serpent.Command{ Use: "version", - Short: "Show wush version.", + Short: "Output the wush version.", Handler: func(inv *serpent.Invocation) error { bi := getBuildInfo() fmt.Printf("Wush %s-%s %s\n", bi.version, bi.commitHash[:7], bi.commitTime.Format(time.RFC1123)) diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f3cfe0f --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1728888510, + "narHash": "sha256-nsNdSldaAyu6PE3YUA+YQLqUDJh+gRbBooMMekZJwvI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a3c0b3b21515f74fd2665903d4ce6bc4dc81c77c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0100a5d --- /dev/null +++ b/flake.nix @@ -0,0 +1,28 @@ +{ + description = "Dev shell for Go backend and React frontend (using pnpm)"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + devShell = pkgs.mkShell + { + buildInputs = with pkgs; [ + go + nodejs + pnpm + binaryen # wasm-opt + ]; + + shellHook = '' + exec $SHELL + ''; + }; + }); +} diff --git a/go.mod b/go.mod index f3ab996..a507d63 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,12 @@ module github.com/coder/wush -go 1.22.6 +go 1.23.1 -replace tailscale.com => github.com/coadler/tailscale v1.1.1-0.20240926000438-059d0c1039af +toolchain go1.23.2 + +replace tailscale.com => github.com/coadler/tailscale v1.1.1-0.20241122221419-49dfbfcd5e09 + +// replace tailscale.com => /home/colin/Projects/coadler/tailscale replace github.com/gliderlabs/ssh => github.com/coder/ssh v0.0.0-20231128192721-70855dedb788 @@ -14,24 +18,26 @@ require ( github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 github.com/coder/serpent v0.8.0 github.com/go-chi/chi/v5 v5.1.0 - github.com/klauspost/compress v1.17.10 + github.com/google/uuid v1.6.0 + github.com/klauspost/compress v1.17.11 github.com/mattn/go-isatty v0.0.20 github.com/mitchellh/go-wordwrap v1.0.1 github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a github.com/pion/stun/v3 v3.0.0 - github.com/prometheus/client_golang v1.20.4 + github.com/pion/webrtc/v4 v4.0.1 + github.com/prometheus/client_golang v1.20.5 github.com/puzpuzpuz/xsync/v3 v3.4.0 - github.com/schollz/progressbar/v3 v3.16.0 + github.com/schollz/progressbar/v3 v3.16.1 github.com/spf13/afero v1.11.0 - github.com/valyala/fasthttp v1.56.0 + github.com/valyala/fasthttp v1.58.0 go4.org/mem v0.0.0-20220726221520-4f986261bf13 - golang.org/x/crypto v0.27.0 + golang.org/x/crypto v0.31.0 golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 - golang.org/x/net v0.29.0 - golang.org/x/sys v0.25.0 - golang.org/x/term v0.24.0 + golang.org/x/net v0.31.0 + golang.org/x/sys v0.28.0 + golang.org/x/term v0.27.0 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da - tailscale.com v1.70.0 + tailscale.com v1.76.1 ) require ( @@ -79,9 +85,10 @@ require ( github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect github.com/coder/terraform-provider-coder v1.0.2 // indirect + github.com/coder/websocket v1.8.12 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/coreos/go-oidc/v3 v3.11.0 // indirect - github.com/creack/pty v1.1.21 // indirect + github.com/creack/pty v1.1.23 // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -104,7 +111,6 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/csrf v1.7.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect @@ -121,7 +127,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect - github.com/illarion/gonotify v1.0.1 // indirect + github.com/illarion/gonotify/v2 v2.0.3 // indirect github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect @@ -152,10 +158,21 @@ require ( github.com/outcaste-io/ristretto v0.2.3 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect - github.com/pion/dtls/v3 v3.0.1 // indirect + github.com/pion/datachannel v1.5.9 // indirect + github.com/pion/dtls/v3 v3.0.3 // indirect + github.com/pion/ice/v4 v4.0.2 // indirect + github.com/pion/interceptor v0.1.37 // indirect github.com/pion/logging v0.2.2 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.14 // indirect + github.com/pion/rtp v1.8.9 // indirect + github.com/pion/sctp v1.8.33 // indirect + github.com/pion/sdp/v3 v3.0.9 // indirect + github.com/pion/srtp/v3 v3.0.4 // indirect github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/turn/v4 v4.0.0 // indirect github.com/pion/udp v0.1.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/sftp v1.13.6 // indirect @@ -176,15 +193,14 @@ require ( github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect - github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect + github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect - github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 // indirect + github.com/tailscale/wireguard-go v0.0.0-20241113014420-4e883d38c8d3 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/u-root/u-root v0.14.0 // indirect github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a // indirect - github.com/vishvananda/netlink v1.2.1-beta.2 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect @@ -194,19 +210,19 @@ require ( github.com/zclconf/go-cty v1.15.0 // indirect github.com/zeebo/errs v1.3.0 // indirect go.nhat.io/otelsql v0.14.0 // indirect - go.opentelemetry.io/otel v1.30.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/sdk v1.30.0 // indirect - go.opentelemetry.io/otel/trace v1.30.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/atomic v1.11.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.25.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect diff --git a/go.sum b/go.sum index bb0787f..c1f84a5 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/appsec-internal-go v1.7.0 h1:iKRNLih83dJeVya3IoUfK+6HLD/hQsIbyBlfvLmAeb0= @@ -48,8 +48,8 @@ github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= @@ -145,8 +145,8 @@ github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/coadler/tailscale v1.1.1-0.20240926000438-059d0c1039af h1:7h0hQxaizCT3u7Fu9b6k1NgGj4EHxx/K3H7YBAFanVE= -github.com/coadler/tailscale v1.1.1-0.20240926000438-059d0c1039af/go.mod h1:rRq+xvgprFys8sZbJgcAMMqpiP6r+Y75CJRhCRmXrd0= +github.com/coadler/tailscale v1.1.1-0.20241122221419-49dfbfcd5e09 h1:wPojSLHQAFdOWuM1qGslacPBy4G+2mBvrwasW5sRU1E= +github.com/coadler/tailscale v1.1.1-0.20241122221419-49dfbfcd5e09/go.mod h1:OQHJodEdJx4e8lKkSpYmK2H/B1D/VN0EU7g9imiGj2k= github.com/coder/coder/v2 v2.16.0 h1:+IzbcLU7YFUp6knJJhS4xw8yphuqrIUKt7mIk7LwUQA= github.com/coder/coder/v2 v2.16.0/go.mod h1:/kiN4IfNwd5T7xGEyVbp7jiNOVaHtdiIDyLqN9OqboE= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= @@ -157,12 +157,14 @@ github.com/coder/ssh v0.0.0-20231128192721-70855dedb788 h1:YoUSJ19E8AtuUFVYBpXuO github.com/coder/ssh v0.0.0-20231128192721-70855dedb788/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ= github.com/coder/terraform-provider-coder v1.0.2 h1:xKbnJF/XUxcUJlZoC3ZkNOj4PZvk5Stdkel2TCZluDQ= github.com/coder/terraform-provider-coder v1.0.2/go.mod h1:1f3EjO+DA9QcIbM7sBSk/Ffw3u7kh6vXNBIQfV59yUk= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= -github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= -github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= +github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -325,8 +327,8 @@ github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1 h1:jWoR2Yqg8tzM0v github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1/go.mod h1:B63hDJMhTupLWCHwopAyEo7wRFowx9kOc8m8j1sfOqE= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= -github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio= -github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= +github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A= +github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= @@ -345,8 +347,8 @@ github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4os github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= -github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= @@ -434,10 +436,30 @@ github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pion/dtls/v3 v3.0.1 h1:0kmoaPYLAo0md/VemjcrAXQiSf8U+tuU3nDYVNpEKaw= -github.com/pion/dtls/v3 v3.0.1/go.mod h1:dfIXcFkKoujDQ+jtd8M6RgqKK3DuaUilm3YatAbGp5k= +github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= +github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= +github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM= +github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU= +github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s= +github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg= +github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= +github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= +github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= +github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= +github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= +github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= +github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= +github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= +github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= @@ -445,8 +467,12 @@ github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQp github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= +github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8= github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us= +github.com/pion/webrtc/v4 v4.0.1 h1:6Unwc6JzoTsjxetcAIoWH81RUM4K5dBc1BbJGcF9WVE= +github.com/pion/webrtc/v4 v4.0.1/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= @@ -456,8 +482,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= -github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= @@ -481,8 +507,8 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= -github.com/schollz/progressbar/v3 v3.16.0 h1:+MbBim/cE9DqDb8UXRfLJ6RZdyDkXG1BDy/sWc5s0Mc= -github.com/schollz/progressbar/v3 v3.16.0/go.mod h1:lLiKjKJ9/yzc9Q8jk+sVLfxWxgXKsktvUf6TO+4Y2nw= +github.com/schollz/progressbar/v3 v3.16.1 h1:RnF1neWZFzLCoGx8yp1yF7SDl4AzNDI5y4I0aUJRrZQ= +github.com/schollz/progressbar/v3 v3.16.1/go.mod h1:I2ILR76gz5VXqYMIY/LdLecvMHDPVcQm3W/MSKi1TME= github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg= github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= @@ -524,16 +550,16 @@ github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPx github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= -github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk= -github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w= github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU= github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g= github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= -github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso= -github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/wireguard-go v0.0.0-20241113014420-4e883d38c8d3 h1:dmoPb3dG27tZgMtrvqfD/LW4w7gA6BSWl8prCPNmkCQ= +github.com/tailscale/wireguard-go v0.0.0-20241113014420-4e883d38c8d3/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= @@ -550,10 +576,8 @@ github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnG github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U= -github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI= -github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= -github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= +github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= @@ -585,10 +609,10 @@ github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtC go.nhat.io/otelsql v0.14.0 h1:Mz4xo+WVQLAOPZy6abxjVzZzNe8xoOUh/tOMJoxo3oo= go.nhat.io/otelsql v0.14.0/go.mod h1:iO9KfDBZO2WI6O7n+ippHe5OHdXQ5iiA2aIa3Kzywo8= go.opentelemetry.io/contrib v1.19.0 h1:rnYI7OEPMWFeM4QCqWQ3InMJ0arWMR1i0Cx9A5hcjYM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= -go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= -go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= @@ -597,14 +621,14 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.30.0 h1:IyFlqNsi8VT/nw go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.30.0/go.mod h1:bxiX8eUeKoAEQmbq/ecUT8UqZwCjZW52yJrXJUSozsk= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.30.0 h1:kn1BudCgwtE7PxLqcZkErpD8GKqLZ6BSzeW9QihQJeM= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.30.0/go.mod h1:ljkUDtAMdleoi9tIG1R6dJUpVwDcYjw3J2Q6Q/SuiC0= -go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= -go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792jZO1bo4BXkM= go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y= -go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= -go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -624,12 +648,12 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= -golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= -golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -652,8 +676,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -661,8 +685,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -700,8 +724,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -709,8 +733,8 @@ golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -721,8 +745,8 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -787,8 +811,8 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= -honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= -honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= +honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= +honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= diff --git a/overlay/auth.go b/overlay/auth.go index ccff82a..d337650 100644 --- a/overlay/auth.go +++ b/overlay/auth.go @@ -15,6 +15,7 @@ import ( ) type ClientAuth struct { + Web bool // OverlayPrivateKey is the main auth mechanism used to secure the overlay. // Peers are sent this private key to encrypt node communication to the // receiver. Leaking this private key would allow anyone to connect. @@ -49,6 +50,14 @@ func (ca *ClientAuth) PrintDebug(logf func(str string, args ...any), dm *tailcfg func (ca *ClientAuth) AuthKey() string { buf := bytes.NewBuffer(nil) + buf.WriteByte(1) + + if ca.Web { + buf.WriteByte(1) + } else { + buf.WriteByte(0) + } + buf.WriteByte(byte(ca.ReceiverStunAddr.Addr().BitLen() / 8)) if ca.ReceiverStunAddr.Addr().BitLen() > 0 { stunBytes, err := ca.ReceiverStunAddr.MarshalBinary() @@ -78,6 +87,24 @@ func (ca *ClientAuth) Parse(authKey string) error { decr := bytes.NewReader(base58.Decode(authKey)) + ver, err := decr.ReadByte() + if err != nil { + return errors.New("read authkey version") + } + + if ver != 1 { + return fmt.Errorf("unsupported authkey version %q", ver) + } + + typ, err := decr.ReadByte() + if err != nil { + return errors.New("read authkey peer type") + } + + if typ == 1 { + ca.Web = true + } + ipLenB, err := decr.ReadByte() if err != nil { return errors.New("read STUN ip len; invalid authkey") diff --git a/overlay/overlay.go b/overlay/overlay.go index 0cfacab..66f6066 100644 --- a/overlay/overlay.go +++ b/overlay/overlay.go @@ -3,16 +3,20 @@ package overlay import ( "net/netip" + "github.com/google/uuid" + "github.com/pion/webrtc/v4" "tailscale.com/tailcfg" ) +type Logf func(format string, args ...any) + // Overlay specifies the mechanism by which senders and receivers exchange // Tailscale nodes over a sidechannel. type Overlay interface { // listenOverlay(ctx context.Context, kind string) error Recv() <-chan *tailcfg.Node - Send() chan<- *tailcfg.Node - IP() netip.Addr + SendTailscaleNodeUpdate(node *tailcfg.Node) + IPs() []netip.Addr } type messageType int @@ -23,17 +27,31 @@ const ( messageTypeHello messageTypeHelloResponse messageTypeNodeUpdate + + messageTypeWebRTCOffer + messageTypeWebRTCAnswer + messageTypeWebRTCCandidate ) type overlayMessage struct { Typ messageType HostInfo HostInfo - IP netip.Addr Node tailcfg.Node + + WebrtcDescription *webrtc.SessionDescription + WebrtcCandidate *webrtc.ICECandidateInit } type HostInfo struct { Username string Hostname string } + +var TailscaleServicePrefix6 = [6]byte{0xfd, 0x7a, 0x11, 0x5c, 0xa1, 0xe0} + +func randv6() netip.Addr { + uid := uuid.New() + copy(uid[:], TailscaleServicePrefix6[:]) + return netip.AddrFrom16(uid) +} diff --git a/overlay/receive.go b/overlay/receive.go index 9bed57e..a6dde68 100644 --- a/overlay/receive.go +++ b/overlay/receive.go @@ -1,20 +1,27 @@ +//go:build !js && !wasm +// +build !js,!wasm + package overlay import ( "context" - "encoding/binary" "encoding/json" "errors" "fmt" + "io" "log/slog" "net" + "net/http" "net/netip" + "os" "sync" "sync/atomic" "time" "github.com/pion/stun/v3" + "github.com/pion/webrtc/v4" "github.com/puzpuzpuz/xsync/v3" + "github.com/schollz/progressbar/v3" "tailscale.com/derp" "tailscale.com/derp/derphttp" "tailscale.com/net/netcheck" @@ -27,17 +34,16 @@ import ( "github.com/coder/wush/cliui" ) -type Logf func(format string, args ...any) - func NewReceiveOverlay(logger *slog.Logger, hlog Logf, dm *tailcfg.DERPMap) *Receive { return &Receive{ - Logger: logger, - HumanLogf: hlog, - DerpMap: dm, - SelfPriv: key.NewNode(), - PeerPriv: key.NewNode(), - in: make(chan *tailcfg.Node, 8), - out: make(chan *tailcfg.Node, 8), + Logger: logger, + HumanLogf: hlog, + DerpMap: dm, + SelfPriv: key.NewNode(), + PeerPriv: key.NewNode(), + webrtcConns: xsync.NewMapOf[key.NodePublic, *webrtc.PeerConnection](), + in: make(chan *tailcfg.Node, 8), + out: make(chan *overlayMessage, 8), } } @@ -60,21 +66,69 @@ type Receive struct { // communication. derpRegionID uint16 - // nextPeerIP is a counter that assigns IP addresses to new peers in - // ascending order. It contains the last two bytes of an IPv4 address, - // 100.64.x.x. - nextPeerIP uint16 + webrtcConns *xsync.MapOf[key.NodePublic, *webrtc.PeerConnection] lastNode atomic.Pointer[tailcfg.Node] - in chan *tailcfg.Node - out chan *tailcfg.Node + // in funnels node updates from other peers to us + in chan *tailcfg.Node + // out fans out our node updates to peers + out chan *overlayMessage } -func (r *Receive) IP() netip.Addr { - return netip.AddrFrom4([4]byte{100, 64, 0, 0}) +func (r *Receive) IPs() []netip.Addr { + i6 := [16]byte{0xfd, 0x7a, 0x11, 0x5c, 0xa1, 0xe0} + i6[15] = 0x01 + return []netip.Addr{ + // netip.AddrFrom4([4]byte{100, 64, 0, 0}), + netip.AddrFrom16(i6), + } +} + +func getWebRTCConfig() webrtc.Configuration { + defaultConfig := webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{ + { + URLs: []string{"stun:stun.l.google.com:19302"}, + }, + }, + } + + resp, err := http.Get("https://wush.dev/api/iceConfig") + if err != nil { + fmt.Println("failed to get ice config:", err) + return defaultConfig + } + defer resp.Body.Close() + + var iceConfig struct { + IceServers []struct { + URLs []string `json:"urls"` + Username string `json:"username"` + Credential string `json:"credential"` + } `json:"iceServers"` + } + + if err := json.NewDecoder(resp.Body).Decode(&iceConfig); err != nil { + return defaultConfig + } + + config := webrtc.Configuration{ + ICEServers: make([]webrtc.ICEServer, len(iceConfig.IceServers)), + } + for i, server := range iceConfig.IceServers { + config.ICEServers[i] = webrtc.ICEServer{ + URLs: server.URLs, + Username: server.Username, + Credential: server.Credential, + CredentialType: webrtc.ICECredentialTypePassword, + } + } + + return config } func (r *Receive) PickDERPHome(ctx context.Context) error { + nm := netmon.NewStatic() nc := netcheck.Client{ NetMon: nm, @@ -111,10 +165,15 @@ func (r *Receive) Recv() <-chan *tailcfg.Node { return r.in } -func (r *Receive) Send() chan<- *tailcfg.Node { - return r.out +func (r *Receive) SendTailscaleNodeUpdate(node *tailcfg.Node) { + r.out <- &overlayMessage{ + Typ: messageTypeNodeUpdate, + Node: *node.Clone(), + } } +// gonna have to do something special for per-peer webrtc connections + func (r *Receive) ListenOverlaySTUN(ctx context.Context) (<-chan struct{}, error) { srvAddr, err := net.ResolveUDPAddr("udp4", "stun.l.google.com:19302") if err != nil { @@ -160,14 +219,13 @@ func (r *Receive) ListenOverlaySTUN(ctx context.Context) (<-chan struct{}, error select { case <-ctx.Done(): return - case node := <-r.out: - r.lastNode.Store(node) - raw, err := json.Marshal(overlayMessage{ - Typ: messageTypeNodeUpdate, - Node: *node, - }) + case msg := <-r.out: + if msg.Typ == messageTypeNodeUpdate { + r.lastNode.Store(&msg.Node) + } + raw, err := json.Marshal(msg) if err != nil { - panic("marshal node: " + err.Error()) + panic("marshal overlay msg: " + err.Error()) } sealed := r.SelfPriv.SealTo(r.PeerPriv.Public(), raw) @@ -234,7 +292,7 @@ func (r *Receive) ListenOverlaySTUN(ctx context.Context) (<-chan struct{}, error continue } - res, key, err := r.handleNextMessage(buf, "STUN") + res, key, err := r.handleNextMessage(key.NodePublic{}, buf, "STUN") if err != nil { r.HumanLogf("Failed to handle overlay message: %s", err.Error()) continue @@ -273,14 +331,13 @@ func (r *Receive) ListenOverlayDERP(ctx context.Context) error { select { case <-ctx.Done(): return - case node := <-r.out: - r.lastNode.Store(node) - raw, err := json.Marshal(overlayMessage{ - Typ: messageTypeNodeUpdate, - Node: *node, - }) + case msg := <-r.out: + if msg.Typ == messageTypeNodeUpdate { + r.lastNode.Store(&msg.Node) + } + raw, err := json.Marshal(msg) if err != nil { - panic("marshal node: " + err.Error()) + panic("marshal overlay msg: " + err.Error()) } sealed := r.SelfPriv.SealTo(r.PeerPriv.Public(), raw) @@ -304,9 +361,9 @@ func (r *Receive) ListenOverlayDERP(ctx context.Context) error { switch msg := msg.(type) { case derp.ReceivedPacket: - res, key, err := r.handleNextMessage(msg.Data, "DERP") + res, key, err := r.handleNextMessage(msg.Source, msg.Data, "DERP") if err != nil { - r.HumanLogf("Failed to handle overlay message: %s", err.Error()) + r.HumanLogf("Failed to handle overlay message from %s: %s", msg.Source.ShortString(), err.Error()) continue } @@ -323,7 +380,7 @@ func (r *Receive) ListenOverlayDERP(ctx context.Context) error { } } -func (r *Receive) handleNextMessage(msg []byte, system string) (resRaw []byte, nodeKey key.NodePublic, _ error) { +func (r *Receive) handleNextMessage(src key.NodePublic, msg []byte, system string) (resRaw []byte, nodeKey key.NodePublic, _ error) { cleartext, ok := r.SelfPriv.OpenFrom(r.PeerPriv.Public(), msg) if !ok { return nil, key.NodePublic{}, errors.New("message failed decryption") @@ -343,7 +400,6 @@ func (r *Receive) handleNextMessage(msg []byte, system string) (resRaw []byte, n // do nothing case messageTypeHello: res.Typ = messageTypeHelloResponse - res.IP = r.assignNextIP() username := "unknown" if u := ovMsg.HostInfo.Username; u != "" { username = u @@ -352,14 +408,34 @@ func (r *Receive) handleNextMessage(msg []byte, system string) (resRaw []byte, n if h := ovMsg.HostInfo.Hostname; h != "" { hostname = h } + if lastNode := r.lastNode.Load(); lastNode != nil { + res.Node = *lastNode + } + + if ovMsg.WebrtcDescription != nil { + r.setupWebrtcConnection(src, &res, *ovMsg.WebrtcDescription) + } + r.HumanLogf("%s Received connection request over %s from %s", cliui.Timestamp(time.Now()), system, cliui.Keyword(fmt.Sprintf("%s@%s", username, hostname))) case messageTypeNodeUpdate: - r.HumanLogf("%s Received updated node from %s", cliui.Timestamp(time.Now()), cliui.Code(ovMsg.Node.Key.String())) + r.Logger.Debug("received updated node", slog.String("node_key", ovMsg.Node.Key.String())) r.in <- &ovMsg.Node res.Typ = messageTypeNodeUpdate if lastNode := r.lastNode.Load(); lastNode != nil { res.Node = *lastNode } + + case messageTypeWebRTCCandidate: + pc, ok := r.webrtcConns.Load(src) + if !ok { + fmt.Println("got candidate for unknown connection") + break + } + + err := pc.AddICECandidate(*ovMsg.WebrtcCandidate) + if err != nil { + fmt.Println("failed to add ice candidate:", err) + } } if res.Typ == 0 { @@ -375,11 +451,124 @@ func (r *Receive) handleNextMessage(msg []byte, system string) (resRaw []byte, n return sealed, ovMsg.Node.Key, nil } -func (r *Receive) assignNextIP() netip.Addr { - r.nextPeerIP += 1 +func (r *Receive) setupWebrtcConnection(src key.NodePublic, res *overlayMessage, offer webrtc.SessionDescription) { + // Configure larger buffer sizes + settingEngine := webrtc.SettingEngine{} + // Set maximum message size to 16MB + settingEngine.SetSCTPMaxReceiveBufferSize(64 * 1024 * 1024) + + api := webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine)) + + // Use the custom API to create the peer connection + peerConnection, err := api.NewPeerConnection(getWebRTCConfig()) + if err != nil { + panic(err) + } + + peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { + switch s { + case webrtc.PeerConnectionStateConnected: + case webrtc.PeerConnectionStateDisconnected: + case webrtc.PeerConnectionStateFailed: + case webrtc.PeerConnectionStateClosed: + } + }) + + peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { + // Increase buffer sizes + d.SetBufferedAmountLowThreshold(65535) + + var ( + fi *os.File + bar *progressbar.ProgressBar + mw io.Writer + fiSize int + read int + ) + + d.OnMessage(func(msg webrtc.DataChannelMessage) { + if msg.IsString { + meta := RtcMetadata{} + + err := json.Unmarshal(msg.Data, &meta) + if err != nil { + fmt.Println("failed to unmarshal file metadata:") + d.Close() + return + } + + if meta.Type == RtcMetadataTypeFileMetadata { + fiSize = meta.FileMetadata.FileSize + fi, err = os.OpenFile(meta.FileMetadata.FileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) + if err != nil { + fmt.Println("failed to open file", err) + } + + bar = progressbar.DefaultBytes( + int64(fiSize), + fmt.Sprintf("Downloading %q", meta.FileMetadata.FileName), + ) + mw = io.MultiWriter(fi, bar) + } + + } else { + read += len(msg.Data) + if fi == nil { + fmt.Println("Error: Received binary data before file was opened") + d.Close() + return + } + + _, err := mw.Write(msg.Data) + if err != nil { + fmt.Printf("Failed to write file data: %v\n", err) + d.Close() + return + } + + if read >= fiSize { + bar.Close() + fmt.Printf("Successfully wrote file %s (%d bytes)\n", fi.Name(), read) + err := fi.Close() + if err != nil { + fmt.Printf("Error closing file: %v\n", err) + } + fi = nil + bar = nil + mw = nil + } + } + }) + }) + + peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { + if i == nil { + return + } + ic := i.ToJSON() + + r.out <- &overlayMessage{ + Typ: messageTypeWebRTCCandidate, + WebrtcCandidate: &ic, + } + }) + + err = peerConnection.SetRemoteDescription(offer) + if err != nil { + fmt.Println("failed to set remote description:", err) + } + + answer, err := peerConnection.CreateAnswer(nil) + if err != nil { + fmt.Println("failed to create answer:", err) + } + + err = peerConnection.SetLocalDescription(answer) + if err != nil { + fmt.Println("failed to set local description:", err) + } - addrBytes := [4]byte{100, 64, 0, 0} - binary.BigEndian.PutUint16(addrBytes[2:], r.nextPeerIP) + res.WebrtcDescription = &answer - return netip.AddrFrom4(addrBytes) + r.webrtcConns.Store(src, peerConnection) } diff --git a/overlay/send.go b/overlay/send.go index 2054040..84b3853 100644 --- a/overlay/send.go +++ b/overlay/send.go @@ -1,3 +1,6 @@ +//go:build !js && !wasm +// +build !js,!wasm + package overlay import ( @@ -10,10 +13,10 @@ import ( "net/netip" "os" "os/user" - "sync" "time" "github.com/coder/wush/cliui" + "github.com/pion/webrtc/v4" "tailscale.com/derp" "tailscale.com/derp/derphttp" "tailscale.com/net/netmon" @@ -22,12 +25,16 @@ import ( ) func NewSendOverlay(logger *slog.Logger, dm *tailcfg.DERPMap) *Send { - return &Send{ - derpMap: dm, - in: make(chan *tailcfg.Node, 8), - out: make(chan *tailcfg.Node, 8), - waitIP: make(chan struct{}), + s := &Send{ + derpMap: dm, + in: make(chan *tailcfg.Node, 8), + out: make(chan *overlayMessage, 8), + waitIce: make(chan struct{}), + WaitTransferDone: make(chan struct{}), + SelfIP: randv6(), } + s.setupWebrtcConnection() + return s } type Send struct { @@ -35,28 +42,33 @@ type Send struct { STUNIPOverride netip.Addr derpMap *tailcfg.DERPMap - // _ip is the ip we get from the receiver, which is our ip on the tailnet. - _ip netip.Addr - waitIP chan struct{} - waitIPOnce sync.Once + SelfIP netip.Addr Auth ClientAuth + RtcConn *webrtc.PeerConnection + RtcDc *webrtc.DataChannel + offer webrtc.SessionDescription + waitIce chan struct{} + WaitTransferDone chan struct{} + in chan *tailcfg.Node - out chan *tailcfg.Node + out chan *overlayMessage } -func (s *Send) IP() netip.Addr { - <-s.waitIP - return s._ip +func (s *Send) IPs() []netip.Addr { + return []netip.Addr{s.SelfIP} } func (s *Send) Recv() <-chan *tailcfg.Node { return s.in } -func (s *Send) Send() chan<- *tailcfg.Node { - return s.out +func (s *Send) SendTailscaleNodeUpdate(node *tailcfg.Node) { + s.out <- &overlayMessage{ + Typ: messageTypeNodeUpdate, + Node: *node.Clone(), + } } func (s *Send) ListenOverlaySTUN(ctx context.Context) error { @@ -88,14 +100,10 @@ func (s *Send) ListenOverlaySTUN(ctx context.Context) error { select { case <-ctx.Done(): return - case node := <-s.out: - msg := overlayMessage{ - Typ: messageTypeNodeUpdate, - Node: *node, - } + case msg := <-s.out: raw, err := json.Marshal(msg) if err != nil { - panic("marshal node: " + err.Error()) + panic("marshal overlay msg: " + err.Error()) } sealed := s.Auth.OverlayPrivateKey.SealTo(s.Auth.ReceiverPublicKey, raw) @@ -131,8 +139,6 @@ func (s *Send) ListenOverlaySTUN(ctx context.Context) error { s.Logger.Error("read from STUN; exiting", "err", err) return err } - _ = addr - fmt.Println("new UDP msg from", addr.String()) buf = buf[:n] @@ -174,14 +180,10 @@ func (s *Send) ListenOverlayDERP(ctx context.Context) error { select { case <-ctx.Done(): return - case node := <-s.out: - msg := overlayMessage{ - Typ: messageTypeNodeUpdate, - Node: *node, - } + case msg := <-s.out: raw, err := json.Marshal(msg) if err != nil { - panic("marshal node: " + err.Error()) + panic("marshal overlay msg: " + err.Error()) } sealed := s.Auth.OverlayPrivateKey.SealTo(s.Auth.ReceiverPublicKey, raw) @@ -242,6 +244,7 @@ func (s *Send) newHelloPacket() []byte { Username: username, Hostname: hostname, }, + WebrtcDescription: &s.offer, }) if err != nil { panic("marshal node: " + err.Error()) @@ -251,6 +254,21 @@ func (s *Send) newHelloPacket() []byte { return sealed } +const ( + RtcMetadataTypeFileMetadata = "file_metadata" + RtcMetadataTypeFileComplete = "file_complete" + RtcMetadataTypeFileAck = "file_ack" +) + +type RtcMetadata struct { + Type string `json:"type"` + FileMetadata RtcFileMetadata `json:"fileMetadata"` +} +type RtcFileMetadata struct { + FileName string `json:"fileName"` + FileSize int `json:"fileSize"` +} + func (s *Send) handleNextMessage(msg []byte) (resRaw []byte, _ error) { cleartext, ok := s.Auth.OverlayPrivateKey.OpenFrom(s.Auth.ReceiverPublicKey, msg) if !ok { @@ -270,13 +288,13 @@ func (s *Send) handleNextMessage(msg []byte) (resRaw []byte, _ error) { case messageTypePong: // do nothing case messageTypeHelloResponse: - s._ip = ovMsg.IP - s.waitIPOnce.Do(func() { - close(s.waitIP) - }) - // fmt.Println("Received IP from peer:", s._ip.String()) + s.in <- &ovMsg.Node + close(s.waitIce) + s.RtcConn.SetRemoteDescription(*ovMsg.WebrtcDescription) case messageTypeNodeUpdate: s.in <- &ovMsg.Node + case messageTypeWebRTCCandidate: + s.RtcConn.AddICECandidate(*ovMsg.WebrtcCandidate) } if res.Typ == 0 { @@ -291,3 +309,60 @@ func (s *Send) handleNextMessage(msg []byte) (resRaw []byte, _ error) { sealed := s.Auth.OverlayPrivateKey.SealTo(s.Auth.ReceiverPublicKey, raw) return sealed, nil } + +func (s *Send) setupWebrtcConnection() { + var err error + s.RtcConn, err = webrtc.NewPeerConnection(getWebRTCConfig()) + if err != nil { + panic("failed to create webrtc connection: " + err.Error()) + } + + s.RtcConn.OnICECandidate(func(i *webrtc.ICECandidate) { + if i == nil { + return + } + ic := i.ToJSON() + + <-s.waitIce + s.out <- &overlayMessage{ + Typ: messageTypeWebRTCCandidate, + WebrtcCandidate: &ic, + } + }) + + s.RtcDc, err = s.RtcConn.CreateDataChannel("fileTransfer", nil) + if err != nil { + fmt.Println("failed to create dc:", err) + } + + // Add message handler to our created data channel + s.RtcDc.OnMessage(func(msg webrtc.DataChannelMessage) { + if msg.IsString { + meta := RtcMetadata{} + + err := json.Unmarshal(msg.Data, &meta) + if err != nil { + fmt.Println("failed to unmarshal metadata:", err) + return + } + + if meta.Type == RtcMetadataTypeFileAck { + close(s.WaitTransferDone) + return + } + return + } + }) + + answer, err := s.RtcConn.CreateOffer(nil) + if err != nil { + fmt.Println("failed to create answer:", err) + } + + err = s.RtcConn.SetLocalDescription(answer) + if err != nil { + fmt.Println("failed to set local description:", err) + } + + s.offer = answer +} diff --git a/overlay/wasm_js.go b/overlay/wasm_js.go new file mode 100644 index 0000000..97241ee --- /dev/null +++ b/overlay/wasm_js.go @@ -0,0 +1,519 @@ +//go:build js && wasm + +package overlay + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "net/netip" + "sync" + "sync/atomic" + "syscall/js" + "time" + + "github.com/coder/wush/cliui" + "github.com/pion/webrtc/v4" + "github.com/puzpuzpuz/xsync/v3" + "tailscale.com/derp" + "tailscale.com/derp/derphttp" + "tailscale.com/net/netcheck" + "tailscale.com/net/netmon" + "tailscale.com/net/portmapper" + "tailscale.com/tailcfg" + "tailscale.com/types/key" + "tailscale.com/types/logger" +) + +func NewWasmOverlay(hlog Logf, dm *tailcfg.DERPMap, + onNewPeer js.Value, + onWebrtcOffer js.Value, + onWebrtcAnswer js.Value, + onWebrtcCandidate js.Value, +) *Wasm { + return &Wasm{ + HumanLogf: hlog, + DerpMap: dm, + SelfPriv: key.NewNode(), + PeerPriv: key.NewNode(), + SelfIP: randv6(), + + onNewPeer: onNewPeer, + onWebrtcOffer: onWebrtcOffer, + onWebrtcAnswer: onWebrtcAnswer, + onWebrtcCandidate: onWebrtcCandidate, + + in: make(chan *tailcfg.Node, 8), + out: make(chan *overlayMessage, 8), + } +} + +type Wasm struct { + Logger *slog.Logger + HumanLogf Logf + DerpMap *tailcfg.DERPMap + // SelfPriv is the private key that peers will encrypt overlay messages to. + // The public key of this is sent in the auth key. + SelfPriv key.NodePrivate + // PeerPriv is the main auth mechanism used to secure the overlay. Peers are + // sent this private key to encrypt node communication. Leaking this private + // key would allow anyone to connect. + PeerPriv key.NodePrivate + SelfIP netip.Addr + + // username is a randomly generated human-readable string displayed on + // wush.dev to identify clients. + username string + + // DerpRegionID is the DERP region that can be used for proxied overlay + // communication. + DerpRegionID uint16 + DerpLatency time.Duration + + activePeer atomic.Pointer[chan *overlayMessage] + onNewPeer js.Value + + onWebrtcOffer js.Value + onWebrtcAnswer js.Value + onWebrtcCandidate js.Value + + lastNode atomic.Pointer[tailcfg.Node] + // in funnels node updates from other peers to us + in chan *tailcfg.Node + // out fans out our node updates to peers connected to us + out chan *overlayMessage +} + +func (r *Wasm) IPs() []netip.Addr { + return []netip.Addr{r.SelfIP} +} + +func (r *Wasm) PickDERPHome(ctx context.Context) error { + nm := netmon.NewStatic() + nc := netcheck.Client{ + NetMon: nm, + PortMapper: portmapper.NewClient(func(format string, args ...any) {}, nm, nil, nil, nil), + Logf: func(format string, args ...any) {}, + } + + report, err := nc.GetReport(ctx, r.DerpMap, nil) + if err != nil { + return err + } + + if report.PreferredDERP == 0 { + r.HumanLogf("Failed to determine overlay DERP region, defaulting to %s.", cliui.Code("NYC")) + r.DerpRegionID = 1 + r.DerpLatency = report.RegionLatency[1] + } else { + r.HumanLogf("Picked DERP region %s as overlay home", cliui.Code(r.DerpMap.Regions[report.PreferredDERP].RegionName)) + r.DerpRegionID = uint16(report.PreferredDERP) + r.DerpLatency = report.RegionLatency[report.PreferredDERP] + } + + return nil +} + +func (r *Wasm) ClientAuth() *ClientAuth { + return &ClientAuth{ + Web: true, + OverlayPrivateKey: r.PeerPriv, + ReceiverPublicKey: r.SelfPriv.Public(), + ReceiverDERPRegionID: r.DerpRegionID, + } +} + +func (r *Wasm) Recv() <-chan *tailcfg.Node { + return r.in +} + +func (r *Wasm) SendTailscaleNodeUpdate(node *tailcfg.Node) { + r.out <- &overlayMessage{ + Typ: messageTypeNodeUpdate, + Node: *node.Clone(), + } +} + +func (r *Wasm) SendWebrtcCandidate(peer string, cand webrtc.ICECandidateInit) { + fmt.Println("go: sending webrtc candidate") + r.out <- &overlayMessage{ + Typ: messageTypeWebRTCCandidate, + WebrtcCandidate: &cand, + } +} + +type Peer struct { + ID string + Name string + IP netip.Addr + Type string +} + +func (r *Wasm) Connect(ctx context.Context, ca ClientAuth, offer webrtc.SessionDescription) (Peer, error) { + derpPriv := key.NewNode() + c := derphttp.NewRegionClient(derpPriv, logger.Logf(r.HumanLogf), netmon.NewStatic(), func() *tailcfg.DERPRegion { + return r.DerpMap.Regions[int(ca.ReceiverDERPRegionID)] + }) + + err := c.Connect(ctx) + if err != nil { + return Peer{}, err + } + + sealed := r.newHelloPacket(ca, offer) + err = c.Send(ca.ReceiverPublicKey, sealed) + if err != nil { + return Peer{}, fmt.Errorf("send overlay hello over derp: %w", err) + } + + updates := make(chan *overlayMessage, 8) + + old := r.activePeer.Swap(&updates) + if old != nil { + close(*old) + } + + go func() { + defer r.activePeer.CompareAndSwap(&updates, nil) + defer c.Close() + defer fmt.Println("closing send goroutine") + + for { + select { + case <-ctx.Done(): + return + case msg, ok := <-updates: + if !ok { + return + } + + raw, err := json.Marshal(msg) + if err != nil { + panic("marshal overlay msg: " + err.Error()) + } + + sealed := ca.OverlayPrivateKey.SealTo(ca.ReceiverPublicKey, raw) + err = c.Send(ca.ReceiverPublicKey, sealed) + if err != nil { + fmt.Println("send response over derp:", err) + return + } + fmt.Println("sent message to connected peer") + } + } + }() + + waitHello := make(chan struct{}) + closeOnce := sync.Once{} + helloResp := overlayMessage{} + helloSrc := key.NodePublic{} + + go func() { + for { + msg, err := c.Recv() + if err != nil { + fmt.Println("Recv derp:", err) + return + } + + switch msg := msg.(type) { + case derp.ReceivedPacket: + if ca.ReceiverPublicKey != msg.Source { + fmt.Printf("message from unknown peer %s\n", msg.Source.String()) + continue + } + + res, _, ovmsg, err := r.handleNextMessage(msg.Source, ca.OverlayPrivateKey, ca.ReceiverPublicKey, msg.Data) + if err != nil { + fmt.Println("Failed to handle overlay message:", err) + continue + } + + if res != nil { + err = c.Send(msg.Source, res) + if err != nil { + fmt.Println(cliui.Timestamp(time.Now()), "Failed to send overlay response over derp:", err.Error()) + return + } + } + + if ovmsg.Typ == messageTypeHelloResponse { + helloResp = ovmsg + helloSrc = msg.Source + closeOnce.Do(func() { + close(waitHello) + }) + } + } + } + }() + + select { + case <-time.After(10 * time.Second): + c.Close() + return Peer{}, errors.New("timed out waiting for peer to respond") + case <-waitHello: + updates <- &overlayMessage{ + Typ: messageTypeNodeUpdate, + Node: *r.lastNode.Load(), + } + if len(helloResp.Node.Addresses) == 0 { + return Peer{}, fmt.Errorf("peer has no addresses") + } + ip := helloResp.Node.Addresses[0].Addr() + typ := "cli" + if ca.Web { + typ = "web" + } + return Peer{ + ID: helloSrc.String(), + IP: ip, + Name: helloResp.HostInfo.Username, + Type: typ, + }, nil + } +} + +func (r *Wasm) ListenOverlayDERP(ctx context.Context) error { + c := derphttp.NewRegionClient(r.SelfPriv, func(format string, args ...any) {}, netmon.NewStatic(), func() *tailcfg.DERPRegion { + return r.DerpMap.Regions[int(r.DerpRegionID)] + }) + defer c.Close() + + err := c.Connect(ctx) + if err != nil { + return err + } + + // node pub -> derp pub + peers := xsync.NewMapOf[key.NodePublic, key.NodePublic]() + + go func() { + for { + + select { + case <-ctx.Done(): + return + case msg := <-r.out: + if msg.Typ == messageTypeNodeUpdate { + r.lastNode.Store(&msg.Node) + } + raw, err := json.Marshal(msg) + if err != nil { + panic("marshal overlay msg: " + err.Error()) + } + + sealed := r.SelfPriv.SealTo(r.PeerPriv.Public(), raw) + // range over peers that have connected to us + peers.Range(func(_, derpKey key.NodePublic) bool { + fmt.Println("sending node to inbound peer") + err = c.Send(derpKey, sealed) + if err != nil { + r.HumanLogf("Send updated node over DERP: %s", err) + return false + } + return true + }) + if selectedPeer := r.activePeer.Load(); selectedPeer != nil { + *selectedPeer <- msg + fmt.Println("sending message") + } + } + } + }() + + for { + msg, err := c.Recv() + if err != nil { + fmt.Println("Recv derp:", err) + return err + } + + switch msg := msg.(type) { + case derp.ReceivedPacket: + res, key, _, err := r.handleNextMessage(msg.Source, r.SelfPriv, r.PeerPriv.Public(), msg.Data) + if err != nil { + r.HumanLogf("Failed to handle overlay message: %s", err.Error()) + continue + } + + if !key.IsZero() { + peers.Store(key, msg.Source) + } + + if res != nil { + err = c.Send(msg.Source, res) + if err != nil { + r.HumanLogf("Failed to send overlay response over derp: %s", err.Error()) + return err + } + } + } + } +} + +func (r *Wasm) newHelloPacket(ca ClientAuth, offer webrtc.SessionDescription) []byte { + var ( + username string = r.username + hostname string = "wush.dev" + ) + + raw, err := json.Marshal(overlayMessage{ + Typ: messageTypeHello, + HostInfo: HostInfo{ + Username: username, + Hostname: hostname, + }, + Node: *r.lastNode.Load(), + WebrtcDescription: &offer, + }) + if err != nil { + panic("marshal node: " + err.Error()) + } + + sealed := ca.OverlayPrivateKey.SealTo(ca.ReceiverPublicKey, raw) + return sealed +} + +func (r *Wasm) handleNextMessage(derpPub key.NodePublic, selfPriv key.NodePrivate, peerPub key.NodePublic, msg []byte) (resRaw []byte, nodeKey key.NodePublic, _ overlayMessage, _ error) { + cleartext, ok := selfPriv.OpenFrom(peerPub, msg) + if !ok { + return nil, key.NodePublic{}, overlayMessage{}, errors.New("message failed decryption") + } + + var ovMsg overlayMessage + fmt.Println(string(cleartext)) + err := json.Unmarshal(cleartext, &ovMsg) + if err != nil { + fmt.Printf("Unmarshal error: %#v\n", err) + panic("unmarshal node: " + err.Error()) + } + + res := overlayMessage{} + switch ovMsg.Typ { + case messageTypePing: + res.Typ = messageTypePong + case messageTypePong: + // do nothing + case messageTypeHello: + res.Typ = messageTypeHelloResponse + res.HostInfo.Username = r.username + res.HostInfo.Hostname = "wush.dev" + username := "unknown" + if u := ovMsg.HostInfo.Username; u != "" { + username = u + } + hostname := "unknown" + if h := ovMsg.HostInfo.Hostname; h != "" { + hostname = h + } + if node := r.lastNode.Load(); node != nil { + res.Node = *node + } + r.HumanLogf("%s Received connection request from %s", cliui.Timestamp(time.Now()), cliui.Keyword(fmt.Sprintf("%s@%s", username, hostname))) + // TODO: impl + r.onNewPeer.Invoke(map[string]any{ + "id": js.ValueOf(derpPub.String()), + "name": js.ValueOf("test"), + "ip": js.ValueOf("1.2.3.4"), + "cancel": js.FuncOf(func(this js.Value, args []js.Value) any { + return nil + }), + }) + + if ovMsg.WebrtcDescription != nil { + r.handleWebrtcOffer(derpPub, &res, *ovMsg.WebrtcDescription) + } + + case messageTypeHelloResponse: + if !ovMsg.Node.Key.IsZero() { + r.in <- &ovMsg.Node + } + + if ovMsg.WebrtcDescription != nil { + r.onWebrtcAnswer.Invoke(js.ValueOf(derpPub.String()), map[string]any{ + "type": js.ValueOf(ovMsg.WebrtcDescription.Type.String()), + "sdp": js.ValueOf(ovMsg.WebrtcDescription.SDP), + }) + } + + case messageTypeNodeUpdate: + r.HumanLogf("%s Received updated node from %s", cliui.Timestamp(time.Now()), cliui.Code(ovMsg.Node.Key.String())) + if !ovMsg.Node.Key.IsZero() { + r.in <- &ovMsg.Node + } + + case messageTypeWebRTCOffer: + res.Typ = messageTypeWebRTCAnswer + r.handleWebrtcOffer(derpPub, &res, *ovMsg.WebrtcDescription) + + case messageTypeWebRTCAnswer: + r.onWebrtcAnswer.Invoke(js.ValueOf(derpPub.String()), js.ValueOf(map[string]any{ + "type": js.ValueOf(ovMsg.WebrtcDescription.Type.String()), + "sdp": js.ValueOf(ovMsg.WebrtcDescription.SDP), + })) + + case messageTypeWebRTCCandidate: + cand := map[string]any{ + "candidate": js.ValueOf(ovMsg.WebrtcCandidate.Candidate), + } + if ovMsg.WebrtcCandidate.SDPMLineIndex != nil { + cand["sdpMLineIndex"] = js.ValueOf(int(*ovMsg.WebrtcCandidate.SDPMLineIndex)) + } + if ovMsg.WebrtcCandidate.SDPMid != nil { + cand["sdpMid"] = js.ValueOf(*ovMsg.WebrtcCandidate.SDPMid) + } + if ovMsg.WebrtcCandidate.UsernameFragment != nil { + cand["usernameFragment"] = js.ValueOf(*ovMsg.WebrtcCandidate.UsernameFragment) + } + + r.onWebrtcCandidate.Invoke(derpPub.String(), cand) + + } + + if res.Typ == 0 { + return nil, ovMsg.Node.Key, ovMsg, nil + } + + raw, err := json.Marshal(res) + if err != nil { + panic("marshal node: " + err.Error()) + } + + sealed := selfPriv.SealTo(peerPub, raw) + return sealed, ovMsg.Node.Key, ovMsg, nil +} + +func (r *Wasm) handleWebrtcOffer(derpPub key.NodePublic, res *overlayMessage, offer webrtc.SessionDescription) { + wait := make(chan struct{}) + + then := js.FuncOf(func(this js.Value, args []js.Value) interface{} { + defer close(wait) + desc := args[0] + + fmt.Printf("desc %#v\n", desc) + res.WebrtcDescription = &webrtc.SessionDescription{} + res.WebrtcDescription.Type = webrtc.NewSDPType(desc.Get("type").String()) + res.WebrtcDescription.SDP = desc.Get("sdp").String() + + return nil + }) + defer then.Release() + catch := js.FuncOf(func(this js.Value, args []js.Value) interface{} { + defer close(wait) + err := args[0] + errStr := err.Call("toString").String() + + fmt.Println("rtc offer callback failed:", errStr) + return nil + }) + defer catch.Release() + + r.onWebrtcOffer.Invoke(js.ValueOf(derpPub.String()), map[string]any{ + "type": js.ValueOf(offer.Type.String()), + "sdp": js.ValueOf(offer.SDP), + }).Call("then", then).Call("catch", catch) + <-wait +} diff --git a/site/.gitignore b/site/.gitignore deleted file mode 100644 index 80ec311..0000000 --- a/site/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules - -/.cache -/build -.env diff --git a/site/app/assets/wasm_exec.js b/site/app/assets/wasm_exec.js deleted file mode 100644 index 8bc1520..0000000 --- a/site/app/assets/wasm_exec.js +++ /dev/null @@ -1,668 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -"use strict"; - -(() => { - const enosys = () => { - const err = new Error("not implemented"); - err.code = "ENOSYS"; - return err; - }; - - if (!globalThis.fs) { - let outputBuf = ""; - globalThis.fs = { - constants: { - O_WRONLY: -1, - O_RDWR: -1, - O_CREAT: -1, - O_TRUNC: -1, - O_APPEND: -1, - O_EXCL: -1, - }, // unused - writeSync(fd, buf) { - outputBuf += decoder.decode(buf); - const nl = outputBuf.lastIndexOf("\n"); - if (nl != -1) { - console.log(outputBuf.substring(0, nl)); - outputBuf = outputBuf.substring(nl + 1); - } - return buf.length; - }, - write(fd, buf, offset, length, position, callback) { - if (offset !== 0 || length !== buf.length || position !== null) { - callback(enosys()); - return; - } - const n = this.writeSync(fd, buf); - callback(null, n); - }, - chmod(path, mode, callback) { - callback(enosys()); - }, - chown(path, uid, gid, callback) { - callback(enosys()); - }, - close(fd, callback) { - callback(enosys()); - }, - fchmod(fd, mode, callback) { - callback(enosys()); - }, - fchown(fd, uid, gid, callback) { - callback(enosys()); - }, - fstat(fd, callback) { - callback(enosys()); - }, - fsync(fd, callback) { - callback(null); - }, - ftruncate(fd, length, callback) { - callback(enosys()); - }, - lchown(path, uid, gid, callback) { - callback(enosys()); - }, - link(path, link, callback) { - callback(enosys()); - }, - lstat(path, callback) { - callback(enosys()); - }, - mkdir(path, perm, callback) { - callback(enosys()); - }, - open(path, flags, mode, callback) { - callback(enosys()); - }, - read(fd, buffer, offset, length, position, callback) { - callback(enosys()); - }, - readdir(path, callback) { - callback(enosys()); - }, - readlink(path, callback) { - callback(enosys()); - }, - rename(from, to, callback) { - callback(enosys()); - }, - rmdir(path, callback) { - callback(enosys()); - }, - stat(path, callback) { - callback(enosys()); - }, - symlink(path, link, callback) { - callback(enosys()); - }, - truncate(path, length, callback) { - callback(enosys()); - }, - unlink(path, callback) { - callback(enosys()); - }, - utimes(path, atime, mtime, callback) { - callback(enosys()); - }, - }; - } - - if (!globalThis.process) { - globalThis.process = { - getuid() { - return -1; - }, - getgid() { - return -1; - }, - geteuid() { - return -1; - }, - getegid() { - return -1; - }, - getgroups() { - throw enosys(); - }, - pid: -1, - ppid: -1, - umask() { - throw enosys(); - }, - cwd() { - throw enosys(); - }, - chdir() { - throw enosys(); - }, - }; - } - - if (!globalThis.crypto) { - throw new Error( - "globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)" - ); - } - - if (!globalThis.performance) { - throw new Error( - "globalThis.performance is not available, polyfill required (performance.now only)" - ); - } - - if (!globalThis.TextEncoder) { - throw new Error( - "globalThis.TextEncoder is not available, polyfill required" - ); - } - - if (!globalThis.TextDecoder) { - throw new Error( - "globalThis.TextDecoder is not available, polyfill required" - ); - } - - const encoder = new TextEncoder("utf-8"); - const decoder = new TextDecoder("utf-8"); - - globalThis.Go = class { - constructor() { - this.argv = ["js"]; - this.env = {}; - this.exit = (code) => { - if (code !== 0) { - console.warn("exit code:", code); - } - }; - this._exitPromise = new Promise((resolve) => { - this._resolveExitPromise = resolve; - }); - this._pendingEvent = null; - this._scheduledTimeouts = new Map(); - this._nextCallbackTimeoutID = 1; - - const setInt64 = (addr, v) => { - this.mem.setUint32(addr + 0, v, true); - this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); - }; - - const setInt32 = (addr, v) => { - this.mem.setUint32(addr + 0, v, true); - }; - - const getInt64 = (addr) => { - const low = this.mem.getUint32(addr + 0, true); - const high = this.mem.getInt32(addr + 4, true); - return low + high * 4294967296; - }; - - const loadValue = (addr) => { - const f = this.mem.getFloat64(addr, true); - if (f === 0) { - return undefined; - } - if (!isNaN(f)) { - return f; - } - - const id = this.mem.getUint32(addr, true); - return this._values[id]; - }; - - const storeValue = (addr, v) => { - const nanHead = 0x7ff80000; - - if (typeof v === "number" && v !== 0) { - if (isNaN(v)) { - this.mem.setUint32(addr + 4, nanHead, true); - this.mem.setUint32(addr, 0, true); - return; - } - this.mem.setFloat64(addr, v, true); - return; - } - - if (v === undefined) { - this.mem.setFloat64(addr, 0, true); - return; - } - - let id = this._ids.get(v); - if (id === undefined) { - id = this._idPool.pop(); - if (id === undefined) { - id = this._values.length; - } - this._values[id] = v; - this._goRefCounts[id] = 0; - this._ids.set(v, id); - } - this._goRefCounts[id]++; - let typeFlag = 0; - switch (typeof v) { - case "object": - if (v !== null) { - typeFlag = 1; - } - break; - case "string": - typeFlag = 2; - break; - case "symbol": - typeFlag = 3; - break; - case "function": - typeFlag = 4; - break; - } - this.mem.setUint32(addr + 4, nanHead | typeFlag, true); - this.mem.setUint32(addr, id, true); - }; - - const loadSlice = (addr) => { - const array = getInt64(addr + 0); - const len = getInt64(addr + 8); - return new Uint8Array(this._inst.exports.mem.buffer, array, len); - }; - - const loadSliceOfValues = (addr) => { - const array = getInt64(addr + 0); - const len = getInt64(addr + 8); - const a = new Array(len); - for (let i = 0; i < len; i++) { - a[i] = loadValue(array + i * 8); - } - return a; - }; - - const loadString = (addr) => { - const saddr = getInt64(addr + 0); - const len = getInt64(addr + 8); - return decoder.decode( - new DataView(this._inst.exports.mem.buffer, saddr, len) - ); - }; - - const timeOrigin = Date.now() - performance.now(); - this.importObject = { - _gotest: { - add: (a, b) => a + b, - }, - gojs: { - // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) - // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported - // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). - // This changes the SP, thus we have to update the SP used by the imported function. - - // func wasmExit(code int32) - "runtime.wasmExit": (sp) => { - sp >>>= 0; - const code = this.mem.getInt32(sp + 8, true); - this.exited = true; - delete this._inst; - delete this._values; - delete this._goRefCounts; - delete this._ids; - delete this._idPool; - this.exit(code); - }, - - // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) - "runtime.wasmWrite": (sp) => { - sp >>>= 0; - const fd = getInt64(sp + 8); - const p = getInt64(sp + 16); - const n = this.mem.getInt32(sp + 24, true); - fs.writeSync( - fd, - new Uint8Array(this._inst.exports.mem.buffer, p, n) - ); - }, - - // func resetMemoryDataView() - "runtime.resetMemoryDataView": (sp) => { - sp >>>= 0; - this.mem = new DataView(this._inst.exports.mem.buffer); - }, - - // func nanotime1() int64 - "runtime.nanotime1": (sp) => { - sp >>>= 0; - setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); - }, - - // func walltime() (sec int64, nsec int32) - "runtime.walltime": (sp) => { - sp >>>= 0; - const msec = new Date().getTime(); - setInt64(sp + 8, msec / 1000); - this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); - }, - - // func scheduleTimeoutEvent(delay int64) int32 - "runtime.scheduleTimeoutEvent": (sp) => { - sp >>>= 0; - const id = this._nextCallbackTimeoutID; - this._nextCallbackTimeoutID++; - this._scheduledTimeouts.set( - id, - setTimeout( - () => { - this._resume(); - while (this._scheduledTimeouts.has(id)) { - // for some reason Go failed to register the timeout event, log and try again - // (temporary workaround for https://github.com/golang/go/issues/28975) - console.warn("scheduleTimeoutEvent: missed timeout event"); - this._resume(); - } - }, - getInt64(sp + 8) - ) - ); - this.mem.setInt32(sp + 16, id, true); - }, - - // func clearTimeoutEvent(id int32) - "runtime.clearTimeoutEvent": (sp) => { - sp >>>= 0; - const id = this.mem.getInt32(sp + 8, true); - clearTimeout(this._scheduledTimeouts.get(id)); - this._scheduledTimeouts.delete(id); - }, - - // func getRandomData(r []byte) - "runtime.getRandomData": (sp) => { - sp >>>= 0; - crypto.getRandomValues(loadSlice(sp + 8)); - }, - - // func finalizeRef(v ref) - "syscall/js.finalizeRef": (sp) => { - sp >>>= 0; - const id = this.mem.getUint32(sp + 8, true); - this._goRefCounts[id]--; - if (this._goRefCounts[id] === 0) { - const v = this._values[id]; - this._values[id] = null; - this._ids.delete(v); - this._idPool.push(id); - } - }, - - // func stringVal(value string) ref - "syscall/js.stringVal": (sp) => { - sp >>>= 0; - storeValue(sp + 24, loadString(sp + 8)); - }, - - // func valueGet(v ref, p string) ref - "syscall/js.valueGet": (sp) => { - sp >>>= 0; - const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 32, result); - }, - - // func valueSet(v ref, p string, x ref) - "syscall/js.valueSet": (sp) => { - sp >>>= 0; - Reflect.set( - loadValue(sp + 8), - loadString(sp + 16), - loadValue(sp + 32) - ); - }, - - // func valueDelete(v ref, p string) - "syscall/js.valueDelete": (sp) => { - sp >>>= 0; - Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); - }, - - // func valueIndex(v ref, i int) ref - "syscall/js.valueIndex": (sp) => { - sp >>>= 0; - storeValue( - sp + 24, - Reflect.get(loadValue(sp + 8), getInt64(sp + 16)) - ); - }, - - // valueSetIndex(v ref, i int, x ref) - "syscall/js.valueSetIndex": (sp) => { - sp >>>= 0; - Reflect.set( - loadValue(sp + 8), - getInt64(sp + 16), - loadValue(sp + 24) - ); - }, - - // func valueCall(v ref, m string, args []ref) (ref, bool) - "syscall/js.valueCall": (sp) => { - sp >>>= 0; - try { - const v = loadValue(sp + 8); - const m = Reflect.get(v, loadString(sp + 16)); - const args = loadSliceOfValues(sp + 32); - const result = Reflect.apply(m, v, args); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 56, result); - this.mem.setUint8(sp + 64, 1); - } catch (err) { - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 56, err); - this.mem.setUint8(sp + 64, 0); - } - }, - - // func valueInvoke(v ref, args []ref) (ref, bool) - "syscall/js.valueInvoke": (sp) => { - sp >>>= 0; - try { - const v = loadValue(sp + 8); - const args = loadSliceOfValues(sp + 16); - const result = Reflect.apply(v, undefined, args); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, result); - this.mem.setUint8(sp + 48, 1); - } catch (err) { - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, err); - this.mem.setUint8(sp + 48, 0); - } - }, - - // func valueNew(v ref, args []ref) (ref, bool) - "syscall/js.valueNew": (sp) => { - sp >>>= 0; - try { - const v = loadValue(sp + 8); - const args = loadSliceOfValues(sp + 16); - const result = Reflect.construct(v, args); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, result); - this.mem.setUint8(sp + 48, 1); - } catch (err) { - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, err); - this.mem.setUint8(sp + 48, 0); - } - }, - - // func valueLength(v ref) int - "syscall/js.valueLength": (sp) => { - sp >>>= 0; - setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); - }, - - // valuePrepareString(v ref) (ref, int) - "syscall/js.valuePrepareString": (sp) => { - sp >>>= 0; - const str = encoder.encode(String(loadValue(sp + 8))); - storeValue(sp + 16, str); - setInt64(sp + 24, str.length); - }, - - // valueLoadString(v ref, b []byte) - "syscall/js.valueLoadString": (sp) => { - sp >>>= 0; - const str = loadValue(sp + 8); - loadSlice(sp + 16).set(str); - }, - - // func valueInstanceOf(v ref, t ref) bool - "syscall/js.valueInstanceOf": (sp) => { - sp >>>= 0; - this.mem.setUint8( - sp + 24, - loadValue(sp + 8) instanceof loadValue(sp + 16) ? 1 : 0 - ); - }, - - // func copyBytesToGo(dst []byte, src ref) (int, bool) - "syscall/js.copyBytesToGo": (sp) => { - sp >>>= 0; - const dst = loadSlice(sp + 8); - const src = loadValue(sp + 32); - if ( - !(src instanceof Uint8Array || src instanceof Uint8ClampedArray) - ) { - this.mem.setUint8(sp + 48, 0); - return; - } - const toCopy = src.subarray(0, dst.length); - dst.set(toCopy); - setInt64(sp + 40, toCopy.length); - this.mem.setUint8(sp + 48, 1); - }, - - // func copyBytesToJS(dst ref, src []byte) (int, bool) - "syscall/js.copyBytesToJS": (sp) => { - sp >>>= 0; - const dst = loadValue(sp + 8); - const src = loadSlice(sp + 16); - if ( - !(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray) - ) { - this.mem.setUint8(sp + 48, 0); - return; - } - const toCopy = src.subarray(0, dst.length); - dst.set(toCopy); - setInt64(sp + 40, toCopy.length); - this.mem.setUint8(sp + 48, 1); - }, - - debug: (value) => { - console.log(value); - }, - }, - }; - } - - async run(instance) { - if (!(instance instanceof WebAssembly.Instance)) { - throw new Error("Go.run: WebAssembly.Instance expected"); - } - this._inst = instance; - this.mem = new DataView(this._inst.exports.mem.buffer); - this._values = [ - // JS values that Go currently has references to, indexed by reference id - NaN, - 0, - null, - true, - false, - globalThis, - this, - ]; - this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id - this._ids = new Map([ - // mapping from JS values to reference ids - [0, 1], - [null, 2], - [true, 3], - [false, 4], - [globalThis, 5], - [this, 6], - ]); - this._idPool = []; // unused ids that have been garbage collected - this.exited = false; // whether the Go program has exited - - // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. - let offset = 4096; - - const strPtr = (str) => { - const ptr = offset; - const bytes = encoder.encode(str + "\0"); - new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); - offset += bytes.length; - if (offset % 8 !== 0) { - offset += 8 - (offset % 8); - } - return ptr; - }; - - const argc = this.argv.length; - - const argvPtrs = []; - this.argv.forEach((arg) => { - argvPtrs.push(strPtr(arg)); - }); - argvPtrs.push(0); - - const keys = Object.keys(this.env).sort(); - keys.forEach((key) => { - argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); - }); - argvPtrs.push(0); - - const argv = offset; - argvPtrs.forEach((ptr) => { - this.mem.setUint32(offset, ptr, true); - this.mem.setUint32(offset + 4, 0, true); - offset += 8; - }); - - // The linker guarantees global data starts from at least wasmMinDataAddr. - // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. - const wasmMinDataAddr = 4096 + 8192; - if (offset >= wasmMinDataAddr) { - throw new Error( - "total length of command line and environment variables exceeds limit" - ); - } - - this._inst.exports.run(argc, argv); - if (this.exited) { - this._resolveExitPromise(); - } - await this._exitPromise; - } - - _resume() { - if (this.exited) { - throw new Error("Go program has already exited"); - } - this._inst.exports.resume(); - if (this.exited) { - this._resolveExitPromise(); - } - } - - _makeFuncWrapper(id) { - const go = this; - return function () { - const event = { id: id, this: this, args: arguments }; - go._pendingEvent = event; - go._resume(); - return event.result; - }; - } - }; -})(); diff --git a/site/app/components/Terminal.client.tsx b/site/app/components/Terminal.client.tsx deleted file mode 100644 index b8a889c..0000000 --- a/site/app/components/Terminal.client.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useEffect, useRef, useState, useContext } from "react"; -import type React from "react"; -import { Terminal } from "@xterm/xterm"; -import { FitAddon } from "@xterm/addon-fit"; -import { CanvasAddon } from "@xterm/addon-canvas"; -// import { WebglAddon } from "@xterm/addon-webgl"; -import { WushContext } from "~/context/wush"; -import "@xterm/xterm/css/xterm.css"; - -interface WushTerminalProps { - authKey: string; -} - -const WushTerminal: React.FC = ({ authKey }) => { - const terminalRef = useRef(null); - const terminalInstance = useRef(null); - const fitAddonRef = useRef(); - const wushInitialized = useContext(WushContext); - const sshSessionRef = useRef(); - - useEffect(() => { - if (!wushInitialized) { - console.log("WASM not initialized, skipping terminal initialization"); - return; - } - - console.log("Terminal component mounted"); - - if (!terminalRef.current) { - console.log("Terminal ref is null, skipping terminal initialization"); - return; - } - - if (terminalInstance.current) { - console.log("Terminal already initialized, skipping"); - return; - } - - console.log("running wush"); - - console.log("Initializing terminal"); - - const term = new Terminal({ - cursorBlink: true, - theme: { - background: "#282a36", - foreground: "#f8f8f2", - }, - scrollback: 0, - }); - const fitAddon = new FitAddon(); - fitAddonRef.current = fitAddon; - term.loadAddon(fitAddon); - term.loadAddon(new CanvasAddon()); - // term.loadAddon(new WebglAddon()); - term.open(terminalRef.current); - fitAddon.fit(); - - let onDataHook: ((data: string) => void) | undefined; - term.onData((e) => { - onDataHook?.(e); - }); - - const resizeObserver = new window.ResizeObserver(() => fitAddon.fit()); - resizeObserver.observe(terminalRef.current); - - newWush({ authKey: authKey }).then((wush) => { - const sshSession = wush.ssh({ - writeFn(input) { - term.write(input); - }, - writeErrorFn(err) { - term.write(err); - }, - setReadFn(hook) { - onDataHook = hook; - }, - rows: term.rows, - cols: term.cols, - onConnectionProgress: (msg) => {}, - onConnected: () => {}, - onDone() { - resizeObserver?.disconnect(); - term.dispose(); - console.log("term done"); - sshSession.close(); - sshSessionRef.current = null; - }, - }); - - sshSessionRef.current = sshSession; - term.onResize(({ rows, cols }) => sshSession.resize(rows, cols)); - }); - - console.log("Terminal initialized and opened"); - terminalInstance.current = term; - fitAddon.fit(); - - return () => { - console.log("Disposing terminal"); - if (terminalInstance.current) { - resizeObserver.disconnect(); - terminalInstance.current.dispose(); - terminalInstance.current = null; - } - if (sshSessionRef.current) { - sshSessionRef.current.close(); - sshSessionRef.current = null; - } - }; - }, [authKey, wushInitialized]); - - return
; -}; - -export default WushTerminal; diff --git a/site/app/context/wush.ts b/site/app/context/wush.ts deleted file mode 100644 index e6759ec..0000000 --- a/site/app/context/wush.ts +++ /dev/null @@ -1,3 +0,0 @@ -import React from "react"; - -export const WushContext = React.createContext(false); diff --git a/site/app/entry.client.tsx b/site/app/entry.client.tsx deleted file mode 100644 index 1db62c7..0000000 --- a/site/app/entry.client.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { RemixBrowser } from "@remix-run/react"; -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; - -startTransition(() => { - hydrateRoot(document, ); -}); diff --git a/site/app/entry.server.tsx b/site/app/entry.server.tsx deleted file mode 100644 index 65ad16e..0000000 --- a/site/app/entry.server.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { PassThrough } from "node:stream"; - -import type { AppLoadContext, EntryContext } from "@remix-run/node"; -import { createReadableStreamFromReadable } from "@remix-run/node"; -import { RemixServer } from "@remix-run/react"; -import * as isbotModule from "isbot"; -import { renderToPipeableStream } from "react-dom/server"; - -const ABORT_DELAY = 5_000; - -export default function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, - loadContext: AppLoadContext -) { - let prohibitOutOfOrderStreaming = - isBotRequest(request.headers.get("user-agent")) || remixContext.isSpaMode; - - return prohibitOutOfOrderStreaming - ? handleBotRequest( - request, - responseStatusCode, - responseHeaders, - remixContext - ) - : handleBrowserRequest( - request, - responseStatusCode, - responseHeaders, - remixContext - ); -} - -// We have some Remix apps in the wild already running with isbot@3 so we need -// to maintain backwards compatibility even though we want new apps to use -// isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev. -function isBotRequest(userAgent: string | null) { - if (!userAgent) { - return false; - } - - // isbot >= 3.8.0, >4 - if ("isbot" in isbotModule && typeof isbotModule.isbot === "function") { - return isbotModule.isbot(userAgent); - } - - // isbot < 3.8.0 - if ("default" in isbotModule && typeof isbotModule.default === "function") { - return isbotModule.default(userAgent); - } - - return false; -} - -function handleBotRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onAllReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }) - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - } - ); - - setTimeout(abort, ABORT_DELAY); - }); -} - -function handleBrowserRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onShellReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }) - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - } - ); - - setTimeout(abort, ABORT_DELAY); - }); -} diff --git a/site/app/root.tsx b/site/app/root.tsx deleted file mode 100644 index b4fb738..0000000 --- a/site/app/root.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "@remix-run/react"; -import type { LinksFunction, MetaFunction } from "@remix-run/node"; -import { useState, useEffect } from "react"; -import { WushContext } from "./context/wush"; -import wasmUrl from "~/assets/main.wasm?url"; -import goWasmUrl from "~/assets/wasm_exec.js?url"; - -import "./tailwind.css"; - -export const links: LinksFunction = () => [ - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap", - crossOrigin: "anonymous", - }, - { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - }, -]; - -export const meta: MetaFunction = () => { - return [ - { title: "$ wush" }, - { name: "description", content: "wush - share terminals in the browser" }, - ]; -}; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - -