main.go 27 KB

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