diff --git a/config.go b/config.go index 7d51cae..61c3cb7 100644 --- a/config.go +++ b/config.go @@ -59,15 +59,16 @@ var _ config = &Config{} // UserSettings checks ~/.ssh and /etc/ssh for configuration files. The config // files are parsed and cached the first time Get() or GetStrict() is called. type UserSettings struct { - IgnoreErrors bool - customConfig *Config - customConfigFinder configFinder - systemConfig *Config - systemConfigFinder configFinder - userConfig *Config - userConfigFinder configFinder - loadConfigs *sync.Once - onceErr error + IgnoreErrors bool + IgnoreMatchDirective bool + customConfig *Config + customConfigFinder configFinder + systemConfig *Config + systemConfigFinder configFinder + userConfig *Config + userConfigFinder configFinder + loadConfigs *sync.Once + onceErr error } func homedir() string { @@ -87,9 +88,10 @@ func userConfigFinder() string { // GetStrict. It checks both $HOME/.ssh/config and /etc/ssh/ssh_config for keys, // and it will return parse errors (if any) instead of swallowing them. var DefaultUserSettings = &UserSettings{ - IgnoreErrors: false, - systemConfigFinder: systemConfigFinder, - userConfigFinder: userConfigFinder, + IgnoreErrors: false, + IgnoreMatchDirective: false, + systemConfigFinder: systemConfigFinder, + userConfigFinder: userConfigFinder, } func systemConfigFinder() string { @@ -295,10 +297,11 @@ func (u *UserSettings) doLoadConfigs() { var err error if u.customConfigFinder != nil { filename = u.customConfigFinder() - u.customConfig, err = parseFile(filename) + u.customConfig, err = parseFile(filename, u.IgnoreMatchDirective) // IsNotExist should be returned because a user specified this // function - not existing likely means they made an error - if err != nil { + // We should also respect the ignore flag + if err != nil && !u.IgnoreErrors { u.onceErr = err } return @@ -308,7 +311,7 @@ func (u *UserSettings) doLoadConfigs() { } else { filename = u.userConfigFinder() } - u.userConfig, err = parseFile(filename) + u.userConfig, err = parseFile(filename, u.IgnoreMatchDirective) //lint:ignore S1002 I prefer it this way if err != nil && os.IsNotExist(err) == false { u.onceErr = err @@ -319,7 +322,7 @@ func (u *UserSettings) doLoadConfigs() { } else { filename = u.systemConfigFinder() } - u.systemConfig, err = parseFile(filename) + u.systemConfig, err = parseFile(filename, u.IgnoreMatchDirective) //lint:ignore S1002 I prefer it this way if err != nil && os.IsNotExist(err) == false { u.onceErr = err @@ -335,16 +338,23 @@ func (u *UserSettings) ReloadConfigs() { u.doLoadConfigs() } -func parseFile(filename string) (*Config, error) { - return parseWithDepth(filename, 0) +// ReloadConfigs clears the cached config data and freshly loads the config +// files again. +func (u *UserSettings) ReloadConfigs() { + u.loadConfigs = new(sync.Once) + u.doLoadConfigs() } -func parseWithDepth(filename string, depth uint8) (*Config, error) { +func parseFile(filename string, ignoreMatchDirective bool) (*Config, error) { + return parseWithDepth(filename, ignoreMatchDirective, 0) +} + +func parseWithDepth(filename string, ignoreMatchDirective bool, depth uint8) (*Config, error) { b, err := os.ReadFile(filename) if err != nil { return nil, err } - return decodeBytes(b, isSystem(filename), depth) + return decodeBytes(b, isSystem(filename), ignoreMatchDirective, depth) } func isSystem(filename string) bool { @@ -354,21 +364,21 @@ func isSystem(filename string) bool { // Decode reads r into a Config, or returns an error if r could not be parsed as // an SSH config file. -func Decode(r io.Reader) (*Config, error) { +func Decode(r io.Reader, ignoreMatchDirective bool) (*Config, error) { b, err := io.ReadAll(r) if err != nil { return nil, err } - return decodeBytes(b, false, 0) + return decodeBytes(b, false, ignoreMatchDirective, 0) } // DecodeBytes reads b into a Config, or returns an error if r could not be // parsed as an SSH config file. -func DecodeBytes(b []byte) (*Config, error) { - return decodeBytes(b, false, 0) +func DecodeBytes(b []byte, ignoreMatchDirective bool) (*Config, error) { + return decodeBytes(b, false, ignoreMatchDirective, 0) } -func decodeBytes(b []byte, system bool, depth uint8) (c *Config, err error) { +func decodeBytes(b []byte, system, ignoreMatchDirective bool, depth uint8) (c *Config, err error) { defer func() { if r := recover(); r != nil { if _, ok := r.(runtime.Error); ok { @@ -382,7 +392,7 @@ func decodeBytes(b []byte, system bool, depth uint8) (c *Config, err error) { } }() - c = parseSSH(lexSSH(b), system, depth) + c = parseSSH(lexSSH(b), system, ignoreMatchDirective, depth) return c, err } @@ -390,9 +400,10 @@ func decodeBytes(b []byte, system bool, depth uint8) (c *Config, err error) { type Config struct { // A list of hosts to match against. The file begins with an implicit // "Host *" declaration matching all hosts. - Hosts []*Host - depth uint8 - position Position + Hosts []*Host + depth uint8 + position Position + ignoreMatchDirective bool } // Get finds the first value in the configuration that matches the alias and @@ -413,7 +424,7 @@ func (c *Config) Get(alias, key string) (string, error) { case *KV: // "keys are case insensitive" per the spec lkey := strings.ToLower(t.Key) - if lkey == "match" { + if lkey == "match" && !c.ignoreMatchDirective { panic("can't handle Match directives") } if lkey == lowerKey { @@ -741,7 +752,8 @@ func removeDups(arr []string) []string { // 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, hasEquals bool, pos Position, comment string, system bool, depth uint8) (*Include, error) { +func NewInclude(directives []string, hasEquals bool, pos Position, comment string, system, ignoreMatchDirective bool, depth uint8, +) (*Include, error) { if depth > maxRecurseDepth { return nil, ErrDepthExceeded } @@ -774,7 +786,7 @@ func NewInclude(directives []string, hasEquals bool, pos Position, comment strin matches = removeDups(matches) inc.matches = matches for i := range matches { - config, err := parseWithDepth(matches[i], depth) + config, err := parseWithDepth(matches[i], ignoreMatchDirective, depth) if err != nil { return nil, err } diff --git a/config_test.go b/config_test.go index 4b06e4e..6b777f4 100644 --- a/config_test.go +++ b/config_test.go @@ -27,7 +27,7 @@ var files = []string{ func TestDecode(t *testing.T) { for _, filename := range files { data := loadFile(t, filename) - cfg, err := Decode(bytes.NewReader(data)) + cfg, err := Decode(bytes.NewReader(data), false) if err != nil { t.Fatal(err) } @@ -404,7 +404,7 @@ func TestIncludeString(t *testing.T) { if err != nil { log.Fatal(err) } - c, err := Decode(bytes.NewReader(data)) + c, err := Decode(bytes.NewReader(data), false) if err != nil { t.Fatal(err) } @@ -532,3 +532,29 @@ func TestCustomFinder(t *testing.T) { t.Errorf("expected to find User root, got %q", val) } } + +func TestCustomFinderWhenIgnoringMatchDirective(t *testing.T) { + us := &UserSettings{ + IgnoreMatchDirective: true, + } + us.ConfigFinder(func() string { + return "testdata/config1-with-match-directive" + }) + + val := us.Get("git.yahoo.com", "HostName") + if val != "git.proxy.com" { + t.Errorf("expected to find Hostname git.proxy.com, got %q", val) + } +} + +func TestCustomFinderWhenNotIgnoringMatchDirective(t *testing.T) { + us := &UserSettings{} + us.ConfigFinder(func() string { + return "testdata/config1-with-match-directive" + }) + + val := us.Get("git.yahoo.com", "HostName") + if val != "" { + t.Errorf("expected to find Hostname empty %q", val) + } +} diff --git a/example_test.go b/example_test.go index a7c16d6..771a287 100644 --- a/example_test.go +++ b/example_test.go @@ -34,7 +34,7 @@ Host *.example.com Compression yes ` - cfg, _ := ssh_config.Decode(strings.NewReader(config)) + cfg, _ := ssh_config.Decode(strings.NewReader(config), false) val, _ := cfg.Get("test.example.com", "Compression") fmt.Println(val) // Output: yes diff --git a/parser.go b/parser.go index 2b1e718..ed30810 100644 --- a/parser.go +++ b/parser.go @@ -7,11 +7,12 @@ import ( ) type sshParser struct { - flow chan token - config *Config - tokensBuffer []token - currentTable []string - seenTableKeys []string + ignoreMatchDirective bool + flow chan token + config *Config + tokensBuffer []token + currentTable []string + seenTableKeys []string // /etc/ssh parser or local parser - used to find the default for relative // filepaths in the Include directive system bool @@ -104,7 +105,7 @@ func (p *sshParser) parseKV() sshParserStateFn { tok = p.getToken() comment = tok.val } - if strings.ToLower(key.val) == "match" { + if strings.ToLower(key.val) == "match" && !p.ignoreMatchDirective { // https://github.com/kevinburke/ssh_config/issues/6 p.raiseErrorf(val, "ssh_config: Match directive parsing is unsupported") return nil @@ -127,18 +128,20 @@ func (p *sshParser) parseKV() sshParserStateFn { hostval := strings.TrimRightFunc(val.val, unicode.IsSpace) spaceBeforeComment := val.val[len(hostval):] val.val = hostval + p.config.ignoreMatchDirective = p.ignoreMatchDirective p.config.Hosts = append(p.config.Hosts, &Host{ Patterns: patterns, Nodes: make([]Node, 0), EOLComment: comment, spaceBeforeComment: spaceBeforeComment, hasEquals: hasEquals, - }) + }, + ) return p.parseStart } lastHost := p.config.Hosts[len(p.config.Hosts)-1] if strings.ToLower(key.val) == "include" { - inc, err := NewInclude(strings.Split(val.val, " "), hasEquals, key.Position, comment, p.system, p.depth+1) + inc, err := NewInclude(strings.Split(val.val, " "), hasEquals, key.Position, comment, p.system, p.ignoreMatchDirective, p.depth+1) if err == ErrDepthExceeded { p.raiseError(val, err) return nil @@ -177,7 +180,7 @@ func (p *sshParser) parseComment() sshParserStateFn { return p.parseStart } -func parseSSH(flow chan token, system bool, depth uint8) *Config { +func parseSSH(flow chan token, system, ignoreMatchDirective bool, depth uint8) *Config { // Ensure we consume tokens to completion even if parser exits early defer func() { for range flow { @@ -187,13 +190,14 @@ func parseSSH(flow chan token, system bool, depth uint8) *Config { result := newConfig() result.position = Position{1, 1} parser := &sshParser{ - flow: flow, - config: result, - tokensBuffer: make([]token, 0), - currentTable: make([]string, 0), - seenTableKeys: make([]string, 0), - system: system, - depth: depth, + ignoreMatchDirective: ignoreMatchDirective, + flow: flow, + config: result, + tokensBuffer: make([]token, 0), + currentTable: make([]string, 0), + seenTableKeys: make([]string, 0), + system: system, + depth: depth, } parser.run() return result diff --git a/parser_test.go b/parser_test.go index ff1ab2f..dc679a4 100644 --- a/parser_test.go +++ b/parser_test.go @@ -14,7 +14,7 @@ func (b *errReader) Read(p []byte) (n int, err error) { func TestIOError(t *testing.T) { buf := &errReader{} - _, err := Decode(buf) + _, err := Decode(buf, false) if err == nil { t.Fatal("expected non-nil err, got nil") } diff --git a/testdata/config1-with-match-directive b/testdata/config1-with-match-directive new file mode 100644 index 0000000..ce7fc1a --- /dev/null +++ b/testdata/config1-with-match-directive @@ -0,0 +1,6 @@ +Match all + Include ~/.ssh +Host * + User usr +Host git.yahoo.com + HostName git.proxy.com