mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-20 15:10:43 +00:00
wasm play
See: * https://twitter.com/bradfitz/status/1450916922288005122 * https://twitter.com/bradfitz/status/1451423386777751561 * https://twitter.com/bradfitz/status/1457830780550275075 Updates #3157 Change-Id: I7f5a1b1bc1b8a4af0a700834c3fe09c8c791f6dc Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
415
wasmtest/wasmmod/wasmmod_js.go
Normal file
415
wasmtest/wasmmod/wasmmod_js.go
Normal file
@@ -0,0 +1,415 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The wasmmod is a Tailscale-in-wasm proof of concept.
|
||||
//
|
||||
// See ../index.html and ../term.js for how it ties together.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall/js"
|
||||
"time"
|
||||
|
||||
"github.com/skip2/go-qrcode"
|
||||
"tailscale.com/cmd/tailscale/cli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
)
|
||||
|
||||
func main() {
|
||||
netns.SetEnabled(false)
|
||||
var logf logger.Logf = log.Printf
|
||||
|
||||
dialer := new(tsdial.Dialer)
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Dialer: dialer,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
tunDev, magicConn, dnsManager, ok := eng.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
log.Fatalf("%T is not a wgengine.InternalsGetter", eng)
|
||||
}
|
||||
ns, err := netstack.Create(logf, tunDev, eng, magicConn, dialer, dnsManager)
|
||||
if err != nil {
|
||||
log.Fatalf("netstack.Create: %v", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = true
|
||||
ns.ProcessSubnets = true
|
||||
ns.ForwardTCPIn = handleIncomingTCP
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
|
||||
doc := js.Global().Get("document")
|
||||
state := doc.Call("getElementById", "state")
|
||||
topBar := doc.Call("getElementById", "topbar")
|
||||
topBarStyle := topBar.Get("style")
|
||||
netmapEle := doc.Call("getElementById", "netmap")
|
||||
loginEle := doc.Call("getElementById", "loginURL")
|
||||
|
||||
netstackHandlePacket := tunDev.PostFilterIn
|
||||
tunDev.PostFilterIn = func(p *packet.Parsed, t *tstun.Wrapper) filter.Response {
|
||||
if p.IsEchoRequest() {
|
||||
go func() {
|
||||
topBarStyle.Set("background", "gray")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
topBarStyle.Set("background", "white")
|
||||
}()
|
||||
}
|
||||
return netstackHandlePacket(p, t)
|
||||
}
|
||||
|
||||
var store ipn.StateStore = new(mem.Store)
|
||||
srv, err := ipnserver.New(log.Printf, "some-logid", store, eng, dialer, nil, ipnserver.Options{
|
||||
SurviveDisconnects: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("ipnserver.New: %v", err)
|
||||
}
|
||||
lb := srv.LocalBackend()
|
||||
|
||||
state.Set("innerHTML", "ready")
|
||||
|
||||
lb.SetNotifyCallback(func(n ipn.Notify) {
|
||||
log.Printf("NOTIFY: %+v", n)
|
||||
if n.State != nil {
|
||||
state.Set("innerHTML", fmt.Sprint(*n.State))
|
||||
switch *n.State {
|
||||
case ipn.Running, ipn.Starting:
|
||||
loginEle.Set("innerHTML", "")
|
||||
}
|
||||
}
|
||||
if nm := n.NetMap; nm != nil {
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "<p>Name: <b>%s</b></p>\n", html.EscapeString(nm.Name))
|
||||
fmt.Fprintf(&buf, "<p>Addresses: ")
|
||||
for i, a := range nm.Addresses {
|
||||
if i == 0 {
|
||||
fmt.Fprintf(&buf, "<b>%s</b>", a.IP())
|
||||
} else {
|
||||
fmt.Fprintf(&buf, ", %s", a.IP())
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&buf, "</p>")
|
||||
fmt.Fprintf(&buf, "<p>Machine: <b>%v</b>, %v</p>\n", nm.MachineStatus, nm.MachineKey)
|
||||
fmt.Fprintf(&buf, "<p>Nodekey: %v</p>\n", nm.NodeKey)
|
||||
fmt.Fprintf(&buf, "<hr><table>")
|
||||
for _, p := range nm.Peers {
|
||||
var ip string
|
||||
if len(p.Addresses) > 0 {
|
||||
ip = p.Addresses[0].IP().String()
|
||||
}
|
||||
fmt.Fprintf(&buf, "<tr><td>%s</td><td>%s</td></tr>\n", ip, html.EscapeString(p.Name))
|
||||
}
|
||||
fmt.Fprintf(&buf, "</table>")
|
||||
netmapEle.Set("innerHTML", buf.String())
|
||||
}
|
||||
if n.BrowseToURL != nil {
|
||||
esc := html.EscapeString(*n.BrowseToURL)
|
||||
pngBytes, _ := qrcode.Encode(*n.BrowseToURL, qrcode.Medium, 256)
|
||||
qrDataURL := "data:image/png;base64," + base64.StdEncoding.EncodeToString(pngBytes)
|
||||
loginEle.Set("innerHTML", fmt.Sprintf("<a href='%s' target=_blank>%s<br/><img src='%s' border=0></a>", esc, esc, qrDataURL))
|
||||
}
|
||||
})
|
||||
|
||||
start := func() {
|
||||
err := lb.Start(ipn.Options{
|
||||
Prefs: &ipn.Prefs{
|
||||
// go run ./cmd/trunkd/ -remote-url=https://controlplane.tailscale.com
|
||||
//ControlURL: "http://tsdev:8080",
|
||||
ControlURL: "https://controlplane.tailscale.com",
|
||||
RouteAll: false,
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
Hostname: "wasm",
|
||||
},
|
||||
})
|
||||
log.Printf("Start error: %v", err)
|
||||
|
||||
}
|
||||
|
||||
js.Global().Set("startClicked", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
go start()
|
||||
return nil
|
||||
}))
|
||||
|
||||
js.Global().Set("logoutClicked", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
log.Printf("Logout clicked")
|
||||
if lb.State() == ipn.NoState {
|
||||
log.Printf("Backend not running")
|
||||
return nil
|
||||
}
|
||||
go lb.Logout()
|
||||
return nil
|
||||
}))
|
||||
|
||||
js.Global().Set("startLoginInteractive", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
log.Printf("State: %v", lb.State)
|
||||
|
||||
go func() {
|
||||
if lb.State() == ipn.NoState {
|
||||
start()
|
||||
}
|
||||
lb.StartLoginInteractive()
|
||||
}()
|
||||
return nil
|
||||
}))
|
||||
|
||||
js.Global().Set("seeGoroutines", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
full := make([]byte, 1<<20)
|
||||
buf := full[:runtime.Stack(full, true)]
|
||||
js.Global().Get("theTerminal").Call("reset")
|
||||
withCR := make([]byte, 0, len(buf)+bytes.Count(buf, []byte{'\n'}))
|
||||
for _, b := range buf {
|
||||
if b == '\n' {
|
||||
withCR = append(withCR, "\r\n"...)
|
||||
} else {
|
||||
withCR = append(withCR, b)
|
||||
}
|
||||
}
|
||||
js.Global().Get("theTerminal").Call("write", string(withCR))
|
||||
return nil
|
||||
}))
|
||||
|
||||
js.Global().Set("startAuthKey", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
authKey := args[0].String()
|
||||
log.Printf("got auth key")
|
||||
go func() {
|
||||
err := lb.Start(ipn.Options{
|
||||
Prefs: &ipn.Prefs{
|
||||
// go run ./cmd/trunkd/ -remote-url=https://controlplane.tailscale.com
|
||||
//ControlURL: "http://tsdev:8080",
|
||||
ControlURL: "https://controlplane.tailscale.com",
|
||||
RouteAll: false,
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
Hostname: "wasm",
|
||||
},
|
||||
AuthKey: authKey,
|
||||
})
|
||||
log.Printf("Start error: %v", err)
|
||||
}()
|
||||
return nil
|
||||
}))
|
||||
|
||||
var termOutOnce sync.Once
|
||||
|
||||
js.Global().Set("runTailscaleCLI", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) < 1 {
|
||||
log.Printf("missing args")
|
||||
return nil
|
||||
}
|
||||
// TODO(bradfitz): enforce that we're only running one
|
||||
// CLI command at a time, as we modify package cli
|
||||
// globals below, like cli.Fatalf.
|
||||
|
||||
go func() {
|
||||
if len(args) >= 2 {
|
||||
onDone := args[1]
|
||||
defer onDone.Invoke() // re-print the prompt
|
||||
}
|
||||
/*
|
||||
fs := js.Global().Get("globalThis").Get("fs")
|
||||
oldWriteSync := fs.Get("writeSync")
|
||||
defer fs.Set("writeSync", oldWriteSync)
|
||||
|
||||
fs.Set("writeSync", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) != 2 {
|
||||
return nil
|
||||
}
|
||||
js.Global().Get("theTerminal").Call("write", fmt.Sprintf("Got a %T %v\r\n", args[1], args[1]))
|
||||
return nil
|
||||
}))
|
||||
*/
|
||||
line := args[0].String()
|
||||
f := strings.Fields(line)
|
||||
term := js.Global().Get("theTerminal")
|
||||
termOutOnce.Do(func() {
|
||||
cli.Stdout = termWriter{term}
|
||||
cli.Stderr = termWriter{term}
|
||||
})
|
||||
|
||||
cli.Fatalf = func(format string, a ...interface{}) {
|
||||
term.Call("write", strings.ReplaceAll(fmt.Sprintf(format, a...), "\n", "\n\r"))
|
||||
runtime.Goexit()
|
||||
}
|
||||
|
||||
// TODO(bradfitz): add a cli package global logger and make that
|
||||
// package use it, rather than messing with log.SetOutput.
|
||||
log.SetOutput(cli.Stderr)
|
||||
defer log.SetOutput(os.Stderr) // back to console
|
||||
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
term.Call("write", fmt.Sprintf("%s\r\n", e))
|
||||
fmt.Fprintf(os.Stderr, "recovered panic from %q: %v", f, e)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cli.Run(f[1:]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "CLI error on %q: %v\n", f, err)
|
||||
term.Call("write", fmt.Sprintf("%v\r\n", err))
|
||||
return
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}))
|
||||
|
||||
js.Global().Set("runFakeCURL", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) < 2 {
|
||||
log.Printf("missing args")
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
onDone := args[1]
|
||||
defer onDone.Invoke() // re-print the prompt
|
||||
|
||||
line := args[0].String()
|
||||
f := strings.Fields(line)
|
||||
if len(f) < 2 {
|
||||
return
|
||||
}
|
||||
wantURL := f[1]
|
||||
|
||||
term := js.Global().Get("theTerminal")
|
||||
|
||||
c := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: dialer.UserDial,
|
||||
},
|
||||
}
|
||||
|
||||
res, err := c.Get(wantURL)
|
||||
if err != nil {
|
||||
term.Call("write", fmt.Sprintf("Error: %v\r\n", err))
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
res.Write(termWriter{term})
|
||||
}()
|
||||
return nil
|
||||
}))
|
||||
|
||||
js.Global().Set("runSSH", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
if len(args) < 2 {
|
||||
log.Printf("missing args")
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
onDone := args[1]
|
||||
defer onDone.Invoke() // re-print the prompt
|
||||
|
||||
line := args[0].String()
|
||||
f := strings.Fields(line)
|
||||
host := f[1]
|
||||
|
||||
term := js.Global().Get("theTerminal")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
c, err := dialer.UserDial(ctx, "tcp", net.JoinHostPort(host, "22"))
|
||||
if err != nil {
|
||||
term.Call("write", fmt.Sprintf("Error: %v\r\n", err))
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
br := bufio.NewReader(c)
|
||||
greet, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
term.Call("write", fmt.Sprintf("Error: %v\r\n", err))
|
||||
return
|
||||
}
|
||||
term.Call("write", fmt.Sprintf("%v\r\n\r\nTODO(bradfitz): rest of the owl", strings.TrimSpace(greet)))
|
||||
}()
|
||||
return nil
|
||||
}))
|
||||
|
||||
ln, _, err := safesocket.Listen("", 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = srv.Run(context.Background(), ln)
|
||||
log.Fatalf("ipnserver.Run exited: %v", err)
|
||||
}
|
||||
|
||||
type termWriter struct {
|
||||
o js.Value
|
||||
}
|
||||
|
||||
func (w termWriter) Write(p []byte) (n int, err error) {
|
||||
r := bytes.Replace(p, []byte("\n"), []byte("\n\r"), -1)
|
||||
w.o.Call("write", string(r))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func handleIncomingTCP(c net.Conn, port uint16) {
|
||||
if port != 80 {
|
||||
log.Printf("incoming conn on port %v; closing", port)
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
log.Printf("incoming conn on port %v", port)
|
||||
s := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Got HTTP request: %+v", r)
|
||||
if c := strings.TrimPrefix(r.URL.Path, "/"); c != "" {
|
||||
body := js.Global().Get("document").Get("body")
|
||||
body.Set("bgColor", c)
|
||||
}
|
||||
}),
|
||||
}
|
||||
err := s.Serve(&oneConnListener{conn: c})
|
||||
log.Printf("http.Serve: %v", err)
|
||||
}
|
||||
|
||||
type dummyAddr string
|
||||
type oneConnListener struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Accept() (c net.Conn, err error) {
|
||||
c = l.conn
|
||||
if c == nil {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
l.conn = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Close() error { return nil }
|
||||
|
||||
func (l *oneConnListener) Addr() net.Addr { return dummyAddr("unused-address") }
|
||||
|
||||
func (a dummyAddr) Network() string { return string(a) }
|
||||
func (a dummyAddr) String() string { return string(a) }
|
Reference in New Issue
Block a user