From e2d4055e4e6be5cc0e3887336a14be2412221586 Mon Sep 17 00:00:00 2001 From: fferro Date: Wed, 17 Jul 2024 10:23:07 +0100 Subject: [PATCH] Add the possibility to ignore the Match directive --- .editorconfig | 21 ++++++++ config.go | 74 +++++++++++++++------------ config_test.go | 35 +++++++++++-- example_test.go | 5 +- parser.go | 39 ++++++++------ parser_test.go | 2 +- testdata/config1-with-match-directive | 6 +++ 7 files changed, 125 insertions(+), 57 deletions(-) create mode 100644 .editorconfig create mode 100644 testdata/config1-with-match-directive diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b58bf63 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig is awesome: http://EditorConfig.org + +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[{Makefile,**.defs}] +# Use tabs for indentation (Makefiles require tabs) +indent_style = tab + +[*.go] +indent_style = tab diff --git a/config.go b/config.go index 4816e67..885fffc 100644 --- a/config.go +++ b/config.go @@ -8,7 +8,7 @@ // the host name to match on ("example.com"), and the second argument is the key // you want to retrieve ("Port"). The keywords are case insensitive. // -// port := ssh_config.Get("myhost", "Port") +// port := ssh_config.Get("myhost", "Port") // // You can also manipulate an SSH config file and then print it or write it back // to disk. @@ -52,15 +52,16 @@ type configFinder func() string // 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 { @@ -80,9 +81,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: true, + systemConfigFinder: systemConfigFinder, + userConfigFinder: userConfigFinder, } func systemConfigFinder() string { @@ -277,10 +279,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 @@ -290,7 +293,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 @@ -301,25 +304,26 @@ 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 return } - }) + }, + ) } -func parseFile(filename string) (*Config, error) { - return parseWithDepth(filename, 0) +func parseFile(filename string, ignoreMatchDirective bool) (*Config, error) { + return parseWithDepth(filename, ignoreMatchDirective, 0) } -func parseWithDepth(filename string, depth uint8) (*Config, error) { +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 { @@ -329,21 +333,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 { @@ -357,7 +361,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 } @@ -365,9 +369,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 @@ -388,7 +393,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 { @@ -711,7 +716,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 } @@ -744,7 +750,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 11b203d..c864be4 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) } @@ -339,7 +339,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) } @@ -460,10 +460,39 @@ func TestCustomFinder(t *testing.T) { us := &UserSettings{} us.ConfigFinder(func() string { return "testdata/config1" - }) + }, + ) val := us.Get("wap", "User") if val != "root" { 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..020296c 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 @@ -53,6 +53,7 @@ func ExampleUserSettings_ConfigFinder() { u := ssh_config.UserSettings{} u.ConfigFinder(func() string { return filepath.Join("testdata", "test_config") - }) + }, + ) u.Get("example.com", "Host") } diff --git a/parser.go b/parser.go index 2b1e718..e59c500 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 @@ -173,11 +176,12 @@ func (p *sshParser) parseComment() sshParserStateFn { // account for the "#" as well leadingSpace: comment.Position.Col - 2, position: comment.Position, - }) + }, + ) 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 +191,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