// Copyright (c) 2022 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. import { useState, useCallback } from "preact/hooks" import { Terminal } from "xterm" import { FitAddon } from "xterm-addon-fit" type SSHSessionDef = { username: string hostname: string } export function SSH({ netMap, ipn }: { netMap: IPNNetMap; ipn: IPN }) { const [sshSessionDef, setSSHSessionDef] = useState(null) const clearSSHSessionDef = useCallback(() => setSSHSessionDef(null), []) if (sshSessionDef) { return ( ) } const sshPeers = netMap.peers.filter( (p) => p.tailscaleSSHEnabled && p.online !== false ) if (sshPeers.length == 0) { return } return } function SSHSession({ def, ipn, onDone, }: { def: SSHSessionDef ipn: IPN onDone: () => void }) { return (
{ if (node) { // Run the SSH session aysnchronously, so that the React render // loop is complete (otherwise the SSH form may still be visible, // which affects the size of the terminal, leading to a spurious // initial resize). setTimeout(() => runSSHSession(node, def, ipn, onDone), 0) } }} /> ) } function runSSHSession( termContainerNode: HTMLDivElement, def: SSHSessionDef, ipn: IPN, onDone: () => void ) { const term = new Terminal({ cursorBlink: true, }) const fitAddon = new FitAddon() term.loadAddon(fitAddon) term.open(termContainerNode) fitAddon.fit() let onDataHook: ((data: string) => void) | undefined term.onData((e) => { onDataHook?.(e) }) term.focus() const sshSession = ipn.ssh(def.hostname, def.username, { writeFn: (input) => term.write(input), setReadFn: (hook) => (onDataHook = hook), rows: term.rows, cols: term.cols, onDone: () => { resizeObserver.disconnect() term.dispose() window.removeEventListener("beforeunload", handleBeforeUnload) onDone() }, }) // Make terminal and SSH session track the size of the containing DOM node. const resizeObserver = new ResizeObserver(() => fitAddon.fit()) resizeObserver.observe(termContainerNode) term.onResize(({ rows, cols }) => sshSession.resize(rows, cols)) // Close the session if the user closes the window without an explicit // exit. const handleBeforeUnload = () => sshSession.close() window.addEventListener("beforeunload", handleBeforeUnload) } function NoSSHPeers() { return (
None of your machines have Tailscale SSH enabled. Give it a try!
) } function SSHForm({ sshPeers, onSubmit, }: { sshPeers: IPNNetMapPeerNode[] onSubmit: (def: SSHSessionDef) => void }) { sshPeers = sshPeers.slice().sort((a, b) => a.name.localeCompare(b.name)) const [username, setUsername] = useState("") const [hostname, setHostname] = useState(sshPeers[0].name) return (
{ e.preventDefault() onSubmit({ username, hostname }) }} > setUsername(e.currentTarget.value)} />
) }