Skip to content

Commit 54475a6

Browse files
committed
feat: init wush.dev
1 parent d6723bd commit 54475a6

27 files changed

+8805
-3
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
dist
22
test
3+
4+
main.wasm
5+
test.txt

cmd/wasm/main.go

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
//go:build js && wasm
2+
3+
package main
4+
5+
import (
6+
"bytes"
7+
"context"
8+
"fmt"
9+
"log"
10+
"log/slog"
11+
"net"
12+
"syscall/js"
13+
"time"
14+
15+
"github.com/coder/wush/overlay"
16+
"github.com/coder/wush/tsserver"
17+
"golang.org/x/crypto/ssh"
18+
"golang.org/x/xerrors"
19+
"tailscale.com/ipn/store"
20+
"tailscale.com/net/netns"
21+
"tailscale.com/tsnet"
22+
)
23+
24+
func main() {
25+
fmt.Println("WebAssembly module initialized")
26+
defer fmt.Println("WebAssembly module exited")
27+
28+
js.Global().Set("newWush", js.FuncOf(func(this js.Value, args []js.Value) any {
29+
handler := js.FuncOf(func(this js.Value, promiseArgs []js.Value) any {
30+
if len(args) != 1 {
31+
log.Fatal("Usage: newWush(config)")
32+
return nil
33+
}
34+
35+
go func() {
36+
w := newWush(args[0])
37+
promiseArgs[0].Invoke(w)
38+
}()
39+
40+
return nil
41+
})
42+
43+
promiseConstructor := js.Global().Get("Promise")
44+
return promiseConstructor.New(handler)
45+
}))
46+
js.Global().Set("exitWush", js.FuncOf(func(this js.Value, args []js.Value) any {
47+
// close(ch)
48+
return nil
49+
}))
50+
51+
// Keep the main function running
52+
<-make(chan struct{}, 0)
53+
}
54+
55+
func newWush(jsConfig js.Value) map[string]any {
56+
ctx := context.Background()
57+
var authKey string
58+
if jsAuthKey := jsConfig.Get("authKey"); jsAuthKey.Type() == js.TypeString {
59+
authKey = jsAuthKey.String()
60+
}
61+
62+
logger := slog.New(slog.NewTextHandler(jsConsoleWriter{}, nil))
63+
hlog := func(format string, args ...any) {
64+
fmt.Printf(format+"\n", args...)
65+
}
66+
dm, err := tsserver.DERPMapTailscale(ctx)
67+
if err != nil {
68+
panic(err)
69+
}
70+
71+
send := overlay.NewSendOverlay(logger, dm)
72+
err = send.Auth.Parse(authKey)
73+
if err != nil {
74+
panic(err)
75+
}
76+
77+
s, err := tsserver.NewServer(ctx, logger, send)
78+
if err != nil {
79+
panic(err)
80+
}
81+
82+
go send.ListenOverlayDERP(ctx)
83+
go s.ListenAndServe(ctx)
84+
netns.SetDialerOverride(s.Dialer())
85+
86+
ts, err := newTSNet("send")
87+
if err != nil {
88+
panic(err)
89+
}
90+
91+
_, err = ts.Up(ctx)
92+
if err != nil {
93+
panic(err)
94+
}
95+
hlog("WireGuard is ready")
96+
97+
return map[string]any{
98+
"stop": js.FuncOf(func(this js.Value, args []js.Value) any {
99+
if len(args) != 0 {
100+
log.Printf("Usage: stop()")
101+
return nil
102+
}
103+
ts.Close()
104+
return nil
105+
}),
106+
"ssh": js.FuncOf(func(this js.Value, args []js.Value) any {
107+
if len(args) != 1 {
108+
log.Printf("Usage: ssh({})")
109+
return nil
110+
}
111+
112+
sess := &sshSession{
113+
ts: ts,
114+
cfg: args[0],
115+
}
116+
117+
go sess.Run()
118+
119+
return map[string]any{
120+
"close": js.FuncOf(func(this js.Value, args []js.Value) any {
121+
return sess.Close() != nil
122+
}),
123+
"resize": js.FuncOf(func(this js.Value, args []js.Value) any {
124+
rows := args[0].Int()
125+
cols := args[1].Int()
126+
return sess.Resize(rows, cols) != nil
127+
}),
128+
}
129+
}),
130+
}
131+
}
132+
133+
type sshSession struct {
134+
ts *tsnet.Server
135+
cfg js.Value
136+
137+
session *ssh.Session
138+
pendingResizeRows int
139+
pendingResizeCols int
140+
}
141+
142+
func (s *sshSession) Close() error {
143+
if s.session == nil {
144+
// We never had a chance to open the session, ignore the close request.
145+
return nil
146+
}
147+
return s.session.Close()
148+
}
149+
150+
func (s *sshSession) Resize(rows, cols int) error {
151+
if s.session == nil {
152+
s.pendingResizeRows = rows
153+
s.pendingResizeCols = cols
154+
return nil
155+
}
156+
return s.session.WindowChange(rows, cols)
157+
}
158+
159+
func (s *sshSession) Run() {
160+
writeFn := s.cfg.Get("writeFn")
161+
writeErrorFn := s.cfg.Get("writeErrorFn")
162+
setReadFn := s.cfg.Get("setReadFn")
163+
rows := s.cfg.Get("rows").Int()
164+
cols := s.cfg.Get("cols").Int()
165+
timeoutSeconds := 5.0
166+
if jsTimeoutSeconds := s.cfg.Get("timeoutSeconds"); jsTimeoutSeconds.Type() == js.TypeNumber {
167+
timeoutSeconds = jsTimeoutSeconds.Float()
168+
}
169+
onConnectionProgress := s.cfg.Get("onConnectionProgress")
170+
onConnected := s.cfg.Get("onConnected")
171+
onDone := s.cfg.Get("onDone")
172+
defer onDone.Invoke()
173+
174+
writeError := func(label string, err error) {
175+
writeErrorFn.Invoke(fmt.Sprintf("%s Error: %v\r\n", label, err))
176+
}
177+
reportProgress := func(message string) {
178+
onConnectionProgress.Invoke(message)
179+
}
180+
181+
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds*float64(time.Second)))
182+
defer cancel()
183+
reportProgress(fmt.Sprintf("Connecting..."))
184+
c, err := s.ts.Dial(ctx, "tcp", net.JoinHostPort("100.64.0.0", "3"))
185+
if err != nil {
186+
writeError("Dial", err)
187+
return
188+
}
189+
defer c.Close()
190+
191+
config := &ssh.ClientConfig{
192+
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
193+
// Host keys are not used with Tailscale SSH, but we can use this
194+
// callback to know that the connection has been established.
195+
reportProgress("SSH connection established…")
196+
return nil
197+
},
198+
}
199+
200+
reportProgress("Starting SSH client…")
201+
sshConn, _, _, err := ssh.NewClientConn(c, "100.64.0.0:3", config)
202+
if err != nil {
203+
writeError("SSH Connection", err)
204+
return
205+
}
206+
defer sshConn.Close()
207+
208+
sshClient := ssh.NewClient(sshConn, nil, nil)
209+
defer sshClient.Close()
210+
211+
session, err := sshClient.NewSession()
212+
if err != nil {
213+
writeError("SSH Session", err)
214+
return
215+
}
216+
s.session = session
217+
defer session.Close()
218+
219+
stdin, err := session.StdinPipe()
220+
if err != nil {
221+
writeError("SSH Stdin", err)
222+
return
223+
}
224+
225+
session.Stdout = termWriter{writeFn}
226+
session.Stderr = termWriter{writeFn}
227+
228+
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) any {
229+
input := args[0].String()
230+
_, err := stdin.Write([]byte(input))
231+
if err != nil {
232+
writeError("Write Input", err)
233+
}
234+
return nil
235+
}))
236+
237+
// We might have gotten a resize notification since we started opening the
238+
// session, pick up the latest size.
239+
if s.pendingResizeRows != 0 {
240+
rows = s.pendingResizeRows
241+
}
242+
if s.pendingResizeCols != 0 {
243+
cols = s.pendingResizeCols
244+
}
245+
err = session.RequestPty("xterm", rows, cols, ssh.TerminalModes{})
246+
247+
if err != nil {
248+
writeError("Pseudo Terminal", err)
249+
return
250+
}
251+
252+
err = session.Shell()
253+
if err != nil {
254+
writeError("Shell", err)
255+
return
256+
}
257+
258+
onConnected.Invoke()
259+
err = session.Wait()
260+
if err != nil {
261+
writeError("Wait", err)
262+
return
263+
}
264+
}
265+
266+
type termWriter struct {
267+
f js.Value
268+
}
269+
270+
func (w termWriter) Write(p []byte) (n int, err error) {
271+
r := bytes.Replace(p, []byte("\n"), []byte("\n\r"), -1)
272+
w.f.Invoke(string(r))
273+
return len(p), nil
274+
}
275+
276+
type jsConsoleWriter struct{}
277+
278+
func (w jsConsoleWriter) Write(p []byte) (n int, err error) {
279+
js.Global().Get("console").Call("log", string(p))
280+
return len(p), nil
281+
}
282+
283+
func newTSNet(direction string) (*tsnet.Server, error) {
284+
var err error
285+
// tmp := os.TempDir()
286+
srv := new(tsnet.Server)
287+
// srv.Dir = tmp
288+
srv.Hostname = "wush-" + direction
289+
srv.Ephemeral = true
290+
srv.AuthKey = direction
291+
srv.ControlURL = "http://127.0.0.1:8080"
292+
// srv.Logf = func(format string, args ...any) {}
293+
srv.Logf = func(format string, args ...any) {
294+
fmt.Printf(format+"\n", args...)
295+
}
296+
// srv.UserLogf = func(format string, args ...any) {}
297+
srv.UserLogf = func(format string, args ...any) {
298+
fmt.Printf(format+"\n", args...)
299+
}
300+
// netns.SetEnabled(false)
301+
302+
srv.Store, err = store.New(func(format string, args ...any) {}, "mem:wush")
303+
if err != nil {
304+
return nil, xerrors.Errorf("create state store: %w", err)
305+
}
306+
307+
return srv, nil
308+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module github.com/coder/wush
22

33
go 1.22.5
44

5-
replace tailscale.com => github.com/coadler/tailscale v1.1.1-0.20240815205130-c39ab8bcc2a9
5+
replace tailscale.com => github.com/coadler/tailscale v1.1.1-0.20240926000438-059d0c1039af
66

77
replace github.com/gliderlabs/ssh => github.com/coder/ssh v0.0.0-20231128192721-70855dedb788
88

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
145145
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
146146
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
147147
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
148-
github.com/coadler/tailscale v1.1.1-0.20240815205130-c39ab8bcc2a9 h1:XLbLUULAjNzo8QOTqDPOIHegRNga3cgJg95srOmYM2Q=
149-
github.com/coadler/tailscale v1.1.1-0.20240815205130-c39ab8bcc2a9/go.mod h1:rRq+xvgprFys8sZbJgcAMMqpiP6r+Y75CJRhCRmXrd0=
148+
github.com/coadler/tailscale v1.1.1-0.20240926000438-059d0c1039af h1:7h0hQxaizCT3u7Fu9b6k1NgGj4EHxx/K3H7YBAFanVE=
149+
github.com/coadler/tailscale v1.1.1-0.20240926000438-059d0c1039af/go.mod h1:rRq+xvgprFys8sZbJgcAMMqpiP6r+Y75CJRhCRmXrd0=
150150
github.com/coder/coder/v2 v2.14.2 h1:RNNDDwjNK5D1XMQlK7LWrS4niVclkl1FXoaOaW7N5rs=
151151
github.com/coder/coder/v2 v2.14.2/go.mod h1:dO79BI5XlP8rrtne1JpRcVehe27bNMXdZKyn1NsWbjA=
152152
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=

site/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
.env

0 commit comments

Comments
 (0)