You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

config.go 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. // Package ssh_config provides tools for manipulating SSH config files.
  2. //
  3. // Importantly, this parser attempts to preserve comments in a given file, so
  4. // you can manipulate a `ssh_config` file from a program, if your heart desires.
  5. //
  6. // The Get() and GetStrict() functions will attempt to read values from
  7. // $HOME/.ssh/config, falling back to /etc/ssh/ssh_config. The first argument is
  8. // the host name to match on ("example.com"), and the second argument is the key
  9. // you want to retrieve ("Port"). The keywords are case insensitive.
  10. //
  11. // port := ssh_config.Get("myhost", "Port")
  12. //
  13. // You can also manipulate an SSH config file and then print it or write it back
  14. // to disk.
  15. //
  16. // f, _ := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "config"))
  17. // cfg, _ := ssh_config.Decode(f)
  18. // for _, host := range cfg.Hosts {
  19. // fmt.Println("patterns:", host.Patterns)
  20. // for _, node := range host.Nodes {
  21. // fmt.Println(node.String())
  22. // }
  23. // }
  24. //
  25. // // Write the cfg back to disk:
  26. // fmt.Println(cfg.String())
  27. //
  28. // BUG: the Match directive is currently unsupported; parsing a config with
  29. // a Match directive will trigger an error.
  30. package ssh_config
  31. import (
  32. "bytes"
  33. "errors"
  34. "fmt"
  35. "io"
  36. "os"
  37. osuser "os/user"
  38. "path/filepath"
  39. "regexp"
  40. "runtime"
  41. "strings"
  42. "sync"
  43. )
  44. const version = "0.5"
  45. type configFinder func() string
  46. // UserSettings checks ~/.ssh and /etc/ssh for configuration files. The config
  47. // files are parsed and cached the first time Get() or GetStrict() is called.
  48. type UserSettings struct {
  49. IgnoreErrors bool
  50. systemConfig *Config
  51. systemConfigFinder configFinder
  52. userConfig *Config
  53. userConfigFinder configFinder
  54. loadConfigs sync.Once
  55. onceErr error
  56. }
  57. func homedir() string {
  58. user, err := osuser.Current()
  59. if err == nil {
  60. return user.HomeDir
  61. } else {
  62. return os.Getenv("HOME")
  63. }
  64. }
  65. func userConfigFinder() string {
  66. return filepath.Join(homedir(), ".ssh", "config")
  67. }
  68. // DefaultUserSettings is the default UserSettings and is used by Get and
  69. // GetStrict. It checks both $HOME/.ssh/config and /etc/ssh/ssh_config for keys,
  70. // and it will return parse errors (if any) instead of swallowing them.
  71. var DefaultUserSettings = &UserSettings{
  72. IgnoreErrors: false,
  73. systemConfigFinder: systemConfigFinder,
  74. userConfigFinder: userConfigFinder,
  75. }
  76. func systemConfigFinder() string {
  77. return filepath.Join("/", "etc", "ssh", "ssh_config")
  78. }
  79. func findVal(c *Config, alias, key string) (string, error) {
  80. if c == nil {
  81. return "", nil
  82. }
  83. val, err := c.Get(alias, key)
  84. if err != nil || val == "" {
  85. return "", err
  86. }
  87. if err := validate(key, val); err != nil {
  88. return "", err
  89. }
  90. return val, nil
  91. }
  92. // Get finds the first value for key within a declaration that matches the
  93. // alias. Get returns the empty string if no value was found, or if IgnoreErrors
  94. // is false and we could not parse the configuration file. Use GetStrict to
  95. // disambiguate the latter cases.
  96. //
  97. // The match for key is case insensitive.
  98. //
  99. // Get is a wrapper around DefaultUserSettings.Get.
  100. func Get(alias, key string) string {
  101. return DefaultUserSettings.Get(alias, key)
  102. }
  103. // GetStrict finds the first value for key within a declaration that matches the
  104. // alias. If key has a default value and no matching configuration is found, the
  105. // default will be returned. For more information on default values and the way
  106. // patterns are matched, see the manpage for ssh_config.
  107. //
  108. // error will be non-nil if and only if a user's configuration file or the
  109. // system configuration file could not be parsed, and u.IgnoreErrors is false.
  110. //
  111. // GetStrict is a wrapper around DefaultUserSettings.GetStrict.
  112. func GetStrict(alias, key string) (string, error) {
  113. return DefaultUserSettings.GetStrict(alias, key)
  114. }
  115. // Get finds the first value for key within a declaration that matches the
  116. // alias. Get returns the empty string if no value was found, or if IgnoreErrors
  117. // is false and we could not parse the configuration file. Use GetStrict to
  118. // disambiguate the latter cases.
  119. //
  120. // The match for key is case insensitive.
  121. func (u *UserSettings) Get(alias, key string) string {
  122. val, err := u.GetStrict(alias, key)
  123. if err != nil {
  124. return ""
  125. }
  126. return val
  127. }
  128. // GetStrict finds the first value for key within a declaration that matches the
  129. // alias. If key has a default value and no matching configuration is found, the
  130. // default will be returned. For more information on default values and the way
  131. // patterns are matched, see the manpage for ssh_config.
  132. //
  133. // error will be non-nil if and only if a user's configuration file or the
  134. // system configuration file could not be parsed, and u.IgnoreErrors is false.
  135. func (u *UserSettings) GetStrict(alias, key string) (string, error) {
  136. u.loadConfigs.Do(func() {
  137. // can't parse user file, that's ok.
  138. var filename string
  139. if u.userConfigFinder == nil {
  140. filename = userConfigFinder()
  141. } else {
  142. filename = u.userConfigFinder()
  143. }
  144. var err error
  145. u.userConfig, err = parseFile(filename)
  146. if err != nil && os.IsNotExist(err) == false {
  147. u.onceErr = err
  148. return
  149. }
  150. if u.systemConfigFinder == nil {
  151. filename = systemConfigFinder()
  152. } else {
  153. filename = u.systemConfigFinder()
  154. }
  155. u.systemConfig, err = parseFile(filename)
  156. if err != nil && os.IsNotExist(err) == false {
  157. u.onceErr = err
  158. return
  159. }
  160. })
  161. if u.onceErr != nil && u.IgnoreErrors == false {
  162. return "", u.onceErr
  163. }
  164. val, err := findVal(u.userConfig, alias, key)
  165. if err != nil || val != "" {
  166. return val, err
  167. }
  168. val2, err2 := findVal(u.systemConfig, alias, key)
  169. if err2 != nil || val2 != "" {
  170. return val2, err2
  171. }
  172. return Default(key), nil
  173. }
  174. func parseFile(filename string) (*Config, error) {
  175. return parseWithDepth(filename, 0)
  176. }
  177. func parseWithDepth(filename string, depth uint8) (*Config, error) {
  178. f, err := os.Open(filename)
  179. if err != nil {
  180. return nil, err
  181. }
  182. defer f.Close()
  183. return decode(f, isSystem(filename), depth)
  184. }
  185. func isSystem(filename string) bool {
  186. // TODO i'm not sure this is the best way to detect a system repo
  187. return strings.HasPrefix(filepath.Clean(filename), "/etc/ssh")
  188. }
  189. // Decode reads r into a Config, or returns an error if r could not be parsed as
  190. // an SSH config file.
  191. func Decode(r io.Reader) (*Config, error) {
  192. return decode(r, false, 0)
  193. }
  194. func decode(r io.Reader, system bool, depth uint8) (c *Config, err error) {
  195. defer func() {
  196. if r := recover(); r != nil {
  197. if _, ok := r.(runtime.Error); ok {
  198. panic(r)
  199. }
  200. if e, ok := r.(error); ok && e == ErrDepthExceeded {
  201. err = e
  202. return
  203. }
  204. err = errors.New(r.(string))
  205. }
  206. }()
  207. c = parseSSH(lexSSH(r), system, depth)
  208. return c, err
  209. }
  210. // Config represents an SSH config file.
  211. type Config struct {
  212. // A list of hosts to match against. The file begins with an implicit
  213. // "Host *" declaration matching all hosts.
  214. Hosts []*Host
  215. depth uint8
  216. position Position
  217. }
  218. // Get finds the first value in the configuration that matches the alias and
  219. // contains key. Get returns the empty string if no value was found, or if the
  220. // Config contains an invalid conditional Include value.
  221. //
  222. // The match for key is case insensitive.
  223. func (c *Config) Get(alias, key string) (string, error) {
  224. lowerKey := strings.ToLower(key)
  225. for _, host := range c.Hosts {
  226. if !host.Matches(alias) {
  227. continue
  228. }
  229. for _, node := range host.Nodes {
  230. switch t := node.(type) {
  231. case *Empty:
  232. continue
  233. case *KV:
  234. // "keys are case insensitive" per the spec
  235. lkey := strings.ToLower(t.Key)
  236. if lkey == "match" {
  237. panic("can't handle Match directives")
  238. }
  239. if lkey == lowerKey {
  240. return t.Value, nil
  241. }
  242. case *Include:
  243. val := t.Get(alias, key)
  244. if val != "" {
  245. return val, nil
  246. }
  247. default:
  248. return "", fmt.Errorf("unknown Node type %v", t)
  249. }
  250. }
  251. }
  252. return "", nil
  253. }
  254. // String returns a string representation of the Config file.
  255. func (c Config) String() string {
  256. return marshal(c).String()
  257. }
  258. func (c Config) MarshalText() ([]byte, error) {
  259. return marshal(c).Bytes(), nil
  260. }
  261. func marshal(c Config) *bytes.Buffer {
  262. var buf bytes.Buffer
  263. for i := range c.Hosts {
  264. buf.WriteString(c.Hosts[i].String())
  265. }
  266. return &buf
  267. }
  268. // Pattern is a pattern in a Host declaration. Patterns are read-only values;
  269. // create a new one with NewPattern().
  270. type Pattern struct {
  271. str string // Its appearance in the file, not the value that gets compiled.
  272. regex *regexp.Regexp
  273. not bool // True if this is a negated match
  274. }
  275. // String prints the string representation of the pattern.
  276. func (p Pattern) String() string {
  277. return p.str
  278. }
  279. // Copied from regexp.go with * and ? removed.
  280. var specialBytes = []byte(`\.+()|[]{}^$`)
  281. func special(b byte) bool {
  282. return bytes.IndexByte(specialBytes, b) >= 0
  283. }
  284. // NewPattern creates a new Pattern for matching hosts. NewPattern("*") creates
  285. // a Pattern that matches all hosts.
  286. //
  287. // From the manpage, a pattern consists of zero or more non-whitespace
  288. // characters, `*' (a wildcard that matches zero or more characters), or `?' (a
  289. // wildcard that matches exactly one character). For example, to specify a set
  290. // of declarations for any host in the ".co.uk" set of domains, the following
  291. // pattern could be used:
  292. //
  293. // Host *.co.uk
  294. //
  295. // The following pattern would match any host in the 192.168.0.[0-9] network range:
  296. //
  297. // Host 192.168.0.?
  298. func NewPattern(s string) (*Pattern, error) {
  299. if s == "" {
  300. return nil, errors.New("ssh_config: empty pattern")
  301. }
  302. negated := false
  303. if s[0] == '!' {
  304. negated = true
  305. s = s[1:]
  306. }
  307. var buf bytes.Buffer
  308. buf.WriteByte('^')
  309. for i := 0; i < len(s); i++ {
  310. // A byte loop is correct because all metacharacters are ASCII.
  311. switch b := s[i]; b {
  312. case '*':
  313. buf.WriteString(".*")
  314. case '?':
  315. buf.WriteString(".?")
  316. default:
  317. // borrowing from QuoteMeta here.
  318. if special(b) {
  319. buf.WriteByte('\\')
  320. }
  321. buf.WriteByte(b)
  322. }
  323. }
  324. buf.WriteByte('$')
  325. r, err := regexp.Compile(buf.String())
  326. if err != nil {
  327. return nil, err
  328. }
  329. return &Pattern{str: s, regex: r, not: negated}, nil
  330. }
  331. // Host describes a Host directive and the keywords that follow it.
  332. type Host struct {
  333. // A list of host patterns that should match this host.
  334. Patterns []*Pattern
  335. // A Node is either a key/value pair or a comment line.
  336. Nodes []Node
  337. // EOLComment is the comment (if any) terminating the Host line.
  338. EOLComment string
  339. hasEquals bool
  340. leadingSpace uint16 // TODO: handle spaces vs tabs here.
  341. // The file starts with an implicit "Host *" declaration.
  342. implicit bool
  343. }
  344. // Matches returns true if the Host matches for the given alias. For
  345. // a description of the rules that provide a match, see the manpage for
  346. // ssh_config.
  347. func (h *Host) Matches(alias string) bool {
  348. found := false
  349. for i := range h.Patterns {
  350. if h.Patterns[i].regex.MatchString(alias) {
  351. if h.Patterns[i].not == true {
  352. // Negated match. "A pattern entry may be negated by prefixing
  353. // it with an exclamation mark (`!'). If a negated entry is
  354. // matched, then the Host entry is ignored, regardless of
  355. // whether any other patterns on the line match. Negated matches
  356. // are therefore useful to provide exceptions for wildcard
  357. // matches."
  358. return false
  359. }
  360. found = true
  361. }
  362. }
  363. return found
  364. }
  365. // String prints h as it would appear in a config file. Minor tweaks may be
  366. // present in the whitespace in the printed file.
  367. func (h *Host) String() string {
  368. var buf bytes.Buffer
  369. if h.implicit == false {
  370. buf.WriteString(strings.Repeat(" ", int(h.leadingSpace)))
  371. buf.WriteString("Host")
  372. if h.hasEquals {
  373. buf.WriteString(" = ")
  374. } else {
  375. buf.WriteString(" ")
  376. }
  377. for i, pat := range h.Patterns {
  378. buf.WriteString(pat.String())
  379. if i < len(h.Patterns)-1 {
  380. buf.WriteString(" ")
  381. }
  382. }
  383. if h.EOLComment != "" {
  384. buf.WriteString(" #")
  385. buf.WriteString(h.EOLComment)
  386. }
  387. buf.WriteByte('\n')
  388. }
  389. for i := range h.Nodes {
  390. buf.WriteString(h.Nodes[i].String())
  391. buf.WriteByte('\n')
  392. }
  393. return buf.String()
  394. }
  395. // Node represents a line in a Config.
  396. type Node interface {
  397. Pos() Position
  398. String() string
  399. }
  400. // KV is a line in the config file that contains a key, a value, and possibly
  401. // a comment.
  402. type KV struct {
  403. Key string
  404. Value string
  405. Comment string
  406. hasEquals bool
  407. leadingSpace uint16 // Space before the key. TODO handle spaces vs tabs.
  408. position Position
  409. }
  410. // Pos returns k's Position.
  411. func (k *KV) Pos() Position {
  412. return k.position
  413. }
  414. // String prints k as it was parsed in the config file. There may be slight
  415. // changes to the whitespace between values.
  416. func (k *KV) String() string {
  417. if k == nil {
  418. return ""
  419. }
  420. equals := " "
  421. if k.hasEquals {
  422. equals = " = "
  423. }
  424. line := fmt.Sprintf("%s%s%s%s", strings.Repeat(" ", int(k.leadingSpace)), k.Key, equals, k.Value)
  425. if k.Comment != "" {
  426. line += " #" + k.Comment
  427. }
  428. return line
  429. }
  430. // Empty is a line in the config file that contains only whitespace or comments.
  431. type Empty struct {
  432. Comment string
  433. leadingSpace uint16 // TODO handle spaces vs tabs.
  434. position Position
  435. }
  436. // Pos returns e's Position.
  437. func (e *Empty) Pos() Position {
  438. return e.position
  439. }
  440. // String prints e as it was parsed in the config file.
  441. func (e *Empty) String() string {
  442. if e == nil {
  443. return ""
  444. }
  445. if e.Comment == "" {
  446. return ""
  447. }
  448. return fmt.Sprintf("%s#%s", strings.Repeat(" ", int(e.leadingSpace)), e.Comment)
  449. }
  450. // Include holds the result of an Include directive, including the config files
  451. // that have been parsed as part of that directive. At most 5 levels of Include
  452. // statements will be parsed.
  453. type Include struct {
  454. // Comment is the contents of any comment at the end of the Include
  455. // statement.
  456. Comment string
  457. parsed bool
  458. // an include directive can include several different files, and wildcards
  459. directives []string
  460. mu sync.Mutex
  461. // 1:1 mapping between matches and keys in files array; matches preserves
  462. // ordering
  463. matches []string
  464. // actual filenames are listed here
  465. files map[string]*Config
  466. leadingSpace uint16
  467. position Position
  468. depth uint8
  469. hasEquals bool
  470. }
  471. const maxRecurseDepth = 5
  472. // ErrDepthExceeded is returned if too many Include directives are parsed.
  473. // Usually this indicates a recursive loop (an Include directive pointing to the
  474. // file it contains).
  475. var ErrDepthExceeded = errors.New("ssh_config: max recurse depth exceeded")
  476. func removeDups(arr []string) []string {
  477. // Use map to record duplicates as we find them.
  478. encountered := make(map[string]bool, len(arr))
  479. result := make([]string, 0)
  480. for v := range arr {
  481. if encountered[arr[v]] == false {
  482. encountered[arr[v]] = true
  483. result = append(result, arr[v])
  484. }
  485. }
  486. return result
  487. }
  488. // NewInclude creates a new Include with a list of file globs to include.
  489. // Configuration files are parsed greedily (e.g. as soon as this function runs).
  490. // Any error encountered while parsing nested configuration files will be
  491. // returned.
  492. func NewInclude(directives []string, hasEquals bool, pos Position, comment string, system bool, depth uint8) (*Include, error) {
  493. if depth > maxRecurseDepth {
  494. return nil, ErrDepthExceeded
  495. }
  496. inc := &Include{
  497. Comment: comment,
  498. directives: directives,
  499. files: make(map[string]*Config),
  500. position: pos,
  501. leadingSpace: uint16(pos.Col) - 1,
  502. depth: depth,
  503. hasEquals: hasEquals,
  504. }
  505. // no need for inc.mu.Lock() since nothing else can access this inc
  506. matches := make([]string, 0)
  507. for i := range directives {
  508. var path string
  509. if filepath.IsAbs(directives[i]) {
  510. path = directives[i]
  511. } else if system {
  512. path = filepath.Join("/etc/ssh", directives[i])
  513. } else {
  514. path = filepath.Join(homedir(), ".ssh", directives[i])
  515. }
  516. theseMatches, err := filepath.Glob(path)
  517. if err != nil {
  518. return nil, err
  519. }
  520. matches = append(matches, theseMatches...)
  521. }
  522. matches = removeDups(matches)
  523. inc.matches = matches
  524. for i := range matches {
  525. config, err := parseWithDepth(matches[i], depth)
  526. if err != nil {
  527. return nil, err
  528. }
  529. inc.files[matches[i]] = config
  530. }
  531. return inc, nil
  532. }
  533. // Pos returns the position of the Include directive in the larger file.
  534. func (i *Include) Pos() Position {
  535. return i.position
  536. }
  537. // Get finds the first value in the Include statement matching the alias and the
  538. // given key.
  539. func (inc *Include) Get(alias, key string) string {
  540. inc.mu.Lock()
  541. defer inc.mu.Unlock()
  542. // TODO: we search files in any order which is not correct
  543. for i := range inc.matches {
  544. cfg := inc.files[inc.matches[i]]
  545. if cfg == nil {
  546. panic("nil cfg")
  547. }
  548. val, err := cfg.Get(alias, key)
  549. if err == nil && val != "" {
  550. return val
  551. }
  552. }
  553. return ""
  554. }
  555. // String prints out a string representation of this Include directive. Note
  556. // included Config files are not printed as part of this representation.
  557. func (inc *Include) String() string {
  558. equals := " "
  559. if inc.hasEquals {
  560. equals = " = "
  561. }
  562. line := fmt.Sprintf("%sInclude%s%s", strings.Repeat(" ", int(inc.leadingSpace)), equals, strings.Join(inc.directives, " "))
  563. if inc.Comment != "" {
  564. line += " #" + inc.Comment
  565. }
  566. return line
  567. }
  568. var matchAll *Pattern
  569. func init() {
  570. var err error
  571. matchAll, err = NewPattern("*")
  572. if err != nil {
  573. panic(err)
  574. }
  575. }
  576. func newConfig() *Config {
  577. return &Config{
  578. Hosts: []*Host{
  579. &Host{
  580. implicit: true,
  581. Patterns: []*Pattern{matchAll},
  582. Nodes: make([]Node, 0),
  583. },
  584. },
  585. depth: 0,
  586. }
  587. }