From 3fad3c64da206df4f90bbc2945acb5172e989ee2 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Wed, 24 May 2017 22:10:17 -0700 Subject: [PATCH] Add more documentation and correctness fix --- config.go | 86 +++++++++++++++++++++++++++++++++---------------- example_test.go | 13 ++++++++ validators.go | 16 +++++---- 3 files changed, 80 insertions(+), 35 deletions(-) create mode 100644 example_test.go diff --git a/config.go b/config.go index b4f467e..5646ce5 100644 --- a/config.go +++ b/config.go @@ -222,11 +222,11 @@ func decode(r io.Reader, system bool, depth uint8) (c *Config, err error) { // Config represents an SSH config file. type Config struct { - position Position // A list of hosts to match against. The file begins with an implicit // "Host *" declaration matching all hosts. - Hosts []*Host - depth uint8 + Hosts []*Host + depth uint8 + position Position } // Get finds the first value in the configuration that matches the alias and @@ -472,11 +472,21 @@ func (e *Empty) String() string { return fmt.Sprintf("%s#%s", strings.Repeat(" ", int(e.leadingSpace)), e.Comment) } +// Include holds the result of an Include directive, including the config files +// that have been parsed as part of that directive. At most 5 levels of Include +// statements will be parsed. type Include struct { + // Comment is the contents of any comment at the end of the Include + // statement. Comment string parsed bool // an include directive can include several different files, and wildcards directives []string + + mu sync.Mutex + // 1:1 mapping between matches and keys in files array; matches preserves + // ordering + matches []string // actual filenames are listed here files map[string]*Config leadingSpace uint16 @@ -486,9 +496,29 @@ type Include struct { const maxRecurseDepth = 5 +// ErrDepthExceeded is returned if too many Include directives are parsed. +// Usually this indicates a recursive loop (an Include directive pointing to the +// file it contains). var ErrDepthExceeded = errors.New("ssh_config: max recurse depth exceeded") -// NewInclude creates a new Include with a list of files to include. +func removeDups(arr []string) []string { + // Use map to record duplicates as we find them. + encountered := make(map[string]bool, len(arr)) + result := make([]string, 0) + + for v := range arr { + if encountered[arr[v]] == false { + encountered[arr[v]] = true + result = append(result, arr[v]) + } + } + return result +} + +// NewInclude creates a new Include with a list of file globs to include. +// Configuration files are parsed greedily (e.g. as soon as this function runs). +// Any error encountered while parsing nested configuration files will be +// returned. func NewInclude(directives []string, comment string, system bool, depth uint8) (*Include, error) { if depth > maxRecurseDepth { return nil, ErrDepthExceeded @@ -499,6 +529,8 @@ func NewInclude(directives []string, comment string, system bool, depth uint8) ( files: make(map[string]*Config), depth: depth, } + // no need for inc.mu.Lock() since nothing else can access this inc + matches := make([]string, 0) for i := range directives { var path string if filepath.IsAbs(directives[i]) { @@ -508,42 +540,40 @@ func NewInclude(directives []string, comment string, system bool, depth uint8) ( } else { path = filepath.Join(homedir(), ".ssh", directives[i]) } - matches, err := filepath.Glob(path) + theseMatches, err := filepath.Glob(path) if err != nil { return nil, err } - for j := range matches { - if _, ok := inc.files[matches[j]]; ok { - // config already parsed - continue - } - config, err := parseWithDepth(matches[j], depth) - if err != nil { - return nil, err - } - inc.files[matches[j]] = config + matches = append(matches, theseMatches...) + } + matches = removeDups(matches) + inc.matches = matches + for i := range matches { + config, err := parseWithDepth(matches[i], depth) + if err != nil { + return nil, err } + inc.files[matches[i]] = config } - // check depth limit - // for each directive: - // for each file in the list: - // - increment the depth array - // - parse it, return any errors - // - add to files map return inc, nil } +// Pos returns the position of the Include directive in the larger file. func (i *Include) Pos() Position { return i.position } -func (i *Include) Add() error { - return nil -} - -// Get finds the first value in the Include statement -func (i *Include) Get(alias, key string) string { - for _, cfg := range i.files { +// Get finds the first value in the Include statement matching the alias and the +// given key. +func (inc *Include) Get(alias, key string) string { + inc.mu.Lock() + defer inc.mu.Unlock() + // TODO: we search files in any order which is not correct + for i := range inc.matches { + cfg := inc.files[inc.matches[i]] + if cfg == nil { + panic("nil cfg") + } val, err := cfg.Get(alias, key) if err == nil && val != "" { return val diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..dac8c14 --- /dev/null +++ b/example_test.go @@ -0,0 +1,13 @@ +package ssh_config + +import "fmt" + +func ExampleHost_Matches() { + pat, _ := NewPattern("test.*.example.com") + host := &Host{Patterns: []*Pattern{pat}} + fmt.Println(host.Matches("test.stage.example.com")) + fmt.Println(host.Matches("othersubdomain.example.com")) + // Output: + // true + // false +} diff --git a/validators.go b/validators.go index 46b7693..29fab6a 100644 --- a/validators.go +++ b/validators.go @@ -6,6 +6,15 @@ import ( "strings" ) +// Default returns the default value for the given keyword, for example "22" if +// the keyword is "Port". Default returns the empty string if the keyword has no +// default, or if the keyword is unknown. Keyword matching is case-insensitive. +// +// Default values are provided by OpenSSH_7.4p1 on a Mac. +func Default(keyword string) string { + return defaults[strings.ToLower(keyword)] +} + // Arguments where the value must be "yes" or "no" and *only* yes or no. var yesnos = map[string]bool{ strings.ToLower("BatchMode"): true, @@ -151,10 +160,3 @@ var defaults = map[string]string{ strings.ToLower("VisualHostKey"): "no", strings.ToLower("XAuthLocation"): "/usr/X11R6/bin/xauth", } - -// Default returns the default value for the given keyword. Default returns the -// empty string if the keyword has no default, or if the keyword is unknown. -// Keyword matching is case-insensitive. -func Default(keyword string) string { - return defaults[strings.ToLower(keyword)] -}