main.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package ssh
  3. import (
  4. "archive/tar"
  5. "bytes"
  6. "compress/gzip"
  7. "encoding/base64"
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "io"
  12. "io/fs"
  13. "kitty"
  14. "maps"
  15. "net/url"
  16. "os"
  17. "os/exec"
  18. "os/signal"
  19. "os/user"
  20. "path"
  21. "path/filepath"
  22. "regexp"
  23. "slices"
  24. "strconv"
  25. "strings"
  26. "syscall"
  27. "time"
  28. "kitty/tools/cli"
  29. "kitty/tools/themes"
  30. "kitty/tools/tty"
  31. "kitty/tools/tui"
  32. "kitty/tools/tui/loop"
  33. "kitty/tools/tui/shell_integration"
  34. "kitty/tools/utils"
  35. "kitty/tools/utils/secrets"
  36. "kitty/tools/utils/shlex"
  37. "kitty/tools/utils/shm"
  38. "golang.org/x/sys/unix"
  39. )
  40. var _ = fmt.Print
  41. func get_destination(hostname string) (username, hostname_for_match string) {
  42. u, err := user.Current()
  43. if err == nil {
  44. username = u.Username
  45. }
  46. hostname_for_match = hostname
  47. parsed := false
  48. if strings.HasPrefix(hostname, "ssh://") {
  49. p, err := url.Parse(hostname)
  50. if err == nil {
  51. hostname_for_match = p.Hostname()
  52. parsed = true
  53. if p.User.Username() != "" {
  54. username = p.User.Username()
  55. }
  56. }
  57. } else if strings.Contains(hostname, "@") && hostname[0] != '@' {
  58. username, hostname_for_match, _ = strings.Cut(hostname, "@")
  59. parsed = true
  60. }
  61. if !parsed && strings.Contains(hostname, "@") && hostname[0] != '@' {
  62. _, hostname_for_match, _ = strings.Cut(hostname, "@")
  63. }
  64. return
  65. }
  66. func read_data_from_shared_memory(shm_name string) ([]byte, error) {
  67. data, err := shm.ReadWithSizeAndUnlink(shm_name, func(s fs.FileInfo) error {
  68. if stat, ok := s.Sys().(syscall.Stat_t); ok {
  69. if os.Getuid() != int(stat.Uid) || os.Getgid() != int(stat.Gid) {
  70. return fmt.Errorf("Incorrect owner on SHM file")
  71. }
  72. }
  73. if s.Mode().Perm() != 0o600 {
  74. return fmt.Errorf("Incorrect permissions on SHM file")
  75. }
  76. return nil
  77. })
  78. return data, err
  79. }
  80. func add_cloned_env(val string) (ans map[string]string, err error) {
  81. data, err := read_data_from_shared_memory(val)
  82. if err != nil {
  83. return nil, err
  84. }
  85. err = json.Unmarshal(data, &ans)
  86. return ans, err
  87. }
  88. func parse_kitten_args(found_extra_args []string, username, hostname_for_match string) (overrides []string, literal_env map[string]string, ferr error) {
  89. literal_env = make(map[string]string)
  90. overrides = make([]string, 0, 4)
  91. for i, a := range found_extra_args {
  92. if i%2 == 0 {
  93. continue
  94. }
  95. if key, val, found := strings.Cut(a, "="); found {
  96. if key == "clone_env" {
  97. le, err := add_cloned_env(val)
  98. if err != nil {
  99. if !errors.Is(err, fs.ErrNotExist) {
  100. return nil, nil, ferr
  101. }
  102. } else if le != nil {
  103. literal_env = le
  104. }
  105. } else if key != "hostname" {
  106. overrides = append(overrides, key+"="+val)
  107. }
  108. }
  109. }
  110. return
  111. }
  112. func connection_sharing_args(kitty_pid int) ([]string, error) {
  113. rd := utils.RuntimeDir()
  114. // Bloody OpenSSH generates a 40 char hash and in creating the socket
  115. // appends a 27 char temp suffix to it. Socket max path length is approx
  116. // ~104 chars. And on idiotic Apple the path length to the runtime dir
  117. // (technically the cache dir since Apple has no runtime dir and thinks it's
  118. // a great idea to delete files in /tmp) is ~48 chars.
  119. if len(rd) > 35 {
  120. idiotic_design := fmt.Sprintf("/tmp/kssh-rdir-%d", os.Geteuid())
  121. if err := utils.AtomicCreateSymlink(rd, idiotic_design); err != nil {
  122. return nil, err
  123. }
  124. rd = idiotic_design
  125. }
  126. cp := strings.Replace(kitty.SSHControlMasterTemplate, "{kitty_pid}", strconv.Itoa(kitty_pid), 1)
  127. cp = strings.Replace(cp, "{ssh_placeholder}", "%C", 1)
  128. return []string{
  129. "-o", "ControlMaster=auto",
  130. "-o", "ControlPath=" + filepath.Join(rd, cp),
  131. "-o", "ControlPersist=yes",
  132. "-o", "ServerAliveInterval=60",
  133. "-o", "ServerAliveCountMax=5",
  134. "-o", "TCPKeepAlive=no",
  135. }, nil
  136. }
  137. func set_askpass() (need_to_request_data bool) {
  138. need_to_request_data = true
  139. sentinel := filepath.Join(utils.CacheDir(), "openssh-is-new-enough-for-askpass")
  140. _, err := os.Stat(sentinel)
  141. sentinel_exists := err == nil
  142. if sentinel_exists || GetSSHVersion().SupportsAskpassRequire() {
  143. if !sentinel_exists {
  144. _ = os.WriteFile(sentinel, []byte{0}, 0o644)
  145. }
  146. need_to_request_data = false
  147. }
  148. exe, err := os.Executable()
  149. if err == nil {
  150. os.Setenv("SSH_ASKPASS", exe)
  151. os.Setenv("KITTY_KITTEN_RUN_MODULE", "ssh_askpass")
  152. if !need_to_request_data {
  153. os.Setenv("SSH_ASKPASS_REQUIRE", "force")
  154. }
  155. } else {
  156. need_to_request_data = true
  157. }
  158. return
  159. }
  160. type connection_data struct {
  161. remote_args []string
  162. host_opts *Config
  163. hostname_for_match string
  164. username string
  165. echo_on bool
  166. request_data bool
  167. literal_env map[string]string
  168. listen_on string
  169. test_script string
  170. dont_create_shm bool
  171. shm_name string
  172. script_type string
  173. rcmd []string
  174. replacements map[string]string
  175. request_id string
  176. bootstrap_script string
  177. }
  178. func get_effective_ksi_env_var(x string) string {
  179. parts := strings.Split(strings.TrimSpace(strings.ToLower(x)), " ")
  180. current := utils.NewSetWithItems(parts...)
  181. if current.Has("disabled") {
  182. return ""
  183. }
  184. allowed := utils.NewSetWithItems(kitty.AllowedShellIntegrationValues...)
  185. if !current.IsSubsetOf(allowed) {
  186. return RelevantKittyOpts().Shell_integration
  187. }
  188. return x
  189. }
  190. func serialize_env(cd *connection_data, get_local_env func(string) (string, bool)) (string, string) {
  191. ksi := ""
  192. if cd.host_opts.Shell_integration == "inherited" {
  193. ksi = get_effective_ksi_env_var(RelevantKittyOpts().Shell_integration)
  194. } else {
  195. ksi = get_effective_ksi_env_var(cd.host_opts.Shell_integration)
  196. }
  197. env := make([]*EnvInstruction, 0, 8)
  198. add_env := func(key, val string, fallback ...string) *EnvInstruction {
  199. if val == "" && len(fallback) > 0 {
  200. val = fallback[0]
  201. }
  202. if val != "" {
  203. env = append(env, &EnvInstruction{key: key, val: val, literal_quote: true})
  204. return env[len(env)-1]
  205. }
  206. return nil
  207. }
  208. add_non_literal_env := func(key, val string, fallback ...string) *EnvInstruction {
  209. ans := add_env(key, val, fallback...)
  210. if ans != nil {
  211. ans.literal_quote = false
  212. }
  213. return ans
  214. }
  215. for k, v := range cd.literal_env {
  216. add_env(k, v)
  217. }
  218. add_env("TERM", os.Getenv("TERM"), RelevantKittyOpts().Term)
  219. add_env("COLORTERM", "truecolor")
  220. env = append(env, cd.host_opts.Env...)
  221. add_env("KITTY_WINDOW_ID", os.Getenv("KITTY_WINDOW_ID"))
  222. add_env("WINDOWID", os.Getenv("WINDOWID"))
  223. if ksi != "" {
  224. add_env("KITTY_SHELL_INTEGRATION", ksi)
  225. } else {
  226. env = append(env, &EnvInstruction{key: "KITTY_SHELL_INTEGRATION", delete_on_remote: true})
  227. }
  228. add_non_literal_env("KITTY_SSH_KITTEN_DATA_DIR", cd.host_opts.Remote_dir)
  229. add_non_literal_env("KITTY_LOGIN_SHELL", cd.host_opts.Login_shell)
  230. add_non_literal_env("KITTY_LOGIN_CWD", cd.host_opts.Cwd)
  231. if cd.host_opts.Remote_kitty != Remote_kitty_no {
  232. add_env("KITTY_REMOTE", cd.host_opts.Remote_kitty.String())
  233. }
  234. add_env("KITTY_PUBLIC_KEY", os.Getenv("KITTY_PUBLIC_KEY"))
  235. if cd.listen_on != "" {
  236. add_env("KITTY_LISTEN_ON", cd.listen_on)
  237. }
  238. return final_env_instructions(cd.script_type == "py", get_local_env, env...), ksi
  239. }
  240. func make_tarfile(cd *connection_data, get_local_env func(string) (string, bool)) ([]byte, error) {
  241. env_script, ksi := serialize_env(cd, get_local_env)
  242. w := bytes.Buffer{}
  243. w.Grow(64 * 1024)
  244. gw, err := gzip.NewWriterLevel(&w, gzip.BestCompression)
  245. if err != nil {
  246. return nil, err
  247. }
  248. tw := tar.NewWriter(gw)
  249. rd := strings.TrimRight(cd.host_opts.Remote_dir, "/")
  250. seen := make(map[file_unique_id]string, 32)
  251. add := func(h *tar.Header, data []byte) (err error) {
  252. // some distro's like nix mess with installed file permissions so ensure
  253. // files are at least readable and writable by owning user
  254. h.Mode |= 0o600
  255. err = tw.WriteHeader(h)
  256. if err != nil {
  257. return
  258. }
  259. if data != nil {
  260. _, err := tw.Write(data)
  261. if err != nil {
  262. return err
  263. }
  264. }
  265. return
  266. }
  267. for _, ci := range cd.host_opts.Copy {
  268. err = ci.get_file_data(add, seen)
  269. if err != nil {
  270. return nil, err
  271. }
  272. }
  273. type fe struct {
  274. arcname string
  275. data []byte
  276. }
  277. now := time.Now()
  278. add_data := func(items ...fe) error {
  279. for _, item := range items {
  280. err := add(
  281. &tar.Header{
  282. Typeflag: tar.TypeReg, Name: item.arcname, Format: tar.FormatPAX, Size: int64(len(item.data)),
  283. Mode: 0o644, ModTime: now, ChangeTime: now, AccessTime: now,
  284. }, item.data)
  285. if err != nil {
  286. return err
  287. }
  288. }
  289. return nil
  290. }
  291. add_entries := func(prefix string, items ...shell_integration.Entry) error {
  292. for _, item := range items {
  293. err := add(
  294. &tar.Header{
  295. Typeflag: item.Metadata.Typeflag, Name: path.Join(prefix, path.Base(item.Metadata.Name)), Format: tar.FormatPAX,
  296. Size: int64(len(item.Data)), Mode: item.Metadata.Mode, ModTime: item.Metadata.ModTime,
  297. AccessTime: item.Metadata.AccessTime, ChangeTime: item.Metadata.ChangeTime,
  298. }, item.Data)
  299. if err != nil {
  300. return err
  301. }
  302. }
  303. return nil
  304. }
  305. if err = add_data(fe{"data.sh", utils.UnsafeStringToBytes(env_script)}); err != nil {
  306. return nil, err
  307. }
  308. if cd.script_type == "sh" {
  309. if err = add_data(fe{"bootstrap-utils.sh", shell_integration.Data()[path.Join("shell-integration/ssh/bootstrap-utils.sh")].Data}); err != nil {
  310. return nil, err
  311. }
  312. }
  313. if ksi != "" {
  314. for _, fname := range shell_integration.Data().FilesMatching(
  315. "shell-integration/",
  316. "shell-integration/ssh/.+", // bootstrap files are sent as command line args
  317. "shell-integration/zsh/kitty.zsh", // backward compat file not needed by ssh kitten
  318. ) {
  319. arcname := path.Join("home/", rd, "/", path.Dir(fname))
  320. err = add_entries(arcname, shell_integration.Data()[fname])
  321. if err != nil {
  322. return nil, err
  323. }
  324. }
  325. }
  326. if cd.host_opts.Remote_kitty != Remote_kitty_no {
  327. arcname := path.Join("home/", rd, "/kitty")
  328. err = add_data(fe{arcname + "/version", utils.UnsafeStringToBytes(kitty.VersionString)})
  329. if err != nil {
  330. return nil, err
  331. }
  332. for _, x := range []string{"kitty", "kitten"} {
  333. err = add_entries(path.Join(arcname, "bin"), shell_integration.Data()[path.Join("shell-integration", "ssh", x)])
  334. if err != nil {
  335. return nil, err
  336. }
  337. }
  338. }
  339. err = add_entries(path.Join("home", ".terminfo"), shell_integration.Data()["terminfo/kitty.terminfo"])
  340. if err == nil {
  341. err = add_entries(path.Join("home", ".terminfo", "x"), shell_integration.Data()["terminfo/x/"+kitty.DefaultTermName])
  342. }
  343. if err == nil {
  344. err = tw.Close()
  345. if err == nil {
  346. err = gw.Close()
  347. }
  348. }
  349. return w.Bytes(), err
  350. }
  351. func prepare_home_command(cd *connection_data) string {
  352. is_python := cd.script_type == "py"
  353. homevar := ""
  354. for _, ei := range cd.host_opts.Env {
  355. if ei.key == "HOME" && !ei.delete_on_remote {
  356. if ei.copy_from_local {
  357. homevar = os.Getenv("HOME")
  358. } else {
  359. homevar = ei.val
  360. }
  361. }
  362. }
  363. export_home_cmd := ""
  364. if homevar != "" {
  365. if is_python {
  366. export_home_cmd = base64.StdEncoding.EncodeToString(utils.UnsafeStringToBytes(homevar))
  367. } else {
  368. export_home_cmd = fmt.Sprintf("export HOME=%s; cd \"$HOME\"", utils.QuoteStringForSH(homevar))
  369. }
  370. }
  371. return export_home_cmd
  372. }
  373. func prepare_exec_cmd(cd *connection_data) string {
  374. // ssh simply concatenates multiple commands using a space see
  375. // line 1129 of ssh.c and on the remote side sshd.c runs the
  376. // concatenated command as shell -c cmd
  377. if cd.script_type == "py" {
  378. return base64.RawStdEncoding.EncodeToString(utils.UnsafeStringToBytes(strings.Join(cd.remote_args, " ")))
  379. }
  380. args := make([]string, len(cd.remote_args))
  381. for i, arg := range cd.remote_args {
  382. args[i] = strings.ReplaceAll(arg, "'", "'\"'\"'")
  383. }
  384. return "unset KITTY_SHELL_INTEGRATION; exec \"$login_shell\" -c '" + strings.Join(args, " ") + "'"
  385. }
  386. var data_shm shm.MMap
  387. func prepare_script(script string, replacements map[string]string) string {
  388. if _, found := replacements["EXEC_CMD"]; !found {
  389. replacements["EXEC_CMD"] = ""
  390. }
  391. if _, found := replacements["EXPORT_HOME_CMD"]; !found {
  392. replacements["EXPORT_HOME_CMD"] = ""
  393. }
  394. keys := utils.Keys(replacements)
  395. for i, key := range keys {
  396. keys[i] = "\\b" + key + "\\b"
  397. }
  398. pat := regexp.MustCompile(strings.Join(keys, "|"))
  399. return pat.ReplaceAllStringFunc(script, func(key string) string { return replacements[key] })
  400. }
  401. func bootstrap_script(cd *connection_data) (err error) {
  402. if cd.request_id == "" {
  403. cd.request_id = os.Getenv("KITTY_PID") + "-" + os.Getenv("KITTY_WINDOW_ID")
  404. }
  405. export_home_cmd := prepare_home_command(cd)
  406. exec_cmd := ""
  407. if len(cd.remote_args) > 0 {
  408. exec_cmd = prepare_exec_cmd(cd)
  409. }
  410. pw, err := secrets.TokenHex()
  411. if err != nil {
  412. return err
  413. }
  414. tfd, err := make_tarfile(cd, os.LookupEnv)
  415. if err != nil {
  416. return err
  417. }
  418. data := map[string]string{
  419. "tarfile": base64.StdEncoding.EncodeToString(tfd),
  420. "pw": pw,
  421. "hostname": cd.hostname_for_match, "username": cd.username,
  422. }
  423. encoded_data, err := json.Marshal(data)
  424. if err == nil && !cd.dont_create_shm {
  425. data_shm, err = shm.CreateTemp(fmt.Sprintf("kssh-%d-", os.Getpid()), uint64(len(encoded_data)+8))
  426. if err == nil {
  427. err = shm.WriteWithSize(data_shm, encoded_data, 0)
  428. if err == nil {
  429. err = data_shm.Flush()
  430. }
  431. }
  432. }
  433. if err != nil {
  434. return err
  435. }
  436. if !cd.dont_create_shm {
  437. cd.shm_name = data_shm.Name()
  438. }
  439. sensitive_data := map[string]string{"REQUEST_ID": cd.request_id, "DATA_PASSWORD": pw, "PASSWORD_FILENAME": cd.shm_name}
  440. replacements := map[string]string{
  441. "EXPORT_HOME_CMD": export_home_cmd,
  442. "EXEC_CMD": exec_cmd,
  443. "TEST_SCRIPT": cd.test_script,
  444. }
  445. add_bool := func(ok bool, key string) {
  446. if ok {
  447. replacements[key] = "1"
  448. } else {
  449. replacements[key] = "0"
  450. }
  451. }
  452. add_bool(cd.request_data, "REQUEST_DATA")
  453. add_bool(cd.echo_on, "ECHO_ON")
  454. sd := maps.Clone(replacements)
  455. if cd.request_data {
  456. maps.Copy(sd, sensitive_data)
  457. }
  458. maps.Copy(replacements, sensitive_data)
  459. cd.replacements = replacements
  460. cd.bootstrap_script = utils.UnsafeBytesToString(shell_integration.Data()["shell-integration/ssh/bootstrap."+cd.script_type].Data)
  461. cd.bootstrap_script = prepare_script(cd.bootstrap_script, sd)
  462. return err
  463. }
  464. func wrap_bootstrap_script(cd *connection_data) {
  465. // sshd will execute the command we pass it by join all command line
  466. // arguments with a space and passing it as a single argument to the users
  467. // login shell with -c. If the user has a non POSIX login shell it might
  468. // have different escaping semantics and syntax, so the command it should
  469. // execute has to be as simple as possible, basically of the form
  470. // interpreter -c unwrap_script escaped_bootstrap_script
  471. // The unwrap_script is responsible for unescaping the bootstrap script and
  472. // executing it.
  473. encoded_script := ""
  474. unwrap_script := ""
  475. if cd.script_type == "py" {
  476. encoded_script = base64.StdEncoding.EncodeToString(utils.UnsafeStringToBytes(cd.bootstrap_script))
  477. unwrap_script = `"import base64, sys; eval(compile(base64.standard_b64decode(sys.argv[-1]), 'bootstrap.py', 'exec'))"`
  478. } else {
  479. // We can't rely on base64 being available on the remote system, so instead
  480. // we quote the bootstrap script by replacing ' and \ with \v and \f
  481. // also replacing \n and ! with \r and \b for tcsh
  482. // finally surrounding with '
  483. encoded_script = "'" + strings.NewReplacer("'", "\v", "\\", "\f", "\n", "\r", "!", "\b").Replace(cd.bootstrap_script) + "'"
  484. unwrap_script = `'eval "$(echo "$0" | tr \\\v\\\f\\\r\\\b \\\047\\\134\\\n\\\041)"' `
  485. }
  486. cd.rcmd = []string{"exec", cd.host_opts.Interpreter, "-c", unwrap_script, encoded_script}
  487. }
  488. func get_remote_command(cd *connection_data) error {
  489. interpreter := cd.host_opts.Interpreter
  490. q := strings.ToLower(path.Base(interpreter))
  491. is_python := strings.Contains(q, "python")
  492. cd.script_type = "sh"
  493. if is_python {
  494. cd.script_type = "py"
  495. }
  496. err := bootstrap_script(cd)
  497. if err != nil {
  498. return err
  499. }
  500. wrap_bootstrap_script(cd)
  501. return nil
  502. }
  503. var debugprintln = tty.DebugPrintln
  504. var _ = debugprintln
  505. func drain_potential_tty_garbage(term *tty.Term) {
  506. err := term.ApplyOperations(tty.TCSANOW, tty.SetRaw)
  507. if err != nil {
  508. return
  509. }
  510. canary, err := secrets.TokenHex()
  511. if err != nil {
  512. return
  513. }
  514. dcs, err := tui.DCSToKitty("echo", canary)
  515. q := utils.UnsafeStringToBytes(canary)
  516. if err != nil {
  517. return
  518. }
  519. err = term.WriteAllString(dcs)
  520. if err != nil {
  521. return
  522. }
  523. data := make([]byte, 0)
  524. give_up_at := time.Now().Add(2 * time.Second)
  525. buf := make([]byte, 0, 8192)
  526. for !bytes.Contains(data, q) {
  527. buf = buf[:cap(buf)]
  528. timeout := time.Until(give_up_at)
  529. if timeout < 0 {
  530. break
  531. }
  532. n, err := term.ReadWithTimeout(buf, timeout)
  533. if err != nil {
  534. break
  535. }
  536. data = append(data, buf[:n]...)
  537. }
  538. }
  539. func change_colors(color_scheme string) (ans string, err error) {
  540. if color_scheme == "" {
  541. return
  542. }
  543. var theme *themes.Theme
  544. if !strings.HasSuffix(color_scheme, ".conf") {
  545. cs := os.ExpandEnv(color_scheme)
  546. tc, closer, err := themes.LoadThemes(-1)
  547. if err != nil && errors.Is(err, themes.ErrNoCacheFound) {
  548. tc, closer, err = themes.LoadThemes(time.Hour * 24)
  549. }
  550. if err != nil {
  551. return "", err
  552. }
  553. defer closer.Close()
  554. theme = tc.ThemeByName(cs)
  555. if theme == nil {
  556. return "", fmt.Errorf("No theme named %#v found", cs)
  557. }
  558. } else {
  559. theme, err = themes.ThemeFromFile(utils.ResolveConfPath(color_scheme))
  560. if err != nil {
  561. return "", err
  562. }
  563. }
  564. ans, err = theme.AsEscapeCodes()
  565. if err == nil {
  566. ans = "\033[#P" + ans
  567. }
  568. return
  569. }
  570. func run_ssh(ssh_args, server_args, found_extra_args []string) (rc int, err error) {
  571. go shell_integration.Data()
  572. go RelevantKittyOpts()
  573. defer func() {
  574. if data_shm != nil {
  575. data_shm.Close()
  576. _ = data_shm.Unlink()
  577. }
  578. }()
  579. cmd := append([]string{SSHExe()}, ssh_args...)
  580. cd := connection_data{remote_args: server_args[1:]}
  581. hostname := server_args[0]
  582. if len(cd.remote_args) == 0 {
  583. cmd = append(cmd, "-t")
  584. }
  585. insertion_point := len(cmd)
  586. cmd = append(cmd, "--", hostname)
  587. uname, hostname_for_match := get_destination(hostname)
  588. overrides, literal_env, err := parse_kitten_args(found_extra_args, uname, hostname_for_match)
  589. if err != nil {
  590. return 1, err
  591. }
  592. host_opts, bad_lines, err := load_config(hostname_for_match, uname, overrides)
  593. if err != nil {
  594. return 1, err
  595. }
  596. if len(bad_lines) > 0 {
  597. for _, x := range bad_lines {
  598. fmt.Fprintf(os.Stderr, "Ignoring bad config line: %s:%d with error: %s", filepath.Base(x.Src_file), x.Line_number, x.Err)
  599. }
  600. }
  601. if host_opts.Delegate != "" {
  602. delegate_cmd, err := shlex.Split(host_opts.Delegate)
  603. if err != nil {
  604. return 1, fmt.Errorf("Could not parse delegate command: %#v with error: %w", host_opts.Delegate, err)
  605. }
  606. return 1, unix.Exec(utils.FindExe(delegate_cmd[0]), utils.Concat(delegate_cmd, ssh_args, server_args), os.Environ())
  607. }
  608. master_is_alive, master_checked := false, false
  609. var control_master_args []string
  610. if host_opts.Share_connections {
  611. kpid, err := strconv.Atoi(os.Getenv("KITTY_PID"))
  612. if err != nil {
  613. return 1, fmt.Errorf("Invalid KITTY_PID env var not an integer: %#v", os.Getenv("KITTY_PID"))
  614. }
  615. control_master_args, err = connection_sharing_args(kpid)
  616. if err != nil {
  617. return 1, err
  618. }
  619. cmd = slices.Insert(cmd, insertion_point, control_master_args...)
  620. }
  621. use_kitty_askpass := host_opts.Askpass == Askpass_native || (host_opts.Askpass == Askpass_unless_set && os.Getenv("SSH_ASKPASS") == "")
  622. need_to_request_data := true
  623. if use_kitty_askpass {
  624. need_to_request_data = set_askpass()
  625. }
  626. master_is_functional := func() bool {
  627. if master_checked {
  628. return master_is_alive
  629. }
  630. master_checked = true
  631. check_cmd := slices.Insert(cmd, 1, "-O", "check")
  632. master_is_alive = exec.Command(check_cmd[0], check_cmd[1:]...).Run() == nil
  633. return master_is_alive
  634. }
  635. if need_to_request_data && host_opts.Share_connections && master_is_functional() {
  636. need_to_request_data = false
  637. }
  638. run_control_master := func() error {
  639. cmcmd := slices.Clone(cmd[:insertion_point])
  640. cmcmd = append(cmcmd, control_master_args...)
  641. cmcmd = append(cmcmd, "-N", "-f")
  642. cmcmd = append(cmcmd, "--", hostname)
  643. c := exec.Command(cmcmd[0], cmcmd[1:]...)
  644. c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr
  645. err := c.Run()
  646. if err != nil {
  647. err = fmt.Errorf("Failed to start SSH ControlMaster with cmdline: %s and error: %w", strings.Join(cmcmd, " "), err)
  648. }
  649. master_checked = false
  650. master_is_alive = false
  651. return err
  652. }
  653. if host_opts.Forward_remote_control && os.Getenv("KITTY_LISTEN_ON") != "" {
  654. if !host_opts.Share_connections {
  655. return 1, fmt.Errorf("Cannot use forward_remote_control=yes without share_connections=yes as it relies on SSH Controlmasters")
  656. }
  657. if !master_is_functional() {
  658. if err = run_control_master(); err != nil {
  659. return 1, err
  660. }
  661. if !master_is_functional() {
  662. return 1, fmt.Errorf("SSH ControlMaster not functional after being started explicitly")
  663. }
  664. }
  665. protocol, listen_on, found := strings.Cut(os.Getenv("KITTY_LISTEN_ON"), ":")
  666. if !found {
  667. return 1, fmt.Errorf("Invalid KITTY_LISTEN_ON: %#v", os.Getenv("KITTY_LISTEN_ON"))
  668. }
  669. if protocol == "unix" && strings.HasPrefix(listen_on, "@") {
  670. 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)
  671. }
  672. cmcmd := slices.Clone(cmd[:insertion_point])
  673. cmcmd = append(cmcmd, control_master_args...)
  674. cmcmd = append(cmcmd, "-R", "0:"+listen_on, "-O", "forward")
  675. cmcmd = append(cmcmd, "--", hostname)
  676. c := exec.Command(cmcmd[0], cmcmd[1:]...)
  677. b := bytes.Buffer{}
  678. c.Stdout = &b
  679. c.Stderr = os.Stderr
  680. if err := c.Run(); err != nil {
  681. return 1, fmt.Errorf("%s\nSetup of port forward in SSH ControlMaster failed with error: %w", b.String(), err)
  682. }
  683. port, err := strconv.Atoi(strings.TrimSpace(b.String()))
  684. if err != nil {
  685. os.Stderr.Write(b.Bytes())
  686. return 1, fmt.Errorf("Setup of port forward in SSH ControlMaster failed with error: invalid resolved port returned: %s", b.String())
  687. }
  688. cd.listen_on = "tcp:localhost:" + strconv.Itoa(port)
  689. }
  690. term, err := tty.OpenControllingTerm(tty.SetNoEcho)
  691. if err != nil {
  692. return 1, fmt.Errorf("Failed to open controlling terminal with error: %w", err)
  693. }
  694. cd.echo_on = term.WasEchoOnOriginally()
  695. cd.host_opts, cd.literal_env = host_opts, literal_env
  696. cd.request_data = need_to_request_data
  697. cd.hostname_for_match, cd.username = hostname_for_match, uname
  698. escape_codes_to_set_colors, err := change_colors(cd.host_opts.Color_scheme)
  699. if err == nil {
  700. err = term.WriteAllString(escape_codes_to_set_colors + loop.SAVE_PRIVATE_MODE_VALUES + loop.HANDLE_TERMIOS_SIGNALS.EscapeCodeToSet())
  701. }
  702. if err != nil {
  703. return 1, err
  704. }
  705. restore_escape_codes := loop.RESTORE_PRIVATE_MODE_VALUES + loop.HANDLE_TERMIOS_SIGNALS.EscapeCodeToReset()
  706. if escape_codes_to_set_colors != "" {
  707. restore_escape_codes += "\x1b[#Q"
  708. }
  709. sigs := make(chan os.Signal, 8)
  710. signal.Notify(sigs, unix.SIGINT, unix.SIGTERM)
  711. cleaned_up := false
  712. cleanup := func() {
  713. if !cleaned_up {
  714. _ = term.WriteAllString(restore_escape_codes)
  715. term.RestoreAndClose()
  716. signal.Reset()
  717. cleaned_up = true
  718. }
  719. }
  720. defer cleanup()
  721. err = get_remote_command(&cd)
  722. if err != nil {
  723. return 1, err
  724. }
  725. cmd = append(cmd, cd.rcmd...)
  726. c := exec.Command(cmd[0], cmd[1:]...)
  727. c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr
  728. err = c.Start()
  729. if err != nil {
  730. return 1, err
  731. }
  732. if !cd.request_data {
  733. rq := fmt.Sprintf("id=%s:pwfile=%s:pw=%s", cd.replacements["REQUEST_ID"], cd.replacements["PASSWORD_FILENAME"], cd.replacements["DATA_PASSWORD"])
  734. err := term.ApplyOperations(tty.TCSANOW, tty.SetNoEcho)
  735. if err == nil {
  736. var dcs string
  737. dcs, err = tui.DCSToKitty("ssh", rq)
  738. if err == nil {
  739. err = term.WriteAllString(dcs)
  740. }
  741. }
  742. if err != nil {
  743. _ = c.Process.Kill()
  744. _ = c.Wait()
  745. return 1, err
  746. }
  747. }
  748. go func() {
  749. <-sigs
  750. // ignore any interrupt and terminate signals as they will usually be sent to the ssh child process as well
  751. // and we are waiting on that.
  752. }()
  753. err = c.Wait()
  754. drain_potential_tty_garbage(term)
  755. if err != nil {
  756. var exit_err *exec.ExitError
  757. if errors.As(err, &exit_err) {
  758. if state := exit_err.ProcessState.String(); state == "signal: interrupt" {
  759. cleanup()
  760. _ = unix.Kill(os.Getpid(), unix.SIGINT)
  761. // Give the signal time to be delivered
  762. time.Sleep(20 * time.Millisecond)
  763. }
  764. return exit_err.ExitCode(), nil
  765. }
  766. return 1, err
  767. }
  768. return 0, nil
  769. }
  770. func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
  771. if len(args) > 0 {
  772. switch args[0] {
  773. case "use-python":
  774. args = args[1:] // backwards compat from when we had a python implementation
  775. case "-h", "--help":
  776. cmd.ShowHelp()
  777. return
  778. }
  779. }
  780. ssh_args, server_args, passthrough, found_extra_args, err := ParseSSHArgs(args, "--kitten")
  781. if err != nil {
  782. var invargs *ErrInvalidSSHArgs
  783. switch {
  784. case errors.As(err, &invargs):
  785. if invargs.Msg != "" {
  786. fmt.Fprintln(os.Stderr, invargs.Msg)
  787. }
  788. return 1, unix.Exec(SSHExe(), []string{"ssh"}, os.Environ())
  789. }
  790. return 1, err
  791. }
  792. if passthrough {
  793. return 1, unix.Exec(SSHExe(), utils.Concat([]string{"ssh"}, ssh_args, server_args), os.Environ())
  794. }
  795. if os.Getenv("KITTY_WINDOW_ID") == "" || os.Getenv("KITTY_PID") == "" {
  796. return 1, fmt.Errorf("The SSH kitten is meant to run inside a kitty window")
  797. }
  798. if !tty.IsTerminal(os.Stdin.Fd()) {
  799. return 1, fmt.Errorf("The SSH kitten is meant for interactive use only, STDIN must be a terminal")
  800. }
  801. return run_ssh(ssh_args, server_args, found_extra_args)
  802. }
  803. func EntryPoint(parent *cli.Command) {
  804. create_cmd(parent, main)
  805. }
  806. func specialize_command(ssh *cli.Command) {
  807. ssh.Usage = "arguments for the ssh command"
  808. ssh.ShortDescription = "Truly convenient SSH"
  809. 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`."
  810. ssh.IgnoreAllArgs = true
  811. ssh.OnlyArgsAllowed = true
  812. ssh.ArgCompleter = cli.CompletionForWrapper("ssh")
  813. }
  814. func test_integration_with_python(args []string) (rc int, err error) {
  815. f, err := os.CreateTemp("", "*.conf")
  816. if err != nil {
  817. return 1, err
  818. }
  819. defer func() {
  820. f.Close()
  821. os.Remove(f.Name())
  822. }()
  823. _, err = io.Copy(f, os.Stdin)
  824. if err != nil {
  825. return 1, err
  826. }
  827. cd := &connection_data{
  828. request_id: "testing", remote_args: []string{},
  829. username: "testuser", hostname_for_match: "host.test", request_data: true,
  830. test_script: args[0], echo_on: true,
  831. }
  832. opts, bad_lines, err := load_config(cd.hostname_for_match, cd.username, nil, f.Name())
  833. if err == nil {
  834. if len(bad_lines) > 0 {
  835. return 1, fmt.Errorf("Bad config lines: %s with error: %s", bad_lines[0].Line, bad_lines[0].Err)
  836. }
  837. cd.host_opts = opts
  838. err = get_remote_command(cd)
  839. }
  840. if err != nil {
  841. return 1, err
  842. }
  843. data, err := json.Marshal(map[string]any{"cmd": cd.rcmd, "shm_name": cd.shm_name})
  844. if err == nil {
  845. _, err = os.Stdout.Write(data)
  846. os.Stdout.Close()
  847. }
  848. if err != nil {
  849. return 1, err
  850. }
  851. return
  852. }
  853. func TestEntryPoint(root *cli.Command) {
  854. root.AddSubCommand(&cli.Command{
  855. Name: "ssh",
  856. OnlyArgsAllowed: true,
  857. Run: func(cmd *cli.Command, args []string) (rc int, err error) {
  858. return test_integration_with_python(args)
  859. },
  860. })
  861. }