config.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package ssh
  3. import (
  4. "archive/tar"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "os"
  9. "path"
  10. "path/filepath"
  11. "strings"
  12. "time"
  13. "kitty/tools/config"
  14. "kitty/tools/utils"
  15. "kitty/tools/utils/paths"
  16. "kitty/tools/utils/shlex"
  17. "github.com/bmatcuk/doublestar/v4"
  18. "golang.org/x/sys/unix"
  19. )
  20. var _ = fmt.Print
  21. type EnvInstruction struct {
  22. key, val string
  23. delete_on_remote, copy_from_local, literal_quote bool
  24. }
  25. func quote_for_sh(val string, literal_quote bool) string {
  26. if literal_quote {
  27. return utils.QuoteStringForSH(val)
  28. }
  29. // See https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
  30. b := strings.Builder{}
  31. b.Grow(len(val) + 16)
  32. b.WriteRune('"')
  33. runes := []rune(val)
  34. for i, ch := range runes {
  35. if ch == '\\' || ch == '`' || ch == '"' || (ch == '$' && i+1 < len(runes) && runes[i+1] == '(') {
  36. // special chars are escaped
  37. // $( is escaped to prevent execution
  38. b.WriteRune('\\')
  39. }
  40. b.WriteRune(ch)
  41. }
  42. b.WriteRune('"')
  43. return b.String()
  44. }
  45. func (self *EnvInstruction) Serialize(for_python bool, get_local_env func(string) (string, bool)) string {
  46. var unset func() string
  47. var export func(string) string
  48. if for_python {
  49. dumps := func(x ...any) string {
  50. ans, _ := json.Marshal(x)
  51. return utils.UnsafeBytesToString(ans)
  52. }
  53. export = func(val string) string {
  54. if val == "" {
  55. return fmt.Sprintf("export %s", dumps(self.key))
  56. }
  57. return fmt.Sprintf("export %s", dumps(self.key, val, self.literal_quote))
  58. }
  59. unset = func() string {
  60. return fmt.Sprintf("unset %s", dumps(self.key))
  61. }
  62. } else {
  63. kq := utils.QuoteStringForSH(self.key)
  64. unset = func() string {
  65. return fmt.Sprintf("unset %s", kq)
  66. }
  67. export = func(val string) string {
  68. return fmt.Sprintf("export %s=%s", kq, quote_for_sh(val, self.literal_quote))
  69. }
  70. }
  71. if self.delete_on_remote {
  72. return unset()
  73. }
  74. if self.copy_from_local {
  75. val, found := get_local_env(self.key)
  76. if !found {
  77. return ""
  78. }
  79. return export(val)
  80. }
  81. return export(self.val)
  82. }
  83. func final_env_instructions(for_python bool, get_local_env func(string) (string, bool), env ...*EnvInstruction) string {
  84. seen := make(map[string]int, len(env))
  85. ans := make([]string, 0, len(env))
  86. for _, ei := range env {
  87. q := ei.Serialize(for_python, get_local_env)
  88. if q != "" {
  89. if pos, found := seen[ei.key]; found {
  90. ans[pos] = q
  91. } else {
  92. seen[ei.key] = len(ans)
  93. ans = append(ans, q)
  94. }
  95. }
  96. }
  97. return strings.Join(ans, "\n")
  98. }
  99. type CopyInstruction struct {
  100. local_path, arcname string
  101. exclude_patterns []string
  102. }
  103. func ParseEnvInstruction(spec string) (ans []*EnvInstruction, err error) {
  104. const COPY_FROM_LOCAL string = "_kitty_copy_env_var_"
  105. ei := &EnvInstruction{}
  106. found := false
  107. ei.key, ei.val, found = strings.Cut(spec, "=")
  108. ei.key = strings.TrimSpace(ei.key)
  109. if found {
  110. ei.val = strings.TrimSpace(ei.val)
  111. if ei.val == COPY_FROM_LOCAL {
  112. ei.val = ""
  113. ei.copy_from_local = true
  114. }
  115. } else {
  116. ei.delete_on_remote = true
  117. }
  118. if ei.key == "" {
  119. err = fmt.Errorf("The env directive must not be empty")
  120. }
  121. ans = []*EnvInstruction{ei}
  122. return
  123. }
  124. var paths_ctx *paths.Ctx
  125. func resolve_file_spec(spec string, is_glob bool) ([]string, error) {
  126. if paths_ctx == nil {
  127. paths_ctx = &paths.Ctx{}
  128. }
  129. ans := os.ExpandEnv(paths_ctx.ExpandHome(spec))
  130. if !filepath.IsAbs(ans) {
  131. ans = paths_ctx.AbspathFromHome(ans)
  132. }
  133. if is_glob {
  134. files, err := doublestar.FilepathGlob(ans)
  135. if err != nil {
  136. return nil, fmt.Errorf("%s is not a valid glob pattern with error: %w", spec, err)
  137. }
  138. if len(files) == 0 {
  139. return nil, fmt.Errorf("%s matches no files", spec)
  140. }
  141. return files, nil
  142. }
  143. err := unix.Access(ans, unix.R_OK)
  144. if err != nil {
  145. if errors.Is(err, os.ErrNotExist) {
  146. return nil, fmt.Errorf("%s does not exist", spec)
  147. }
  148. return nil, fmt.Errorf("Cannot read from: %s with error: %w", spec, err)
  149. }
  150. return []string{ans}, nil
  151. }
  152. func get_arcname(loc, dest, home string) (arcname string) {
  153. if dest != "" {
  154. arcname = dest
  155. } else {
  156. arcname = filepath.Clean(loc)
  157. if strings.HasPrefix(arcname, home) {
  158. ra, err := filepath.Rel(home, arcname)
  159. if err == nil {
  160. arcname = ra
  161. }
  162. }
  163. }
  164. prefix := "home/"
  165. if strings.HasPrefix(arcname, "/") {
  166. prefix = "root"
  167. }
  168. return prefix + arcname
  169. }
  170. func ParseCopyInstruction(spec string) (ans []*CopyInstruction, err error) {
  171. args, err := shlex.Split("copy " + spec)
  172. if err != nil {
  173. return nil, err
  174. }
  175. opts, args, err := parse_copy_args(args)
  176. if err != nil {
  177. return nil, err
  178. }
  179. locations := make([]string, 0, len(args))
  180. for _, arg := range args {
  181. locs, err := resolve_file_spec(arg, opts.Glob)
  182. if err != nil {
  183. return nil, err
  184. }
  185. locations = append(locations, locs...)
  186. }
  187. if len(locations) == 0 {
  188. return nil, fmt.Errorf("No files to copy specified")
  189. }
  190. if len(locations) > 1 && opts.Dest != "" {
  191. return nil, fmt.Errorf("Specifying a remote location with more than one file is not supported")
  192. }
  193. home := paths_ctx.HomePath()
  194. ans = make([]*CopyInstruction, 0, len(locations))
  195. for _, loc := range locations {
  196. ci := CopyInstruction{local_path: loc, exclude_patterns: opts.Exclude}
  197. if opts.SymlinkStrategy != "preserve" {
  198. ci.local_path, err = filepath.EvalSymlinks(loc)
  199. if err != nil {
  200. return nil, fmt.Errorf("Failed to resolve symlinks in %#v with error: %w", loc, err)
  201. }
  202. }
  203. if opts.SymlinkStrategy == "resolve" {
  204. ci.arcname = get_arcname(ci.local_path, opts.Dest, home)
  205. } else {
  206. ci.arcname = get_arcname(loc, opts.Dest, home)
  207. }
  208. ans = append(ans, &ci)
  209. }
  210. return
  211. }
  212. type file_unique_id struct {
  213. dev, inode uint64
  214. }
  215. func excluded(pattern, path string) bool {
  216. if !strings.ContainsRune(pattern, '/') {
  217. path = filepath.Base(path)
  218. }
  219. if matched, err := doublestar.PathMatch(pattern, path); matched && err == nil {
  220. return true
  221. }
  222. return false
  223. }
  224. func get_file_data(callback func(h *tar.Header, data []byte) error, seen map[file_unique_id]string, local_path, arcname string, exclude_patterns []string) error {
  225. var s unix.Stat_t
  226. if err := unix.Lstat(local_path, &s); err != nil {
  227. return err
  228. }
  229. cb := func(h *tar.Header, data []byte, arcname string) error {
  230. h.Name = arcname
  231. if h.Typeflag == tar.TypeDir {
  232. h.Name = strings.TrimRight(h.Name, "/") + "/"
  233. }
  234. h.Size = int64(len(data))
  235. h.Mode = int64(s.Mode & 0777) // discard the setuid, setgid and sticky bits
  236. h.ModTime = time.Unix(s.Mtim.Unix())
  237. h.AccessTime = time.Unix(s.Atim.Unix())
  238. h.ChangeTime = time.Unix(s.Ctim.Unix())
  239. h.Format = tar.FormatPAX
  240. return callback(h, data)
  241. }
  242. // we only copy regular files, directories and symlinks
  243. switch s.Mode & unix.S_IFMT {
  244. case unix.S_IFBLK, unix.S_IFIFO, unix.S_IFCHR, unix.S_IFSOCK: // ignored
  245. case unix.S_IFLNK: // symlink
  246. target, err := os.Readlink(local_path)
  247. if err != nil {
  248. return err
  249. }
  250. err = cb(&tar.Header{
  251. Typeflag: tar.TypeSymlink,
  252. Linkname: target,
  253. }, nil, arcname)
  254. if err != nil {
  255. return err
  256. }
  257. case unix.S_IFDIR: // directory
  258. local_path = filepath.Clean(local_path)
  259. type entry struct {
  260. path, arcname string
  261. }
  262. stack := []entry{{local_path, arcname}}
  263. for len(stack) > 0 {
  264. x := stack[0]
  265. stack = stack[1:]
  266. entries, err := os.ReadDir(x.path)
  267. if err != nil {
  268. if x.path == local_path {
  269. return err
  270. }
  271. continue
  272. }
  273. err = cb(&tar.Header{Typeflag: tar.TypeDir}, nil, x.arcname)
  274. if err != nil {
  275. return err
  276. }
  277. for _, e := range entries {
  278. entry_path := filepath.Join(x.path, e.Name())
  279. aname := path.Join(x.arcname, e.Name())
  280. ok := true
  281. for _, pat := range exclude_patterns {
  282. if excluded(pat, entry_path) {
  283. ok = false
  284. break
  285. }
  286. }
  287. if !ok {
  288. continue
  289. }
  290. if e.IsDir() {
  291. stack = append(stack, entry{entry_path, aname})
  292. } else {
  293. err = get_file_data(callback, seen, entry_path, aname, exclude_patterns)
  294. if err != nil {
  295. return err
  296. }
  297. }
  298. }
  299. }
  300. case unix.S_IFREG: // Regular file
  301. fid := file_unique_id{dev: uint64(s.Dev), inode: uint64(s.Ino)}
  302. if prev, ok := seen[fid]; ok { // Hard link
  303. return cb(&tar.Header{Typeflag: tar.TypeLink, Linkname: prev}, nil, arcname)
  304. }
  305. seen[fid] = arcname
  306. data, err := os.ReadFile(local_path)
  307. if err != nil {
  308. return err
  309. }
  310. err = cb(&tar.Header{Typeflag: tar.TypeReg}, data, arcname)
  311. if err != nil {
  312. return err
  313. }
  314. }
  315. return nil
  316. }
  317. func (ci *CopyInstruction) get_file_data(callback func(h *tar.Header, data []byte) error, seen map[file_unique_id]string) (err error) {
  318. ep := ci.exclude_patterns
  319. for _, folder_name := range []string{"__pycache__", ".DS_Store"} {
  320. ep = append(ep, "**/"+folder_name, "**/"+folder_name+"/**")
  321. }
  322. return get_file_data(callback, seen, ci.local_path, ci.arcname, ep)
  323. }
  324. type ConfigSet struct {
  325. all_configs []*Config
  326. }
  327. func config_for_hostname(hostname_to_match, username_to_match string, cs *ConfigSet) *Config {
  328. matcher := func(q *Config) bool {
  329. for _, pat := range strings.Split(q.Hostname, " ") {
  330. upat := "*"
  331. if strings.Contains(pat, "@") {
  332. upat, pat, _ = strings.Cut(pat, "@")
  333. }
  334. var host_matched, user_matched bool
  335. if matched, err := filepath.Match(pat, hostname_to_match); matched && err == nil {
  336. host_matched = true
  337. }
  338. if matched, err := filepath.Match(upat, username_to_match); matched && err == nil {
  339. user_matched = true
  340. }
  341. if host_matched && user_matched {
  342. return true
  343. }
  344. }
  345. return false
  346. }
  347. for _, c := range utils.Reversed(cs.all_configs) {
  348. if matcher(c) {
  349. return c
  350. }
  351. }
  352. return cs.all_configs[0]
  353. }
  354. func (self *ConfigSet) line_handler(key, val string) error {
  355. c := self.all_configs[len(self.all_configs)-1]
  356. if key == "hostname" {
  357. c = NewConfig()
  358. self.all_configs = append(self.all_configs, c)
  359. }
  360. return c.Parse(key, val)
  361. }
  362. func load_config(hostname_to_match string, username_to_match string, overrides []string, paths ...string) (*Config, []config.ConfigLine, error) {
  363. ans := &ConfigSet{all_configs: []*Config{NewConfig()}}
  364. p := config.ConfigParser{LineHandler: ans.line_handler}
  365. err := p.LoadConfig("ssh.conf", paths, nil)
  366. if err != nil {
  367. return nil, nil, err
  368. }
  369. final_conf := config_for_hostname(hostname_to_match, username_to_match, ans)
  370. bad_lines := p.BadLines()
  371. if len(overrides) > 0 {
  372. h := final_conf.Hostname
  373. override_parser := config.ConfigParser{LineHandler: final_conf.Parse}
  374. if err = override_parser.ParseOverrides(overrides...); err != nil {
  375. return nil, nil, err
  376. }
  377. bad_lines = append(bad_lines, override_parser.BadLines()...)
  378. final_conf.Hostname = h
  379. }
  380. return final_conf, bad_lines, nil
  381. }