本站源代码
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.

650 lines
17KB

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