123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898 |
- // 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"
- "maps"
- "net/url"
- "os"
- "os/exec"
- "os/signal"
- "os/user"
- "path"
- "path/filepath"
- "regexp"
- "slices"
- "strconv"
- "strings"
- "time"
- "kitty/tools/cli"
- "kitty/tools/themes"
- "kitty/tools/tty"
- "kitty/tools/tui"
- "kitty/tools/tui/loop"
- "kitty/tools/tui/shell_integration"
- "kitty/tools/utils"
- "kitty/tools/utils/secrets"
- "kitty/tools/utils/shlex"
- "kitty/tools/utils/shm"
- "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
- parsed := false
- if strings.HasPrefix(hostname, "ssh://") {
- p, err := url.Parse(hostname)
- if err == nil {
- hostname_for_match = p.Hostname()
- parsed = true
- if p.User.Username() != "" {
- username = p.User.Username()
- }
- }
- } else if strings.Contains(hostname, "@") && hostname[0] != '@' {
- username, hostname_for_match, _ = strings.Cut(hostname, "@")
- parsed = true
- }
- if !parsed && strings.Contains(hostname, "@") && hostname[0] != '@' {
- _, hostname_for_match, _ = strings.Cut(hostname, "@")
- }
- return
- }
- func read_data_from_shared_memory(shm_name string) ([]byte, error) {
- data, err := shm.ReadWithSizeAndUnlink(shm_name, func(s fs.FileInfo) error {
- 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)
- }
- }
- }
- 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
- listen_on string
- test_script string
- dont_create_shm bool
- 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"))
- if cd.listen_on != "" {
- add_env("KITTY_LISTEN_ON", cd.listen_on)
- }
- 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 ...shell_integration.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
- }
- if err = add_data(fe{"data.sh", utils.UnsafeStringToBytes(env_script)}); err != nil {
- return nil, err
- }
- if cd.script_type == "sh" {
- if err = add_data(fe{"bootstrap-utils.sh", shell_integration.Data()[path.Join("shell-integration/ssh/bootstrap-utils.sh")].Data}); err != nil {
- return nil, err
- }
- }
- if ksi != "" {
- for _, fname := range shell_integration.Data().FilesMatching(
- "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, shell_integration.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"), shell_integration.Data()[path.Join("shell-integration", "ssh", x)])
- if err != nil {
- return nil, err
- }
- }
- }
- err = add_entries(path.Join("home", ".terminfo"), shell_integration.Data()["terminfo/kitty.terminfo"])
- if err == nil {
- err = add_entries(path.Join("home", ".terminfo", "x"), shell_integration.Data()["terminfo/x/"+kitty.DefaultTermName])
- }
- 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 := utils.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 && !cd.dont_create_shm {
- data_shm, err = shm.CreateTemp(fmt.Sprintf("kssh-%d-", os.Getpid()), uint64(len(encoded_data)+8))
- if err == nil {
- err = shm.WriteWithSize(data_shm, encoded_data, 0)
- if err == nil {
- err = data_shm.Flush()
- }
- }
- }
- if err != nil {
- return err
- }
- if !cd.dont_create_shm {
- 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(shell_integration.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 can't 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
- }
- var debugprintln = tty.DebugPrintln
- var _ = debugprintln
- func drain_potential_tty_garbage(term *tty.Term) {
- err := term.ApplyOperations(tty.TCSANOW, tty.SetRaw)
- if err != nil {
- return
- }
- canary, err := secrets.TokenHex()
- if err != nil {
- return
- }
- dcs, err := tui.DCSToKitty("echo", canary)
- q := utils.UnsafeStringToBytes(canary)
- if err != nil {
- return
- }
- err = term.WriteAllString(dcs)
- if err != nil {
- return
- }
- 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 := time.Until(give_up_at)
- if timeout < 0 {
- break
- }
- n, err := term.ReadWithTimeout(buf, timeout)
- if err != nil {
- break
- }
- data = append(data, buf[:n]...)
- }
- }
- func change_colors(color_scheme string) (ans string, err error) {
- if color_scheme == "" {
- return
- }
- var theme *themes.Theme
- if !strings.HasSuffix(color_scheme, ".conf") {
- cs := os.ExpandEnv(color_scheme)
- tc, closer, err := themes.LoadThemes(-1)
- if err != nil && errors.Is(err, themes.ErrNoCacheFound) {
- tc, closer, err = themes.LoadThemes(time.Hour * 24)
- }
- if err != nil {
- return "", err
- }
- defer closer.Close()
- theme = tc.ThemeByName(cs)
- if theme == nil {
- return "", fmt.Errorf("No theme named %#v found", cs)
- }
- } else {
- theme, err = themes.ThemeFromFile(utils.ResolveConfPath(color_scheme))
- if err != nil {
- return "", err
- }
- }
- ans, err = theme.AsEscapeCodes()
- if err == nil {
- ans = "\033[#P" + ans
- }
- return
- }
- func run_ssh(ssh_args, server_args, found_extra_args []string) (rc int, err error) {
- go shell_integration.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.Delegate != "" {
- delegate_cmd, err := shlex.Split(host_opts.Delegate)
- if err != nil {
- return 1, fmt.Errorf("Could not parse delegate command: %#v with error: %w", host_opts.Delegate, err)
- }
- return 1, unix.Exec(utils.FindExe(delegate_cmd[0]), utils.Concat(delegate_cmd, ssh_args, server_args), os.Environ())
- }
- master_is_alive, master_checked := false, false
- var control_master_args []string
- 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"))
- }
- control_master_args, err = connection_sharing_args(kpid)
- if err != nil {
- return 1, err
- }
- cmd = slices.Insert(cmd, insertion_point, control_master_args...)
- }
- 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()
- }
- master_is_functional := func() bool {
- if master_checked {
- return master_is_alive
- }
- master_checked = true
- check_cmd := slices.Insert(cmd, 1, "-O", "check")
- master_is_alive = exec.Command(check_cmd[0], check_cmd[1:]...).Run() == nil
- return master_is_alive
- }
- if need_to_request_data && host_opts.Share_connections && master_is_functional() {
- need_to_request_data = false
- }
- run_control_master := func() error {
- cmcmd := slices.Clone(cmd[:insertion_point])
- cmcmd = append(cmcmd, control_master_args...)
- cmcmd = append(cmcmd, "-N", "-f")
- cmcmd = append(cmcmd, "--", hostname)
- c := exec.Command(cmcmd[0], cmcmd[1:]...)
- c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr
- err := c.Run()
- if err != nil {
- err = fmt.Errorf("Failed to start SSH ControlMaster with cmdline: %s and error: %w", strings.Join(cmcmd, " "), err)
- }
- master_checked = false
- master_is_alive = false
- return err
- }
- if host_opts.Forward_remote_control && os.Getenv("KITTY_LISTEN_ON") != "" {
- if !host_opts.Share_connections {
- return 1, fmt.Errorf("Cannot use forward_remote_control=yes without share_connections=yes as it relies on SSH Controlmasters")
- }
- if !master_is_functional() {
- if err = run_control_master(); err != nil {
- return 1, err
- }
- if !master_is_functional() {
- return 1, fmt.Errorf("SSH ControlMaster not functional after being started explicitly")
- }
- }
- protocol, listen_on, found := strings.Cut(os.Getenv("KITTY_LISTEN_ON"), ":")
- if !found {
- return 1, fmt.Errorf("Invalid KITTY_LISTEN_ON: %#v", os.Getenv("KITTY_LISTEN_ON"))
- }
- if protocol == "unix" && strings.HasPrefix(listen_on, "@") {
- return 1, fmt.Errorf("Cannot forward kitty remote control socket when an abstract UNIX socket (%s) is used, due to limitations in OpenSSH. Use either a path based one or a TCP socket", listen_on)
- }
- cmcmd := slices.Clone(cmd[:insertion_point])
- cmcmd = append(cmcmd, control_master_args...)
- cmcmd = append(cmcmd, "-R", "0:"+listen_on, "-O", "forward")
- cmcmd = append(cmcmd, "--", hostname)
- c := exec.Command(cmcmd[0], cmcmd[1:]...)
- b := bytes.Buffer{}
- c.Stdout = &b
- c.Stderr = os.Stderr
- if err := c.Run(); err != nil {
- return 1, fmt.Errorf("%s\nSetup of port forward in SSH ControlMaster failed with error: %w", b.String(), err)
- }
- port, err := strconv.Atoi(strings.TrimSpace(b.String()))
- if err != nil {
- os.Stderr.Write(b.Bytes())
- return 1, fmt.Errorf("Setup of port forward in SSH ControlMaster failed with error: invalid resolved port returned: %s", b.String())
- }
- cd.listen_on = "tcp:localhost:" + strconv.Itoa(port)
- }
- 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
- escape_codes_to_set_colors, err := change_colors(cd.host_opts.Color_scheme)
- if err == nil {
- err = term.WriteAllString(escape_codes_to_set_colors + loop.SAVE_PRIVATE_MODE_VALUES + loop.HANDLE_TERMIOS_SIGNALS.EscapeCodeToSet())
- }
- if err != nil {
- return 1, err
- }
- restore_escape_codes := loop.RESTORE_PRIVATE_MODE_VALUES + loop.HANDLE_TERMIOS_SIGNALS.EscapeCodeToReset()
- if escape_codes_to_set_colors != "" {
- restore_escape_codes += "\x1b[#Q"
- }
- sigs := make(chan os.Signal, 8)
- signal.Notify(sigs, unix.SIGINT, unix.SIGTERM)
- cleaned_up := false
- cleanup := func() {
- if !cleaned_up {
- _ = term.WriteAllString(restore_escape_codes)
- term.RestoreAndClose()
- signal.Reset()
- cleaned_up = true
- }
- }
- defer cleanup()
- 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
- }
- }
- go func() {
- <-sigs
- // ignore any interrupt and terminate signals as they will usually be sent to the ssh child process as well
- // and we are waiting on that.
- }()
- err = c.Wait()
- drain_potential_tty_garbage(term)
- if err != nil {
- var exit_err *exec.ExitError
- if errors.As(err, &exit_err) {
- if state := exit_err.ProcessState.String(); state == "signal: interrupt" {
- cleanup()
- _ = unix.Kill(os.Getpid(), unix.SIGINT)
- // Give the signal time to be delivered
- time.Sleep(20 * time.Millisecond)
- }
- 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 {
- return 1, unix.Exec(SSHExe(), utils.Concat([]string{"ssh"}, ssh_args, server_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. Its 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)
- },
- })
- }
|