diff --git a/cmd/tsconnect/wasm/wasm_js.go b/cmd/tsconnect/wasm/wasm_js.go index 613652d39..bc5ed3e80 100644 --- a/cmd/tsconnect/wasm/wasm_js.go +++ b/cmd/tsconnect/wasm/wasm_js.go @@ -6,11 +6,10 @@ // // 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(...). +// run(callbacks), login(), and logout(). package main import ( - "bytes" "context" "crypto/tls" "crypto/x509" @@ -31,7 +30,6 @@ import ( "syscall/js" "time" - "golang.org/x/crypto/ssh" "golang.org/x/net/dns/dnsmessage" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" @@ -233,25 +231,6 @@ func newIPN(jsConfig js.Value, shutdownCh chan struct{}) map[string]any { 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) - }), "dial": js.FuncOf(func(this js.Value, args []js.Value) any { if len(args) != 2 { log.Printf("Usage: dial(network, addr)") @@ -291,13 +270,6 @@ func newIPN(jsConfig js.Value, shutdownCh chan struct{}) map[string]any { } return jsIPN.setExitNode(args[0].String()) }), - "setExitNodeEnabled": js.FuncOf(func(this js.Value, args []js.Value) any { - if len(args) != 1 { - log.Printf("Usage: setExitNodeEnabled(enabled)") - return nil - } - return jsIPN.setExitNodeEnabled(args[0].Bool()) - }), "listFileTargets": js.FuncOf(func(this js.Value, args []js.Value) any { return jsIPN.listFileTargets() }), @@ -660,193 +632,6 @@ func (i *jsIPN) shutdown() js.Value { }) } -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 - } - 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 - } -} - -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 - }) -} - func (i *jsIPN) setExitNode(stableNodeID string) js.Value { return makePromise(func() (any, error) { mp := &ipn.MaskedPrefs{ @@ -858,13 +643,6 @@ func (i *jsIPN) setExitNode(stableNodeID string) js.Value { }) } -func (i *jsIPN) setExitNodeEnabled(enabled bool) js.Value { - return makePromise(func() (any, error) { - _, err := i.lb.SetUseExitNodeEnabled(ipnauth.Self, enabled) - return nil, err - }) -} - func (i *jsIPN) dial(network, addr string) js.Value { return makePromise(func() (any, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) @@ -1336,10 +1114,10 @@ func (i *jsIPN) ping(ip string, pingType string, size int) js.Value { return nil, fmt.Errorf("ping: invalid IP %q: %w", ip, err) } switch tailcfg.PingType(pingType) { - case tailcfg.PingDisco, tailcfg.PingTSMP, tailcfg.PingICMP, tailcfg.PingPeerAPI: + case tailcfg.PingTSMP, tailcfg.PingICMP, tailcfg.PingPeerAPI: // valid default: - return nil, fmt.Errorf("ping: unknown type %q, must be one of: disco, TSMP, ICMP, peerapi", pingType) + return nil, fmt.Errorf("ping: unknown type %q, must be one of: TSMP, ICMP, peerapi", pingType) } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -1664,16 +1442,6 @@ func resolveUDPAddr(s string) (*net.UDPAddr, error) { return &net.UDPAddr{IP: ip, Port: port}, 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 -} - // jsIncomingFile is the JSON representation of an in-progress inbound file // transfer sent to the notifyIncomingFiles callback. type jsIncomingFile struct {