760 lines
22 KiB
Go
760 lines
22 KiB
Go
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"kitty"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"kitty/tools/cli"
|
|
"kitty/tools/tty"
|
|
"kitty/tools/tui"
|
|
"kitty/tools/tui/loop"
|
|
"kitty/tools/utils"
|
|
"kitty/tools/utils/secrets"
|
|
"kitty/tools/utils/shm"
|
|
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/exp/slices"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
func get_destination(hostname string) (username, hostname_for_match string) {
|
|
u, err := user.Current()
|
|
if err == nil {
|
|
username = u.Username
|
|
}
|
|
hostname_for_match = hostname
|
|
if strings.HasPrefix(hostname, "ssh://") {
|
|
p, err := url.Parse(hostname)
|
|
if err == nil {
|
|
hostname_for_match = p.Hostname()
|
|
if p.User.Username() != "" {
|
|
username = p.User.Username()
|
|
}
|
|
}
|
|
} else if strings.Contains(hostname, "@") && hostname[0] != '@' {
|
|
username, hostname_for_match, _ = strings.Cut(hostname, "@")
|
|
}
|
|
if strings.Contains(hostname, "@") && hostname[0] != '@' {
|
|
_, hostname_for_match, _ = strings.Cut(hostname_for_match, "@")
|
|
}
|
|
hostname_for_match, _, _ = strings.Cut(hostname_for_match, ":")
|
|
return
|
|
}
|
|
|
|
func read_data_from_shared_memory(shm_name string) ([]byte, error) {
|
|
data, err := shm.ReadWithSizeAndUnlink(shm_name, func(f *os.File) error {
|
|
s, err := f.Stat()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to stat SHM file with error: %w", err)
|
|
}
|
|
if stat, ok := s.Sys().(unix.Stat_t); ok {
|
|
if os.Getuid() != int(stat.Uid) || os.Getgid() != int(stat.Gid) {
|
|
return fmt.Errorf("Incorrect owner on SHM file")
|
|
}
|
|
}
|
|
if s.Mode().Perm() != 0o600 {
|
|
return fmt.Errorf("Incorrect permissions on SHM file")
|
|
}
|
|
return nil
|
|
})
|
|
return data, err
|
|
}
|
|
|
|
func add_cloned_env(val string) (ans map[string]string, err error) {
|
|
data, err := read_data_from_shared_memory(val)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(data, &ans)
|
|
return ans, err
|
|
}
|
|
|
|
func parse_kitten_args(found_extra_args []string, username, hostname_for_match string) (overrides []string, literal_env map[string]string, ferr error) {
|
|
literal_env = make(map[string]string)
|
|
overrides = make([]string, 0, 4)
|
|
for i, a := range found_extra_args {
|
|
if i%2 == 0 {
|
|
continue
|
|
}
|
|
if key, val, found := strings.Cut(a, "="); found {
|
|
if key == "clone_env" {
|
|
le, err := add_cloned_env(val)
|
|
if err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
return nil, nil, ferr
|
|
}
|
|
} else if le != nil {
|
|
literal_env = le
|
|
}
|
|
} else if key != "hostname" {
|
|
overrides = append(overrides, key+" "+val)
|
|
}
|
|
}
|
|
}
|
|
if len(overrides) > 0 {
|
|
overrides = append([]string{"hostname " + username + "@" + hostname_for_match}, overrides...)
|
|
}
|
|
return
|
|
}
|
|
|
|
func connection_sharing_args(kitty_pid int) ([]string, error) {
|
|
rd := utils.RuntimeDir()
|
|
// Bloody OpenSSH generates a 40 char hash and in creating the socket
|
|
// appends a 27 char temp suffix to it. Socket max path length is approx
|
|
// ~104 chars. And on idiotic Apple the path length to the runtime dir
|
|
// (technically the cache dir since Apple has no runtime dir and thinks it's
|
|
// a great idea to delete files in /tmp) is ~48 chars.
|
|
if len(rd) > 35 {
|
|
idiotic_design := fmt.Sprintf("/tmp/kssh-rdir-%d", os.Geteuid())
|
|
if err := utils.AtomicCreateSymlink(rd, idiotic_design); err != nil {
|
|
return nil, err
|
|
}
|
|
rd = idiotic_design
|
|
}
|
|
cp := strings.Replace(kitty.SSHControlMasterTemplate, "{kitty_pid}", strconv.Itoa(kitty_pid), 1)
|
|
cp = strings.Replace(cp, "{ssh_placeholder}", "%C", 1)
|
|
return []string{
|
|
"-o", "ControlMaster=auto",
|
|
"-o", "ControlPath=" + filepath.Join(rd, cp),
|
|
"-o", "ControlPersist=yes",
|
|
"-o", "ServerAliveInterval=60",
|
|
"-o", "ServerAliveCountMax=5",
|
|
"-o", "TCPKeepAlive=no",
|
|
}, nil
|
|
}
|
|
|
|
func set_askpass() (need_to_request_data bool) {
|
|
need_to_request_data = true
|
|
sentinel := filepath.Join(utils.CacheDir(), "openssh-is-new-enough-for-askpass")
|
|
_, err := os.Stat(sentinel)
|
|
sentinel_exists := err == nil
|
|
if sentinel_exists || GetSSHVersion().SupportsAskpassRequire() {
|
|
if !sentinel_exists {
|
|
os.WriteFile(sentinel, []byte{0}, 0o644)
|
|
}
|
|
need_to_request_data = false
|
|
}
|
|
exe, err := os.Executable()
|
|
if err == nil {
|
|
os.Setenv("SSH_ASKPASS", exe)
|
|
os.Setenv("KITTY_KITTEN_RUN_MODULE", "ssh_askpass")
|
|
if !need_to_request_data {
|
|
os.Setenv("SSH_ASKPASS_REQUIRE", "force")
|
|
}
|
|
} else {
|
|
need_to_request_data = true
|
|
}
|
|
return
|
|
}
|
|
|
|
type connection_data struct {
|
|
remote_args []string
|
|
host_opts *Config
|
|
hostname_for_match string
|
|
username string
|
|
echo_on bool
|
|
request_data bool
|
|
literal_env map[string]string
|
|
test_script string
|
|
|
|
shm_name string
|
|
script_type string
|
|
rcmd []string
|
|
replacements map[string]string
|
|
request_id string
|
|
bootstrap_script string
|
|
}
|
|
|
|
func get_effective_ksi_env_var(x string) string {
|
|
parts := strings.Split(strings.TrimSpace(strings.ToLower(x)), " ")
|
|
current := utils.NewSetWithItems(parts...)
|
|
if current.Has("disabled") {
|
|
return ""
|
|
}
|
|
allowed := utils.NewSetWithItems(kitty.AllowedShellIntegrationValues...)
|
|
if !current.IsSubsetOf(allowed) {
|
|
return RelevantKittyOpts().Shell_integration
|
|
}
|
|
return x
|
|
}
|
|
|
|
func serialize_env(cd *connection_data, get_local_env func(string) (string, bool)) (string, string) {
|
|
ksi := ""
|
|
if cd.host_opts.Shell_integration == "inherited" {
|
|
ksi = get_effective_ksi_env_var(RelevantKittyOpts().Shell_integration)
|
|
} else {
|
|
ksi = get_effective_ksi_env_var(cd.host_opts.Shell_integration)
|
|
}
|
|
env := make([]*EnvInstruction, 0, 8)
|
|
add_env := func(key, val string, fallback ...string) *EnvInstruction {
|
|
if val == "" && len(fallback) > 0 {
|
|
val = fallback[0]
|
|
}
|
|
if val != "" {
|
|
env = append(env, &EnvInstruction{key: key, val: val, literal_quote: true})
|
|
return env[len(env)-1]
|
|
}
|
|
return nil
|
|
}
|
|
add_non_literal_env := func(key, val string, fallback ...string) *EnvInstruction {
|
|
ans := add_env(key, val, fallback...)
|
|
if ans != nil {
|
|
ans.literal_quote = false
|
|
}
|
|
return ans
|
|
}
|
|
for k, v := range cd.literal_env {
|
|
add_env(k, v)
|
|
}
|
|
add_env("TERM", os.Getenv("TERM"), RelevantKittyOpts().Term)
|
|
add_env("COLORTERM", "truecolor")
|
|
env = append(env, cd.host_opts.Env...)
|
|
add_env("KITTY_WINDOW_ID", os.Getenv("KITTY_WINDOW_ID"))
|
|
add_env("WINDOWID", os.Getenv("WINDOWID"))
|
|
if ksi != "" {
|
|
add_env("KITTY_SHELL_INTEGRATION", ksi)
|
|
} else {
|
|
env = append(env, &EnvInstruction{key: "KITTY_SHELL_INTEGRATION", delete_on_remote: true})
|
|
}
|
|
add_non_literal_env("KITTY_SSH_KITTEN_DATA_DIR", cd.host_opts.Remote_dir)
|
|
add_non_literal_env("KITTY_LOGIN_SHELL", cd.host_opts.Login_shell)
|
|
add_non_literal_env("KITTY_LOGIN_CWD", cd.host_opts.Cwd)
|
|
if cd.host_opts.Remote_kitty != Remote_kitty_no {
|
|
add_env("KITTY_REMOTE", cd.host_opts.Remote_kitty.String())
|
|
}
|
|
add_env("KITTY_PUBLIC_KEY", os.Getenv("KITTY_PUBLIC_KEY"))
|
|
return final_env_instructions(cd.script_type == "py", get_local_env, env...), ksi
|
|
}
|
|
|
|
func make_tarfile(cd *connection_data, get_local_env func(string) (string, bool)) ([]byte, error) {
|
|
env_script, ksi := serialize_env(cd, get_local_env)
|
|
w := bytes.Buffer{}
|
|
w.Grow(64 * 1024)
|
|
gw, err := gzip.NewWriterLevel(&w, gzip.BestCompression)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tw := tar.NewWriter(gw)
|
|
rd := strings.TrimRight(cd.host_opts.Remote_dir, "/")
|
|
seen := make(map[file_unique_id]string, 32)
|
|
add := func(h *tar.Header, data []byte) (err error) {
|
|
// some distro's like nix mess with installed file permissions so ensure
|
|
// files are at least readable and writable by owning user
|
|
h.Mode |= 0o600
|
|
err = tw.WriteHeader(h)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if data != nil {
|
|
_, err := tw.Write(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return
|
|
}
|
|
for _, ci := range cd.host_opts.Copy {
|
|
err = ci.get_file_data(add, seen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
type fe struct {
|
|
arcname string
|
|
data []byte
|
|
}
|
|
now := time.Now()
|
|
add_data := func(items ...fe) error {
|
|
for _, item := range items {
|
|
err := add(
|
|
&tar.Header{
|
|
Typeflag: tar.TypeReg, Name: item.arcname, Format: tar.FormatPAX, Size: int64(len(item.data)),
|
|
Mode: 0o644, ModTime: now, ChangeTime: now, AccessTime: now,
|
|
}, item.data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
add_entries := func(prefix string, items ...Entry) error {
|
|
for _, item := range items {
|
|
err := add(
|
|
&tar.Header{
|
|
Typeflag: item.metadata.Typeflag, Name: path.Join(prefix, path.Base(item.metadata.Name)), Format: tar.FormatPAX,
|
|
Size: int64(len(item.data)), Mode: item.metadata.Mode, ModTime: item.metadata.ModTime,
|
|
AccessTime: item.metadata.AccessTime, ChangeTime: item.metadata.ChangeTime,
|
|
}, item.data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
|
|
}
|
|
add_data(fe{"data.sh", utils.UnsafeStringToBytes(env_script)})
|
|
if cd.script_type == "sh" {
|
|
add_data(fe{"bootstrap-utils.sh", Data()[path.Join("shell-integration/ssh/bootstrap-utils.sh")].data})
|
|
}
|
|
if ksi != "" {
|
|
for _, fname := range Data().files_matching(
|
|
"shell-integration/",
|
|
"shell-integration/ssh/.+", // bootstrap files are sent as command line args
|
|
"shell-integration/zsh/kitty.zsh", // backward compat file not needed by ssh kitten
|
|
) {
|
|
arcname := path.Join("home/", rd, "/", path.Dir(fname))
|
|
err = add_entries(arcname, Data()[fname])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
if cd.host_opts.Remote_kitty != Remote_kitty_no {
|
|
arcname := path.Join("home/", rd, "/kitty")
|
|
err = add_data(fe{arcname + "/version", utils.UnsafeStringToBytes(kitty.VersionString)})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, x := range []string{"kitty", "kitten"} {
|
|
err = add_entries(path.Join(arcname, "bin"), Data()[path.Join("shell-integration", "ssh", x)])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
err = add_entries(path.Join("home", ".terminfo"), Data()["terminfo/kitty.terminfo"])
|
|
if err == nil {
|
|
err = add_entries(path.Join("home", ".terminfo", "x"), Data()["terminfo/x/xterm-kitty"])
|
|
}
|
|
if err == nil {
|
|
err = tw.Close()
|
|
if err == nil {
|
|
err = gw.Close()
|
|
}
|
|
}
|
|
return w.Bytes(), err
|
|
}
|
|
|
|
func prepare_home_command(cd *connection_data) string {
|
|
is_python := cd.script_type == "py"
|
|
homevar := ""
|
|
for _, ei := range cd.host_opts.Env {
|
|
if ei.key == "HOME" && !ei.delete_on_remote {
|
|
if ei.copy_from_local {
|
|
homevar = os.Getenv("HOME")
|
|
} else {
|
|
homevar = ei.val
|
|
}
|
|
}
|
|
}
|
|
export_home_cmd := ""
|
|
if homevar != "" {
|
|
if is_python {
|
|
export_home_cmd = base64.StdEncoding.EncodeToString(utils.UnsafeStringToBytes(homevar))
|
|
} else {
|
|
export_home_cmd = fmt.Sprintf("export HOME=%s; cd \"$HOME\"", utils.QuoteStringForSH(homevar))
|
|
}
|
|
}
|
|
return export_home_cmd
|
|
}
|
|
|
|
func prepare_exec_cmd(cd *connection_data) string {
|
|
// ssh simply concatenates multiple commands using a space see
|
|
// line 1129 of ssh.c and on the remote side sshd.c runs the
|
|
// concatenated command as shell -c cmd
|
|
if cd.script_type == "py" {
|
|
return base64.RawStdEncoding.EncodeToString(utils.UnsafeStringToBytes(strings.Join(cd.remote_args, " ")))
|
|
}
|
|
args := make([]string, len(cd.remote_args))
|
|
for i, arg := range cd.remote_args {
|
|
args[i] = strings.ReplaceAll(arg, "'", "'\"'\"'")
|
|
}
|
|
return "unset KITTY_SHELL_INTEGRATION; exec \"$login_shell\" -c '" + strings.Join(args, " ") + "'"
|
|
}
|
|
|
|
var data_shm shm.MMap
|
|
|
|
func prepare_script(script string, replacements map[string]string) string {
|
|
if _, found := replacements["EXEC_CMD"]; !found {
|
|
replacements["EXEC_CMD"] = ""
|
|
}
|
|
if _, found := replacements["EXPORT_HOME_CMD"]; !found {
|
|
replacements["EXPORT_HOME_CMD"] = ""
|
|
}
|
|
keys := maps.Keys(replacements)
|
|
for i, key := range keys {
|
|
keys[i] = "\\b" + key + "\\b"
|
|
}
|
|
pat := regexp.MustCompile(strings.Join(keys, "|"))
|
|
return pat.ReplaceAllStringFunc(script, func(key string) string { return replacements[key] })
|
|
}
|
|
|
|
func bootstrap_script(cd *connection_data) (err error) {
|
|
if cd.request_id == "" {
|
|
cd.request_id = os.Getenv("KITTY_PID") + "-" + os.Getenv("KITTY_WINDOW_ID")
|
|
}
|
|
export_home_cmd := prepare_home_command(cd)
|
|
exec_cmd := ""
|
|
if len(cd.remote_args) > 0 {
|
|
exec_cmd = prepare_exec_cmd(cd)
|
|
}
|
|
pw, err := secrets.TokenHex()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tfd, err := make_tarfile(cd, os.LookupEnv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data := map[string]string{
|
|
"tarfile": base64.StdEncoding.EncodeToString(tfd),
|
|
"pw": pw,
|
|
"hostname": cd.hostname_for_match, "username": cd.username,
|
|
}
|
|
encoded_data, err := json.Marshal(data)
|
|
if err == nil {
|
|
data_shm, err = shm.CreateTemp(fmt.Sprintf("kssh-%d-", os.Getpid()), uint64(len(encoded_data)+8))
|
|
if err == nil {
|
|
err = data_shm.WriteWithSize(encoded_data)
|
|
if err == nil {
|
|
err = data_shm.Flush()
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cd.shm_name = data_shm.Name()
|
|
sensitive_data := map[string]string{"REQUEST_ID": cd.request_id, "DATA_PASSWORD": pw, "PASSWORD_FILENAME": cd.shm_name}
|
|
replacements := map[string]string{
|
|
"EXPORT_HOME_CMD": export_home_cmd,
|
|
"EXEC_CMD": exec_cmd,
|
|
"TEST_SCRIPT": cd.test_script,
|
|
}
|
|
add_bool := func(ok bool, key string) {
|
|
if ok {
|
|
replacements[key] = "1"
|
|
} else {
|
|
replacements[key] = "0"
|
|
}
|
|
}
|
|
add_bool(cd.request_data, "REQUEST_DATA")
|
|
add_bool(cd.echo_on, "ECHO_ON")
|
|
sd := maps.Clone(replacements)
|
|
if cd.request_data {
|
|
maps.Copy(sd, sensitive_data)
|
|
}
|
|
maps.Copy(replacements, sensitive_data)
|
|
cd.replacements = replacements
|
|
cd.bootstrap_script = utils.UnsafeBytesToString(Data()["shell-integration/ssh/bootstrap."+cd.script_type].data)
|
|
cd.bootstrap_script = prepare_script(cd.bootstrap_script, sd)
|
|
return err
|
|
}
|
|
|
|
func wrap_bootstrap_script(cd *connection_data) {
|
|
// sshd will execute the command we pass it by join all command line
|
|
// arguments with a space and passing it as a single argument to the users
|
|
// login shell with -c. If the user has a non POSIX login shell it might
|
|
// have different escaping semantics and syntax, so the command it should
|
|
// execute has to be as simple as possible, basically of the form
|
|
// interpreter -c unwrap_script escaped_bootstrap_script
|
|
// The unwrap_script is responsible for unescaping the bootstrap script and
|
|
// executing it.
|
|
encoded_script := ""
|
|
unwrap_script := ""
|
|
if cd.script_type == "py" {
|
|
encoded_script = base64.StdEncoding.EncodeToString(utils.UnsafeStringToBytes(cd.bootstrap_script))
|
|
unwrap_script = `"import base64, sys; eval(compile(base64.standard_b64decode(sys.argv[-1]), 'bootstrap.py', 'exec'))"`
|
|
} else {
|
|
// We cant rely on base64 being available on the remote system, so instead
|
|
// we quote the bootstrap script by replacing ' and \ with \v and \f
|
|
// also replacing \n and ! with \r and \b for tcsh
|
|
// finally surrounding with '
|
|
encoded_script = "'" + strings.NewReplacer("'", "\v", "\\", "\f", "\n", "\r", "!", "\b").Replace(cd.bootstrap_script) + "'"
|
|
unwrap_script = `'eval "$(echo "$0" | tr \\\v\\\f\\\r\\\b \\\047\\\134\\\n\\\041)"' `
|
|
}
|
|
cd.rcmd = []string{"exec", cd.host_opts.Interpreter, "-c", unwrap_script, encoded_script}
|
|
}
|
|
|
|
func get_remote_command(cd *connection_data) error {
|
|
interpreter := cd.host_opts.Interpreter
|
|
q := strings.ToLower(path.Base(interpreter))
|
|
is_python := strings.Contains(q, "python")
|
|
cd.script_type = "sh"
|
|
if is_python {
|
|
cd.script_type = "py"
|
|
}
|
|
err := bootstrap_script(cd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wrap_bootstrap_script(cd)
|
|
return nil
|
|
}
|
|
|
|
func drain_potential_tty_garbage(term *tty.Term) {
|
|
err := term.ApplyOperations(tty.TCSANOW, tty.SetNoEcho)
|
|
if err != nil {
|
|
return
|
|
}
|
|
canary, err := secrets.TokenBase64()
|
|
if err != nil {
|
|
return
|
|
}
|
|
dcs, err := tui.DCSToKitty("echo", canary+"\n\r")
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = term.WriteAllString(dcs)
|
|
if err != nil {
|
|
return
|
|
}
|
|
q := utils.UnsafeStringToBytes(canary)
|
|
data := make([]byte, 0)
|
|
give_up_at := time.Now().Add(2 * time.Second)
|
|
buf := make([]byte, 0, 8192)
|
|
for !bytes.Contains(data, q) {
|
|
buf = buf[:cap(buf)]
|
|
timeout := give_up_at.Sub(time.Now())
|
|
if timeout < 0 {
|
|
break
|
|
}
|
|
n, err := term.ReadWithTimeout(buf, timeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
data = append(data, buf[:n]...)
|
|
}
|
|
}
|
|
|
|
func run_ssh(ssh_args, server_args, found_extra_args []string) (rc int, err error) {
|
|
go Data()
|
|
go RelevantKittyOpts()
|
|
defer func() {
|
|
if data_shm != nil {
|
|
data_shm.Close()
|
|
data_shm.Unlink()
|
|
}
|
|
}()
|
|
cmd := append([]string{SSHExe()}, ssh_args...)
|
|
cd := connection_data{remote_args: server_args[1:]}
|
|
hostname := server_args[0]
|
|
if len(cd.remote_args) == 0 {
|
|
cmd = append(cmd, "-t")
|
|
}
|
|
insertion_point := len(cmd)
|
|
cmd = append(cmd, "--", hostname)
|
|
uname, hostname_for_match := get_destination(hostname)
|
|
overrides, literal_env, err := parse_kitten_args(found_extra_args, uname, hostname_for_match)
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
host_opts, bad_lines, err := load_config(hostname_for_match, uname, overrides)
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
if len(bad_lines) > 0 {
|
|
for _, x := range bad_lines {
|
|
fmt.Fprintf(os.Stderr, "Ignoring bad config line: %s:%d with error: %s", filepath.Base(x.Src_file), x.Line_number, x.Err)
|
|
}
|
|
}
|
|
if host_opts.Share_connections {
|
|
kpid, err := strconv.Atoi(os.Getenv("KITTY_PID"))
|
|
if err != nil {
|
|
return 1, fmt.Errorf("Invalid KITTY_PID env var not an integer: %#v", os.Getenv("KITTY_PID"))
|
|
}
|
|
cpargs, err := connection_sharing_args(kpid)
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
cmd = slices.Insert(cmd, insertion_point, cpargs...)
|
|
}
|
|
use_kitty_askpass := host_opts.Askpass == Askpass_native || (host_opts.Askpass == Askpass_unless_set && os.Getenv("SSH_ASKPASS") == "")
|
|
need_to_request_data := true
|
|
if use_kitty_askpass {
|
|
need_to_request_data = set_askpass()
|
|
}
|
|
if need_to_request_data && host_opts.Share_connections {
|
|
check_cmd := slices.Insert(cmd, 1, "-O", "check")
|
|
err = exec.Command(check_cmd[0], check_cmd[1:]...).Run()
|
|
if err == nil {
|
|
need_to_request_data = false
|
|
}
|
|
}
|
|
term, err := tty.OpenControllingTerm(tty.SetNoEcho)
|
|
if err != nil {
|
|
return 1, fmt.Errorf("Failed to open controlling terminal with error: %w", err)
|
|
}
|
|
cd.echo_on = term.WasEchoOnOriginally()
|
|
cd.host_opts, cd.literal_env = host_opts, literal_env
|
|
cd.request_data = need_to_request_data
|
|
cd.hostname_for_match, cd.username = hostname_for_match, uname
|
|
err = term.WriteAllString(loop.SAVE_PRIVATE_MODE_VALUES + loop.HANDLE_TERMIOS_SIGNALS.EscapeCodeToSet())
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
defer term.WriteAllString(loop.RESTORE_PRIVATE_MODE_VALUES)
|
|
defer term.RestoreAndClose()
|
|
err = get_remote_command(&cd)
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
cmd = append(cmd, cd.rcmd...)
|
|
c := exec.Command(cmd[0], cmd[1:]...)
|
|
c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
err = c.Start()
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
if !cd.request_data {
|
|
rq := fmt.Sprintf("id=%s:pwfile=%s:pw=%s", cd.replacements["REQUEST_ID"], cd.replacements["PASSWORD_FILENAME"], cd.replacements["DATA_PASSWORD"])
|
|
err := term.ApplyOperations(tty.TCSANOW, tty.SetNoEcho)
|
|
if err == nil {
|
|
var dcs string
|
|
dcs, err = tui.DCSToKitty("ssh", rq)
|
|
if err == nil {
|
|
err = term.WriteAllString(dcs)
|
|
}
|
|
}
|
|
if err != nil {
|
|
c.Process.Kill()
|
|
c.Wait()
|
|
return 1, err
|
|
}
|
|
}
|
|
err = c.Wait()
|
|
drain_potential_tty_garbage(term)
|
|
if err != nil {
|
|
var exit_err *exec.ExitError
|
|
if errors.As(err, &exit_err) {
|
|
return exit_err.ExitCode(), nil
|
|
}
|
|
return 1, err
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
|
|
if len(args) > 0 {
|
|
switch args[0] {
|
|
case "use-python":
|
|
args = args[1:] // backwards compat from when we had a python implementation
|
|
case "-h", "--help":
|
|
cmd.ShowHelp()
|
|
return
|
|
}
|
|
}
|
|
ssh_args, server_args, passthrough, found_extra_args, err := ParseSSHArgs(args, "--kitten")
|
|
if err != nil {
|
|
var invargs *ErrInvalidSSHArgs
|
|
switch {
|
|
case errors.As(err, &invargs):
|
|
if invargs.Msg != "" {
|
|
fmt.Fprintln(os.Stderr, invargs.Msg)
|
|
}
|
|
return 1, unix.Exec(SSHExe(), []string{"ssh"}, os.Environ())
|
|
}
|
|
return 1, err
|
|
}
|
|
if passthrough {
|
|
if len(found_extra_args) > 0 {
|
|
return 1, fmt.Errorf("The SSH kitten cannot work with the options: %s", strings.Join(maps.Keys(PassthroughArgs()), " "))
|
|
}
|
|
return 1, unix.Exec(SSHExe(), append([]string{"ssh"}, args...), os.Environ())
|
|
}
|
|
if os.Getenv("KITTY_WINDOW_ID") == "" || os.Getenv("KITTY_PID") == "" {
|
|
return 1, fmt.Errorf("The SSH kitten is meant to run inside a kitty window")
|
|
}
|
|
if !tty.IsTerminal(os.Stdin.Fd()) {
|
|
return 1, fmt.Errorf("The SSH kitten is meant for interactive use only, STDIN must be a terminal")
|
|
}
|
|
return run_ssh(ssh_args, server_args, found_extra_args)
|
|
}
|
|
|
|
func EntryPoint(parent *cli.Command) {
|
|
create_cmd(parent, main)
|
|
}
|
|
|
|
func specialize_command(ssh *cli.Command) {
|
|
ssh.Usage = "arguments for the ssh command"
|
|
ssh.ShortDescription = "Truly convenient SSH"
|
|
ssh.HelpText = "The ssh kitten is a thin wrapper around the ssh command. It automatically enables shell integration on the remote host, re-uses existing connections to reduce latency, makes the kitty terminfo database available, etc. It's invocation is identical to the ssh command. For details on its usage, see :doc:`/kittens/ssh`."
|
|
ssh.IgnoreAllArgs = true
|
|
ssh.OnlyArgsAllowed = true
|
|
ssh.ArgCompleter = cli.CompletionForWrapper("ssh")
|
|
}
|
|
|
|
func test_integration_with_python(args []string) (rc int, err error) {
|
|
f, err := os.CreateTemp("", "*.conf")
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
defer func() {
|
|
f.Close()
|
|
os.Remove(f.Name())
|
|
}()
|
|
_, err = io.Copy(f, os.Stdin)
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
cd := &connection_data{
|
|
request_id: "testing", remote_args: []string{},
|
|
username: "testuser", hostname_for_match: "host.test", request_data: true,
|
|
test_script: args[0], echo_on: true,
|
|
}
|
|
opts, bad_lines, err := load_config(cd.hostname_for_match, cd.username, nil, f.Name())
|
|
if err == nil {
|
|
if len(bad_lines) > 0 {
|
|
return 1, fmt.Errorf("Bad config lines: %s with error: %s", bad_lines[0].Line, bad_lines[0].Err)
|
|
}
|
|
cd.host_opts = opts
|
|
err = get_remote_command(cd)
|
|
}
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
data, err := json.Marshal(map[string]any{"cmd": cd.rcmd, "shm_name": cd.shm_name})
|
|
if err == nil {
|
|
_, err = os.Stdout.Write(data)
|
|
os.Stdout.Close()
|
|
}
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func TestEntryPoint(root *cli.Command) {
|
|
root.AddSubCommand(&cli.Command{
|
|
Name: "ssh",
|
|
OnlyArgsAllowed: true,
|
|
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
|
|
return test_integration_with_python(args)
|
|
},
|
|
})
|
|
|
|
}
|