mirror of
https://github.com/tale/headplane.git
synced 2026-08-30 00:47:17 +00:00
feat(ssh): bump restty and follow upstream tailscale ssh
This commit is contained in:
@@ -0,0 +1,718 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Vendored from tailscale.com/cmd/tsconnect/wasm/wasm_js.go.
|
||||
// Upstream ref: fb27d87e02c7358e44a063668902a183216b72ae
|
||||
//
|
||||
// Local changes live in patches/tsconnect-term-type.patch and are already
|
||||
// applied here. Run scripts/sync-tsconnect.sh to move to a newer upstream.
|
||||
|
||||
// The wasm package builds a WebAssembly module that provides a subset of
|
||||
// Tailscale APIs to JavaScript.
|
||||
//
|
||||
// When run in the browser, a newIPN(config) function is added to the global JS
|
||||
// namespace. When called it returns an ipn object with the methods
|
||||
// run(callbacks), login(), logout(), and ssh(...).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"syscall/js"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/words"
|
||||
)
|
||||
|
||||
// ControlURL defines the URL to be used for connection to Control.
|
||||
var ControlURL = ipn.DefaultControlURL
|
||||
|
||||
func main() {
|
||||
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
log.Fatal("Usage: newIPN(config)")
|
||||
return nil
|
||||
}
|
||||
return newIPN(args[0])
|
||||
}))
|
||||
// Keep Go runtime alive, otherwise it will be shut down before newIPN gets
|
||||
// called.
|
||||
<-make(chan bool)
|
||||
}
|
||||
|
||||
func newIPN(jsConfig js.Value) map[string]any {
|
||||
netns.SetEnabled(false)
|
||||
|
||||
var store ipn.StateStore
|
||||
if jsStateStorage := jsConfig.Get("stateStorage"); !jsStateStorage.IsUndefined() {
|
||||
store = &jsStateStore{jsStateStorage}
|
||||
} else {
|
||||
store = new(mem.Store)
|
||||
}
|
||||
|
||||
controlURL := ControlURL
|
||||
if jsControlURL := jsConfig.Get("controlURL"); jsControlURL.Type() == js.TypeString {
|
||||
controlURL = jsControlURL.String()
|
||||
}
|
||||
|
||||
var authKey string
|
||||
if jsAuthKey := jsConfig.Get("authKey"); jsAuthKey.Type() == js.TypeString {
|
||||
authKey = jsAuthKey.String()
|
||||
}
|
||||
|
||||
var hostname string
|
||||
if jsHostname := jsConfig.Get("hostname"); jsHostname.Type() == js.TypeString {
|
||||
hostname = jsHostname.String()
|
||||
} else {
|
||||
hostname = generateHostname()
|
||||
}
|
||||
|
||||
lpc := getOrCreateLogPolicyConfig(store)
|
||||
c := logtail.Config{
|
||||
Collection: lpc.Collection,
|
||||
PrivateID: lpc.PrivateID,
|
||||
|
||||
// Compressed requests set HTTP headers that are not supported by the
|
||||
// no-cors fetching mode:
|
||||
CompressLogs: false,
|
||||
|
||||
HTTPC: &http.Client{Transport: &noCORSTransport{http.DefaultTransport}},
|
||||
}
|
||||
logtail := logtail.NewLogger(c, log.Printf)
|
||||
logf := logtail.Logf
|
||||
|
||||
sys := tsd.NewSystem()
|
||||
sys.Set(store)
|
||||
dialer := &tsdial.Dialer{Logf: logf}
|
||||
dialer.SetBus(sys.Bus.Get())
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Dialer: dialer,
|
||||
SetSubsystem: sys.Set,
|
||||
ControlKnobs: sys.ControlKnobs(),
|
||||
HealthTracker: sys.HealthTracker.Get(),
|
||||
ExtraRootCAs: sys.ExtraRootCAs,
|
||||
Metrics: sys.UserMetricsRegistry(),
|
||||
EventBus: sys.Bus.Get(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sys.Set(eng)
|
||||
|
||||
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
|
||||
if err != nil {
|
||||
log.Fatalf("netstack.Create: %v", err)
|
||||
}
|
||||
sys.Set(ns)
|
||||
ns.ProcessLocalIPs = true
|
||||
ns.ProcessSubnets = true
|
||||
|
||||
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
|
||||
return true
|
||||
}
|
||||
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
||||
// Note: don't just return ns.DialContextTCP or we'll return
|
||||
// *gonet.TCPConn(nil) instead of a nil interface which trips up
|
||||
// callers.
|
||||
tcpConn, err := ns.DialContextTCP(ctx, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tcpConn, nil
|
||||
}
|
||||
dialer.NetstackDialUDP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
||||
// Note: don't just return ns.DialContextUDP or we'll return
|
||||
// *gonet.UDPConn(nil) instead of a nil interface which trips up
|
||||
// callers.
|
||||
udpConn, err := ns.DialContextUDP(ctx, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return udpConn, nil
|
||||
}
|
||||
sys.NetstackRouter.Set(true)
|
||||
sys.Tun.Get().Start()
|
||||
|
||||
logid := lpc.PublicID
|
||||
srv := ipnserver.New(logf, logid, sys.Bus.Get(), sys.NetMon.Get())
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, sys, controlclient.LoginEphemeral)
|
||||
if err != nil {
|
||||
log.Fatalf("ipnlocal.NewLocalBackend: %v", err)
|
||||
}
|
||||
if err := ns.Start(lb); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
srv.SetLocalBackend(lb)
|
||||
|
||||
jsIPN := &jsIPN{
|
||||
dialer: dialer,
|
||||
srv: srv,
|
||||
lb: lb,
|
||||
controlURL: controlURL,
|
||||
authKey: authKey,
|
||||
hostname: hostname,
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"run": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
log.Fatal(`Usage: run({
|
||||
notifyState(state: int): void,
|
||||
notifyNetMap(netMap: object): void,
|
||||
notifyBrowseToURL(url: string): void,
|
||||
notifyPanicRecover(err: string): void,
|
||||
})`)
|
||||
return nil
|
||||
}
|
||||
jsIPN.run(args[0])
|
||||
return nil
|
||||
}),
|
||||
"login": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 0 {
|
||||
log.Printf("Usage: login()")
|
||||
return nil
|
||||
}
|
||||
jsIPN.login()
|
||||
return nil
|
||||
}),
|
||||
"logout": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 0 {
|
||||
log.Printf("Usage: logout()")
|
||||
return nil
|
||||
}
|
||||
jsIPN.logout()
|
||||
return nil
|
||||
}),
|
||||
"ssh": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 3 {
|
||||
log.Printf("Usage: ssh(hostname, userName, termConfig)")
|
||||
return nil
|
||||
}
|
||||
return jsIPN.ssh(
|
||||
args[0].String(),
|
||||
args[1].String(),
|
||||
args[2])
|
||||
}),
|
||||
"fetch": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
log.Printf("Usage: fetch(url)")
|
||||
return nil
|
||||
}
|
||||
|
||||
url := args[0].String()
|
||||
return jsIPN.fetch(url)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type jsIPN struct {
|
||||
dialer *tsdial.Dialer
|
||||
srv *ipnserver.Server
|
||||
lb *ipnlocal.LocalBackend
|
||||
controlURL string
|
||||
authKey string
|
||||
hostname string
|
||||
}
|
||||
|
||||
var jsIPNState = map[ipn.State]string{
|
||||
ipn.NoState: "NoState",
|
||||
ipn.InUseOtherUser: "InUseOtherUser",
|
||||
ipn.NeedsLogin: "NeedsLogin",
|
||||
ipn.NeedsMachineAuth: "NeedsMachineAuth",
|
||||
ipn.Stopped: "Stopped",
|
||||
ipn.Starting: "Starting",
|
||||
ipn.Running: "Running",
|
||||
}
|
||||
|
||||
var jsMachineStatus = map[tailcfg.MachineStatus]string{
|
||||
tailcfg.MachineUnknown: "MachineUnknown",
|
||||
tailcfg.MachineUnauthorized: "MachineUnauthorized",
|
||||
tailcfg.MachineAuthorized: "MachineAuthorized",
|
||||
tailcfg.MachineInvalid: "MachineInvalid",
|
||||
}
|
||||
|
||||
func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
notifyState := func(state ipn.State) {
|
||||
jsCallbacks.Call("notifyState", jsIPNState[state])
|
||||
}
|
||||
notifyState(ipn.NoState)
|
||||
|
||||
i.lb.SetNotifyCallback(func(n ipn.Notify) {
|
||||
// Panics in the notify callback are likely due to be due to bugs in
|
||||
// this bridging module (as opposed to actual bugs in Tailscale) and
|
||||
// thus may be recoverable. Let the UI know, and allow the user to
|
||||
// choose if they want to reload the page.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("Panic recovered:", r)
|
||||
jsCallbacks.Call("notifyPanicRecover", fmt.Sprint(r))
|
||||
}
|
||||
}()
|
||||
log.Printf("NOTIFY: %+v", n)
|
||||
if n.State != nil {
|
||||
notifyState(*n.State)
|
||||
}
|
||||
if n.SelfChange != nil {
|
||||
// Self changed: rebuild the JS-side NetMap snapshot. Peers
|
||||
// don't ride on the bus anymore, so fetch them on demand
|
||||
// from LocalBackend.
|
||||
nm := i.lb.NetMapWithPeers()
|
||||
if nm != nil {
|
||||
jsNetMap := jsNetMap{
|
||||
Self: jsNetMapSelfNode{
|
||||
jsNetMapNode: jsNetMapNode{
|
||||
Name: nm.SelfName(),
|
||||
Addresses: mapSliceView(nm.GetAddresses(), func(a netip.Prefix) string { return a.Addr().String() }),
|
||||
NodeKey: nm.NodeKey.String(),
|
||||
MachineKey: nm.MachineKey.String(),
|
||||
},
|
||||
MachineStatus: jsMachineStatus[nm.GetMachineStatus()],
|
||||
},
|
||||
Peers: mapSlice(nm.Peers, func(p tailcfg.NodeView) jsNetMapPeerNode {
|
||||
name := p.Name()
|
||||
if name == "" {
|
||||
// In practice this should only happen for Hello.
|
||||
name = p.Hostinfo().Hostname()
|
||||
}
|
||||
addrs := make([]string, p.Addresses().Len())
|
||||
for i, ap := range p.Addresses().All() {
|
||||
addrs[i] = ap.Addr().String()
|
||||
}
|
||||
return jsNetMapPeerNode{
|
||||
jsNetMapNode: jsNetMapNode{
|
||||
Name: name,
|
||||
Addresses: addrs,
|
||||
MachineKey: p.Machine().String(),
|
||||
NodeKey: p.Key().String(),
|
||||
},
|
||||
Online: p.Online().Clone(),
|
||||
TailscaleSSHEnabled: p.Hostinfo().TailscaleSSHEnabled(),
|
||||
}
|
||||
}),
|
||||
LockedOut: nm.TKAEnabled && nm.SelfNode.KeySignature().Len() == 0,
|
||||
}
|
||||
if jsonNetMap, err := json.Marshal(jsNetMap); err == nil {
|
||||
jsCallbacks.Call("notifyNetMap", string(jsonNetMap))
|
||||
} else {
|
||||
log.Printf("Could not generate JSON netmap: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.BrowseToURL != nil {
|
||||
jsCallbacks.Call("notifyBrowseToURL", *n.BrowseToURL)
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
err := i.lb.Start(ipn.Options{
|
||||
UpdatePrefs: &ipn.Prefs{
|
||||
ControlURL: i.controlURL,
|
||||
RouteAll: false,
|
||||
WantRunning: true,
|
||||
Hostname: i.hostname,
|
||||
},
|
||||
AuthKey: i.authKey,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Start error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ln, err := safesocket.Listen("")
|
||||
if err != nil {
|
||||
log.Fatalf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
err = i.srv.Run(context.Background(), ln)
|
||||
log.Fatalf("ipnserver.Run exited: %v", err)
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *jsIPN) login() {
|
||||
go i.lb.StartLoginInteractive(context.Background())
|
||||
}
|
||||
|
||||
func (i *jsIPN) logout() {
|
||||
if i.lb.State() == ipn.NoState {
|
||||
log.Printf("Backend not running")
|
||||
}
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
i.lb.Logout(ctx, ipnauth.Self)
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *jsIPN) ssh(host, username string, termConfig js.Value) map[string]any {
|
||||
jsSSHSession := &jsSSHSession{
|
||||
jsIPN: i,
|
||||
host: host,
|
||||
username: username,
|
||||
termConfig: termConfig,
|
||||
}
|
||||
|
||||
go jsSSHSession.Run()
|
||||
|
||||
return map[string]any{
|
||||
"close": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
return jsSSHSession.Close() != nil
|
||||
}),
|
||||
"resize": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
rows := args[0].Int()
|
||||
cols := args[1].Int()
|
||||
return jsSSHSession.Resize(rows, cols) != nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type jsSSHSession struct {
|
||||
jsIPN *jsIPN
|
||||
host string
|
||||
username string
|
||||
termConfig js.Value
|
||||
session *ssh.Session
|
||||
|
||||
pendingResizeRows int
|
||||
pendingResizeCols int
|
||||
}
|
||||
|
||||
func (s *jsSSHSession) Run() {
|
||||
writeFn := s.termConfig.Get("writeFn")
|
||||
writeErrorFn := s.termConfig.Get("writeErrorFn")
|
||||
setReadFn := s.termConfig.Get("setReadFn")
|
||||
rows := s.termConfig.Get("rows").Int()
|
||||
cols := s.termConfig.Get("cols").Int()
|
||||
timeoutSeconds := 5.0
|
||||
if jsTimeoutSeconds := s.termConfig.Get("timeoutSeconds"); jsTimeoutSeconds.Type() == js.TypeNumber {
|
||||
timeoutSeconds = jsTimeoutSeconds.Float()
|
||||
}
|
||||
onConnectionProgress := s.termConfig.Get("onConnectionProgress")
|
||||
onConnected := s.termConfig.Get("onConnected")
|
||||
onDone := s.termConfig.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 to %s…", strings.Split(s.host, ".")[0]))
|
||||
c, err := s.jsIPN.dialer.UserDial(ctx, "tcp", net.JoinHostPort(s.host, "22"))
|
||||
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
|
||||
},
|
||||
User: s.username,
|
||||
}
|
||||
|
||||
reportProgress("Starting SSH client…")
|
||||
sshConn, _, _, err := ssh.NewClientConn(c, s.host, 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
|
||||
}
|
||||
termType := "xterm"
|
||||
if v := s.termConfig.Get("termType"); v.Type() == js.TypeString {
|
||||
termType = v.String()
|
||||
}
|
||||
err = session.RequestPty(termType, rows, cols, ssh.TerminalModes{
|
||||
ssh.ECHO: 1, ssh.ICANON: 1, ssh.ISIG: 1, ssh.ICRNL: 1, ssh.IUTF8: 1,
|
||||
ssh.TTY_OP_ISPEED: 14400, ssh.TTY_OP_OSPEED: 14400,
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (s *jsSSHSession) 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 *jsSSHSession) Resize(rows, cols int) error {
|
||||
if s.session == nil {
|
||||
s.pendingResizeRows = rows
|
||||
s.pendingResizeCols = cols
|
||||
return nil
|
||||
}
|
||||
return s.session.WindowChange(rows, cols)
|
||||
}
|
||||
|
||||
func (i *jsIPN) fetch(url string) js.Value {
|
||||
return makePromise(func() (any, error) {
|
||||
c := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: i.dialer.UserDial,
|
||||
},
|
||||
}
|
||||
res, err := c.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"status": res.StatusCode,
|
||||
"statusText": res.Status,
|
||||
"text": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
return makePromise(func() (any, error) {
|
||||
defer res.Body.Close()
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(res.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.String(), nil
|
||||
})
|
||||
}),
|
||||
// TODO: populate a more complete JS Response object
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
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 jsNetMap struct {
|
||||
Self jsNetMapSelfNode `json:"self"`
|
||||
Peers []jsNetMapPeerNode `json:"peers"`
|
||||
LockedOut bool `json:"lockedOut"`
|
||||
}
|
||||
|
||||
type jsNetMapNode struct {
|
||||
Name string `json:"name"`
|
||||
Addresses []string `json:"addresses"`
|
||||
MachineKey string `json:"machineKey"`
|
||||
NodeKey string `json:"nodeKey"`
|
||||
}
|
||||
|
||||
type jsNetMapSelfNode struct {
|
||||
jsNetMapNode
|
||||
MachineStatus string `json:"machineStatus"`
|
||||
}
|
||||
|
||||
type jsNetMapPeerNode struct {
|
||||
jsNetMapNode
|
||||
Online *bool `json:"online,omitempty"`
|
||||
TailscaleSSHEnabled bool `json:"tailscaleSSHEnabled"`
|
||||
}
|
||||
|
||||
type jsStateStore struct {
|
||||
jsStateStorage js.Value
|
||||
}
|
||||
|
||||
func (s *jsStateStore) ReadState(id ipn.StateKey) ([]byte, error) {
|
||||
jsValue := s.jsStateStorage.Call("getState", string(id))
|
||||
if jsValue.String() == "" {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return hex.DecodeString(jsValue.String())
|
||||
}
|
||||
|
||||
func (s *jsStateStore) WriteState(id ipn.StateKey, bs []byte) error {
|
||||
s.jsStateStorage.Call("setState", string(id), hex.EncodeToString(bs))
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapSlice[T any, M any](a []T, f func(T) M) []M {
|
||||
n := make([]M, len(a))
|
||||
for i, e := range a {
|
||||
n[i] = f(e)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func mapSliceView[T any, M any](a views.Slice[T], f func(T) M) []M {
|
||||
n := make([]M, a.Len())
|
||||
for i, v := range a.All() {
|
||||
n[i] = f(v)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func filterSlice[T any](a []T, f func(T) bool) []T {
|
||||
n := make([]T, 0, len(a))
|
||||
for _, e := range a {
|
||||
if f(e) {
|
||||
n = append(n, e)
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func generateHostname() string {
|
||||
tails := words.Tails()
|
||||
scales := words.Scales()
|
||||
if rand.IntN(2) == 0 {
|
||||
// JavaScript
|
||||
tails = filterSlice(tails, func(s string) bool { return strings.HasPrefix(s, "j") })
|
||||
scales = filterSlice(scales, func(s string) bool { return strings.HasPrefix(s, "s") })
|
||||
} else {
|
||||
// WebAssembly
|
||||
tails = filterSlice(tails, func(s string) bool { return strings.HasPrefix(s, "w") })
|
||||
scales = filterSlice(scales, func(s string) bool { return strings.HasPrefix(s, "a") })
|
||||
}
|
||||
|
||||
tail := tails[rand.IntN(len(tails))]
|
||||
scale := scales[rand.IntN(len(scales))]
|
||||
return fmt.Sprintf("%s-%s", tail, scale)
|
||||
}
|
||||
|
||||
// makePromise handles the boilerplate of wrapping goroutines with JS promises.
|
||||
// f is run on a goroutine and its return value is used to resolve the promise
|
||||
// (or reject it if an error is returned).
|
||||
func makePromise(f func() (any, error)) js.Value {
|
||||
handler := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
resolve := args[0]
|
||||
reject := args[1]
|
||||
go func() {
|
||||
if res, err := f(); err == nil {
|
||||
resolve.Invoke(res)
|
||||
} else {
|
||||
reject.Invoke(err.Error())
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
|
||||
promiseConstructor := js.Global().Get("Promise")
|
||||
return promiseConstructor.New(handler)
|
||||
}
|
||||
|
||||
const logPolicyStateKey = "log-policy"
|
||||
|
||||
func getOrCreateLogPolicyConfig(state ipn.StateStore) *logpolicy.Config {
|
||||
if configBytes, err := state.ReadState(logPolicyStateKey); err == nil {
|
||||
if config, err := logpolicy.ConfigFromBytes(configBytes); err == nil {
|
||||
return config
|
||||
} else {
|
||||
log.Printf("Could not parse log policy config: %v", err)
|
||||
}
|
||||
} else if err != ipn.ErrStateNotExist {
|
||||
log.Printf("Could not get log policy config from state store: %v", err)
|
||||
}
|
||||
config := logpolicy.NewConfig(logtail.CollectionNode)
|
||||
if err := state.WriteState(logPolicyStateKey, config.ToBytes()); err != nil {
|
||||
log.Printf("Could not save log policy config to state store: %v", err)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// noCORSTransport wraps a RoundTripper and forces the no-cors mode on requests,
|
||||
// so that we can use it with non-CORS-aware servers.
|
||||
type noCORSTransport struct {
|
||||
http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *noCORSTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("js.fetch:mode", "no-cors")
|
||||
resp, err := t.RoundTripper.RoundTrip(req)
|
||||
if err == nil {
|
||||
// In no-cors mode no response properties are returned. Populate just
|
||||
// the status so that callers do not think this was an error.
|
||||
resp.StatusCode = http.StatusOK
|
||||
resp.Status = http.StatusText(http.StatusOK)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
Reference in New Issue
Block a user