Allow User to Ignore Match Directive (#5)

* Add the possibility to ignore the Match directive

* cleanup

* cleanup

* cleanup

* cleanup

* fix: duplicated code leftover from merge

* fix: match directive bypass for GetAll

The new ignoreMatchDirective flag needed to be added in one other place.

---------

Co-authored-by: fferro <francesco.ferro@yahooinc.com>
This commit is contained in:
Sylvie Crowe
2024-10-27 16:23:32 -07:00
committed by GitHub
co-authored by fferro
parent 17e2087ebd
commit ed12436768
6 changed files with 94 additions and 53 deletions
+38 -33
View File
@@ -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,16 @@ func (u *UserSettings) ReloadConfigs() {
u.doLoadConfigs()
}
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 {
@@ -354,21 +357,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 +385,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 +393,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 +417,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 {
@@ -453,7 +457,7 @@ func (c *Config) GetAll(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 +745,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 +779,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
}
+28 -2
View File
@@ -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)
}
}
+1 -1
View File
@@ -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
+20 -16
View File
@@ -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
+1 -1
View File
@@ -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")
}
+6
View File
@@ -0,0 +1,6 @@
Match all
Include ~/.ssh
Host *
User usr
Host git.yahoo.com
HostName git.proxy.com