diff --git a/config.go b/config.go index 4816e67..2ad6021 100644 --- a/config.go +++ b/config.go @@ -4,11 +4,11 @@ // you can manipulate a `ssh_config` file from a program, if your heart desires. // // The Get() and GetStrict() functions will attempt to read values from -// $HOME/.ssh/config, falling back to /etc/ssh/ssh_config. The first argument is +// $HOME/.ssh/config, falling back to /etc/ssh/ssh_config (on linux). The first argument is // 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. +// 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. @@ -35,7 +35,6 @@ import ( "fmt" "io" "os" - osuser "os/user" "path/filepath" "regexp" "runtime" @@ -47,95 +46,53 @@ const version = "1.2" var _ = version -type configFinder func() string +type ConfigFileFinder func() (string, error) -// 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 -} - -func homedir() string { - user, err := osuser.Current() - if err == nil { - return user.HomeDir - } else { - return os.Getenv("HOME") +// UserHomeConfigFileFinder return ~/.ssh/config regardless of your current os, +func UserHomeConfigFileFinder() (string, error) { + osUserHome, err := os.UserHomeDir() + if err != nil { + return "", err } -} -func userConfigFinder() string { - return filepath.Join(homedir(), ".ssh", "config") + return filepath.ToSlash(filepath.Join(osUserHome, ".ssh", "config")), nil } -// DefaultUserSettings is the default UserSettings and is used by Get and -// 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, -} +// UserSettings checks all files listed via configFiles. +// The list of available files will be traversed from begin to end. +// The first file which holds the desired key will be used. +// The config files are parsed and cached the first time Get() or GetStrict() is called. +type UserSettings struct { + IgnoreErrors bool -func systemConfigFinder() string { - return filepath.Join("/", "etc", "ssh", "ssh_config") + configFiles []ConfigFileFinder + configs []*Config + loadConfigs sync.Once + onceErr error } -func findVal(c *Config, alias, key string) (string, error) { - if c == nil { - return "", nil - } - val, err := c.Get(alias, key) - if err != nil || val == "" { - return "", err - } - if err := validate(key, val); err != nil { - return "", err - } - return val, nil -} +// DefaultUserSettings is the default UserSettings and is used by Get and GetStrict. +// It checks both $HOME/.ssh/config and /etc/ssh/ssh_config for keys (on linux) , +// and it will return parse errors (if any) instead of swallowing them. +var DefaultUserSettings *UserSettings = nil -func findAll(c *Config, alias, key string) ([]string, error) { - if c == nil { - return nil, nil - } - return c.GetAll(alias, key) +func init() { + defaultSettings := UserSettings{IgnoreErrors: false} + DefaultUserSettings = defaultSettings.WithConfigLocations(DefaultConfigFileFinders...) } -// Get finds the first value for key within a declaration that matches the -// alias. Get returns the empty string if no value was found, or if IgnoreErrors -// is false and we could not parse the configuration file. Use GetStrict to -// disambiguate the latter cases. -// -// The match for key is case insensitive. -// -// Get is a wrapper around DefaultUserSettings.Get. -func Get(alias, key string) string { - return DefaultUserSettings.Get(alias, key) +// WithConfigLocations sets the list of ssh_config files to be searched when using Get, GetAll, GetStrict or +// GetAllStrict. The list will be traversed from begin to end; the first file which holds the desired key will be used +func (u *UserSettings) WithConfigLocations(finders ...ConfigFileFinder) *UserSettings { + u.configFiles = finders + return u } -// GetAll retrieves zero or more directives for key for the given alias. GetAll -// returns nil if no value was found, or if IgnoreErrors is false and we could -// not parse the configuration file. Use GetAllStrict to disambiguate the -// latter cases. -// -// In most cases you want to use Get or GetStrict, which returns a single value. -// However, a subset of ssh configuration values (IdentityFile, for example) -// allow you to specify multiple directives. -// -// The match for key is case insensitive. -// -// GetAll is a wrapper around DefaultUserSettings.GetAll. -func GetAll(alias, key string) []string { - return DefaultUserSettings.GetAll(alias, key) +// AddConfigLocations adds a file to the list of ssh_config files to be searched when using Get, GetAll, GetStrict or +// GetAllStrict. The list will be traversed from begin to end; the first file which holds the desired key will be used +func (u *UserSettings) AddConfigLocations(finders ...ConfigFileFinder) *UserSettings { + u.configFiles = append(u.configFiles, finders...) + return u } // GetStrict finds the first value for key within a declaration that matches the @@ -143,28 +100,36 @@ func GetAll(alias, key string) []string { // default will be returned. For more information on default values and the way // patterns are matched, see the manpage for ssh_config. // -// The returned error will be non-nil if and only if a user's configuration file -// or the system configuration file could not be parsed, and u.IgnoreErrors is -// false. -// -// GetStrict is a wrapper around DefaultUserSettings.GetStrict. -func GetStrict(alias, key string) (string, error) { - return DefaultUserSettings.GetStrict(alias, key) -} +// error will be non-nil if and only if a user's configuration file or the +// system configuration file could not be parsed, and u.IgnoreErrors is false. +func (u *UserSettings) doLoadConfigs() { + u.loadConfigs.Do(func() { + //use defaults if no ConfigFileFinder where set + if len(u.configFiles) == 0 { + u.configFiles = DefaultConfigFileFinders + } -// GetAllStrict retrieves zero or more directives for key for the given alias. -// -// In most cases you want to use Get or GetStrict, which returns a single value. -// However, a subset of ssh configuration values (IdentityFile, for example) -// allow you to specify multiple directives. -// -// The returned error will be non-nil if and only if a user's configuration file -// or the system configuration file could not be parsed, and u.IgnoreErrors is -// false. -// -// GetAllStrict is a wrapper around DefaultUserSettings.GetAllStrict. -func GetAllStrict(alias, key string) ([]string, error) { - return DefaultUserSettings.GetAllStrict(alias, key) + var location string + var err error + + u.configs = make([]*Config, len(u.configFiles), len(u.configFiles)) + + for i := 0; i < len(u.configFiles); i++ { + location, err = u.configFiles[i]() + if err != nil { + u.onceErr = err + return + } + + u.configs[i], err = parseFile(location) + // IsNotExist should be returned because a user specified this + // function - not existing likely means they made an error + if err != nil { + u.onceErr = err + return + } + } + }) } // Get finds the first value for key within a declaration that matches the @@ -172,7 +137,11 @@ func GetAllStrict(alias, key string) ([]string, error) { // is false and we could not parse the configuration file. Use GetStrict to // disambiguate the latter cases. // -// The match for key is case insensitive. +// All files specified using WithConfigLocations and AddConfigLocations are candidates for the searched alias, key +// tuple. The list of available files will be traversed from begin to end. +// The first file which holds the desired key will be used. +// +// The match for key is case-insensitive. func (u *UserSettings) Get(alias, key string) string { val, err := u.GetStrict(alias, key) if err != nil { @@ -186,7 +155,11 @@ func (u *UserSettings) Get(alias, key string) string { // not parse the configuration file. Use GetStrict to disambiguate the latter // cases. // -// The match for key is case insensitive. +// All files specified using WithConfigLocations and AddConfigLocations are candidates for the searched alias, key +// tuple. The list of available files will be traversed from begin to end. +// The first file which holds the desired key will be used. +// +// The match for key is case-insensitive. func (u *UserSettings) GetAll(alias, key string) []string { val, _ := u.GetAllStrict(alias, key) return val @@ -197,29 +170,27 @@ func (u *UserSettings) GetAll(alias, key string) []string { // default will be returned. For more information on default values and the way // patterns are matched, see the manpage for ssh_config. // -// error will be non-nil if and only if a user's configuration file or the -// system configuration file could not be parsed, and u.IgnoreErrors is false. +// All files specified using WithConfigLocations and AddConfigLocations are candidates for the searched alias, key +// tuple. The list of available files will be traversed from begin to end. +// The first file which holds the desired key will be used. +// +// The returned error will be non-nil if and only if a user's configuration file +// or the system configuration file could not be parsed, and u.IgnoreErrors is +// false. func (u *UserSettings) GetStrict(alias, key string) (string, error) { u.doLoadConfigs() //lint:ignore S1002 I prefer it this way if u.onceErr != nil && u.IgnoreErrors == false { return "", u.onceErr } - // TODO this is getting repetitive - if u.customConfig != nil { - val, err := findVal(u.customConfig, alias, key) + + for _, c := range u.configs { + val, err := findVal(c, alias, key) if err != nil || val != "" { return val, err } } - val, err := findVal(u.userConfig, alias, key) - if err != nil || val != "" { - return val, err - } - val2, err2 := findVal(u.systemConfig, alias, key) - if err2 != nil || val2 != "" { - return val2, err2 - } + return Default(key), nil } @@ -228,6 +199,10 @@ func (u *UserSettings) GetStrict(alias, key string) (string, error) { // default will be returned. For more information on default values and the way // patterns are matched, see the manpage for ssh_config. // +// All files specified using WithConfigLocations and AddConfigLocations are candidates for the searched alias, key +// tuple. The list of available files will be traversed from begin to end. +// The first file which holds the desired key will be used. +// // The returned error will be non-nil if and only if a user's configuration file // or the system configuration file could not be parsed, and u.IgnoreErrors is // false. @@ -237,20 +212,12 @@ func (u *UserSettings) GetAllStrict(alias, key string) ([]string, error) { if u.onceErr != nil && u.IgnoreErrors == false { return nil, u.onceErr } - if u.customConfig != nil { - val, err := findAll(u.customConfig, alias, key) + for _, c := range u.configs { + val, err := findAll(c, alias, key) if err != nil || val != nil { return val, err } } - val, err := findAll(u.userConfig, alias, key) - if err != nil || val != nil { - return val, err - } - val2, err2 := findAll(u.systemConfig, alias, key) - if err2 != nil || val2 != nil { - return val2, err2 - } // TODO: IdentityFile has multiple default values that we should return. if def := Default(key); def != "" { return []string{def}, nil @@ -258,56 +225,82 @@ func (u *UserSettings) GetAllStrict(alias, key string) ([]string, error) { return []string{}, nil } -// ConfigFinder will invoke f to try to find a ssh config file in a custom -// location on disk, instead of in /etc/ssh or $HOME/.ssh. f should return the -// name of a file containing SSH configuration. -// -// ConfigFinder must be invoked before any calls to Get or GetStrict and panics -// if f is nil. Most users should not need to use this function. -func (u *UserSettings) ConfigFinder(f func() string) { - if f == nil { - panic("cannot call ConfigFinder with nil function") +func findVal(c *Config, alias, key string) (string, error) { + if c == nil { + return "", nil } - u.customConfigFinder = f + val, err := c.Get(alias, key) + if err != nil || val == "" { + return "", err + } + if err := validate(key, val); err != nil { + return "", err + } + return val, nil } -func (u *UserSettings) doLoadConfigs() { - u.loadConfigs.Do(func() { - var filename string - var err error - if u.customConfigFinder != nil { - filename = u.customConfigFinder() - u.customConfig, err = parseFile(filename) - // IsNotExist should be returned because a user specified this - // function - not existing likely means they made an error - if err != nil { - u.onceErr = err - } - return - } - if u.userConfigFinder == nil { - filename = userConfigFinder() - } else { - filename = u.userConfigFinder() - } - u.userConfig, err = parseFile(filename) - //lint:ignore S1002 I prefer it this way - if err != nil && os.IsNotExist(err) == false { - u.onceErr = err - return - } - if u.systemConfigFinder == nil { - filename = systemConfigFinder() - } else { - filename = u.systemConfigFinder() - } - u.systemConfig, err = parseFile(filename) - //lint:ignore S1002 I prefer it this way - if err != nil && os.IsNotExist(err) == false { - u.onceErr = err - return - } - }) +func findAll(c *Config, alias, key string) ([]string, error) { + if c == nil { + return nil, nil + } + return c.GetAll(alias, key) +} + +// Get finds the first value for key within a declaration that matches the +// alias. Get returns the empty string if no value was found, or if IgnoreErrors +// is false and we could not parse the configuration file. Use GetStrict to +// disambiguate the latter cases. +// +// The match for key is case-insensitive. +// +// Get is a wrapper around DefaultUserSettings.Get. +func Get(alias, key string) string { + return DefaultUserSettings.Get(alias, key) +} + +// GetAll retrieves zero or more directives for key for the given alias. GetAll +// returns nil if no value was found, or if IgnoreErrors is false and we could +// not parse the configuration file. Use GetAllStrict to disambiguate the +// latter cases. +// +// In most cases you want to use Get or GetStrict, which returns a single value. +// However, a subset of ssh configuration values (IdentityFile, for example) +// allow you to specify multiple directives. +// +// The match for key is case-insensitive. +// +// GetAll is a wrapper around DefaultUserSettings.GetAll. +func GetAll(alias, key string) []string { + return DefaultUserSettings.GetAll(alias, key) +} + +// GetStrict finds the first value for key within a declaration that matches the +// alias. If key has a default value and no matching configuration is found, the +// default will be returned. For more information on default values and the way +// patterns are matched, see the manpage for ssh_config. +// +// The returned error will be non-nil if and only if a user's configuration file +// or the system configuration file could not be parsed, and u.IgnoreErrors is +// false. +// +// GetStrict is a wrapper around DefaultUserSettings.GetStrict. +func GetStrict(alias, key string) (string, error) { + return DefaultUserSettings.GetStrict(alias, key) +} + +// GetAllStrict retrieves zero or more directives for key for the given alias. +// +// In most cases you want to use Get or GetStrict, which returns a single value. +// However, a subset of ssh configuration values (IdentityFile, for example) +// allow you to specify multiple directives. +// +// The returned error will be non-nil if and only if a user's configuration file +// or the system configuration file could not be parsed, and u.IgnoreErrors is +// false. +// +// GetAllStrict is a wrapper around DefaultUserSettings.GetAllStrict. +func GetAllStrict(alias, key string) ([]string, error) { + return DefaultUserSettings.GetAllStrict(alias, key) } func parseFile(filename string) (*Config, error) { @@ -319,12 +312,9 @@ func parseWithDepth(filename string, depth uint8) (*Config, error) { if err != nil { return nil, err } - return decodeBytes(b, isSystem(filename), depth) -} -func isSystem(filename string) bool { - // TODO: not sure this is the best way to detect a system repo - return strings.HasPrefix(filepath.Clean(filename), "/etc/ssh") + parent := filepath.Dir(filename) + return decodeBytes(b, parent, depth) } // Decode reads r into a Config, or returns an error if r could not be parsed as @@ -334,16 +324,16 @@ func Decode(r io.Reader) (*Config, error) { if err != nil { return nil, err } - return decodeBytes(b, false, 0) + return decodeBytes(b, "", 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) + return decodeBytes(b, "", 0) } -func decodeBytes(b []byte, system bool, depth uint8) (c *Config, err error) { +func decodeBytes(b []byte, cwd string, depth uint8) (c *Config, err error) { defer func() { if r := recover(); r != nil { if _, ok := r.(runtime.Error); ok { @@ -357,7 +347,7 @@ func decodeBytes(b []byte, system bool, depth uint8) (c *Config, err error) { } }() - c = parseSSH(lexSSH(b), system, depth) + c = parseSSH(lexSSH(b), cwd, depth) return c, err } @@ -374,7 +364,7 @@ type Config struct { // contains key. Get returns the empty string if no value was found, or if the // Config contains an invalid conditional Include value. // -// The match for key is case insensitive. +// The match for key is case-insensitive. func (c *Config) Get(alias, key string) (string, error) { lowerKey := strings.ToLower(key) for _, host := range c.Hosts { @@ -386,7 +376,7 @@ func (c *Config) Get(alias, key string) (string, error) { case *Empty: continue case *KV: - // "keys are case insensitive" per the spec + // "keys are case-insensitive" per the spec lkey := strings.ToLower(t.Key) if lkey == "match" { panic("can't handle Match directives") @@ -421,7 +411,7 @@ func (c *Config) GetAll(alias, key string) ([]string, error) { case *Empty: continue case *KV: - // "keys are case insensitive" per the spec + // "keys are case-insensitive" per the spec lkey := strings.ToLower(t.Key) if lkey == "match" { panic("can't handle Match directives") @@ -711,7 +701,7 @@ 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(basePath string, directives []string, hasEquals bool, pos Position, comment string, depth uint8) (*Include, error) { if depth > maxRecurseDepth { return nil, ErrDepthExceeded } @@ -730,10 +720,8 @@ func NewInclude(directives []string, hasEquals bool, pos Position, comment strin var path string if filepath.IsAbs(directives[i]) { path = directives[i] - } else if system { - path = filepath.Join("/etc/ssh", directives[i]) } else { - path = filepath.Join(homedir(), ".ssh", directives[i]) + path = filepath.Join(basePath, directives[i]) } theseMatches, err := filepath.Glob(path) if err != nil { diff --git a/config_linux.go b/config_linux.go new file mode 100644 index 0000000..82cccaf --- /dev/null +++ b/config_linux.go @@ -0,0 +1,10 @@ +package ssh_config + +import "path/filepath" + +// SystemConfigFileFinder return ~/etc/ssh/ssh_config on linux os, +func SystemConfigFileFinder() (string, error) { + return filepath.Join("/", "etc", "ssh", "ssh_config"), nil +} + +var DefaultConfigFileFinders = []ConfigFileFinder{UserHomeConfigFileFinder, SystemConfigFileFinder} diff --git a/config_nonlinux.go b/config_nonlinux.go new file mode 100644 index 0000000..8116a49 --- /dev/null +++ b/config_nonlinux.go @@ -0,0 +1,5 @@ +//go:build !linux + +package ssh_config + +var DefaultConfigFileFinders = []ConfigFileFinder{UserHomeConfigFileFinder} diff --git a/config_test.go b/config_test.go index 11b203d..a1af8b1 100644 --- a/config_test.go +++ b/config_test.go @@ -4,7 +4,6 @@ import ( "bytes" "log" "os" - "path/filepath" "strings" "testing" ) @@ -26,327 +25,504 @@ var files = []string{ func TestDecode(t *testing.T) { for _, filename := range files { - data := loadFile(t, filename) - cfg, err := Decode(bytes.NewReader(data)) - if err != nil { - t.Fatal(err) - } - out := cfg.String() - if out != string(data) { - t.Errorf("%s out != data: got:\n%s\nwant:\n%s\n", filename, out, string(data)) - } + t.Run(filename, func(t *testing.T) { + data := loadFile(t, filename) + cfg, err := Decode(bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + out := cfg.String() + if out != string(data) { + t.Errorf("%s out != data: got:\n%s\nwant:\n%s\n", filename, out, string(data)) + } + }) } } -func testConfigFinder(filename string) func() string { - return func() string { return filename } -} - func nullConfigFinder() string { return "" } -func TestGet(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/config1"), - } +func TestUserSettings(t *testing.T) { - val := us.Get("wap", "User") - if val != "root" { - t.Errorf("expected to find User root, got %q", val) + assertFileFinder := func(t *testing.T, target *UserSettings, idx int, expected string) { + file, _ := target.configFiles[idx]() + if file != expected { + t.Errorf("set configuration was not previously used finder function; idx: %d", idx) + } } -} -func TestGetWithDefault(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/config1"), - } + finderFncA := func() (string, error) { return "expected_file_A", nil } + finderFncB := func() (string, error) { return "expected_file_B", nil } + finderFncC := func() (string, error) { return "expected_file_C", nil } - val, err := us.GetStrict("wap", "PasswordAuthentication") - if err != nil { - t.Fatalf("expected nil err, got %v", err) - } - if val != "yes" { - t.Errorf("expected to get PasswordAuthentication yes, got %q", val) + testConfigFinder := func(filename string) ConfigFileFinder { + return func() (string, error) { return filename, nil } } -} -func TestGetAllWithDefault(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/config1"), - } + assert := func(t *testing.T, target *UserSettings, host, key, expect string) { + val, err := target.GetStrict(host, key) + if err != nil { + t.Fatal(err) + } - val, err := us.GetAllStrict("wap", "PasswordAuthentication") - if err != nil { - t.Fatalf("expected nil err, got %v", err) - } - if len(val) != 1 || val[0] != "yes" { - t.Errorf("expected to get PasswordAuthentication yes, got %q", val) + if val != expect { + t.Errorf("wrong port: got %q want %s", val, expect) + } } -} -func TestGetIdentities(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/identities"), + asserter := func(target *UserSettings, host, key, expect string) func(t *testing.T) { + return func(t *testing.T) { assert(t, target, host, key, expect) } } - val, err := us.GetAllStrict("hasidentity", "IdentityFile") - if err != nil { - t.Errorf("expected nil err, got %v", err) - } - if len(val) != 1 || val[0] != "file1" { - t.Errorf(`expected ["file1"], got %v`, val) - } + t.Run("WithConfigLocations", func(t *testing.T) { - val, err = us.GetAllStrict("has2identity", "IdentityFile") - if err != nil { - t.Errorf("expected nil err, got %v", err) - } - if len(val) != 2 || val[0] != "f1" || val[1] != "f2" { - t.Errorf(`expected [\"f1\", \"f2\"], got %v`, val) - } + obj := &UserSettings{} - val, err = us.GetAllStrict("randomhost", "IdentityFile") - if err != nil { - t.Errorf("expected nil err, got %v", err) - } - if len(val) != len(defaultProtocol2Identities) { - // TODO: return the right values here. - log.Printf("expected defaults, got %v", val) - } else { - for i, v := range defaultProtocol2Identities { - if val[i] != v { - t.Errorf("invalid %d in val, expected %s got %s", i, v, val[i]) + configuredObject := obj.WithConfigLocations(finderFncA, finderFncB) + + if configuredObject != obj { + t.Errorf("the same instance back, got %v", configuredObject) + } + + if len(obj.configFiles) != 2 { + t.Errorf("number of set file finder function does not match; expected 2 got : %d", len(obj.configFiles)) + } + + assertFileFinder(t, obj, 0, "expected_file_A") + assertFileFinder(t, obj, 1, "expected_file_B") + }) + + t.Run("AddConfigLocations", func(t *testing.T) { + + obj := &UserSettings{configFiles: []ConfigFileFinder{finderFncA}} + + if len(obj.configFiles) != 1 { + t.Errorf("number of set file finder function does not match; expected 1 got : %d", len(obj.configFiles)) + } + + configuredObject := obj.AddConfigLocations(finderFncB, finderFncC) + + if configuredObject != obj { + t.Errorf("the same instance back, got %v", configuredObject) + } + if len(obj.configFiles) != 3 { + t.Errorf("number of set file finder function does not match; expected 1 got : %d", len(obj.configFiles)) + } + + assertFileFinder(t, obj, 0, "expected_file_A") + assertFileFinder(t, obj, 1, "expected_file_B") + assertFileFinder(t, obj, 2, "expected_file_C") + }) + + t.Run("Get", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/config1")}, + } + + val := us.Get("wap", "User") + if val != "root" { + t.Errorf("expected to find User root, got %q", val) + } + }) + + t.Run("GetWithDefault", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/config1")}, + } + + val, err := us.GetStrict("wap", "PasswordAuthentication") + if err != nil { + t.Fatalf("expected nil err, got %v", err) + } + if val != "yes" { + t.Errorf("expected to get PasswordAuthentication yes, got %q", val) + } + }) + + t.Run("GetAllWithDefault", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/config1")}, + } + + val, err := us.GetAllStrict("wap", "PasswordAuthentication") + if err != nil { + t.Fatalf("expected nil err, got %v", err) + } + if len(val) != 1 || val[0] != "yes" { + t.Errorf("expected to get PasswordAuthentication yes, got %q", val) + } + }) + + t.Run("GetIdentities", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/identities")}, + } + + val, err := us.GetAllStrict("hasidentity", "IdentityFile") + if err != nil { + t.Errorf("expected nil err, got %v", err) + } + if len(val) != 1 || val[0] != "file1" { + t.Errorf(`expected ["file1"], got %v`, val) + } + + val, err = us.GetAllStrict("has2identity", "IdentityFile") + if err != nil { + t.Errorf("expected nil err, got %v", err) + } + if len(val) != 2 || val[0] != "f1" || val[1] != "f2" { + t.Errorf(`expected [\"f1\", \"f2\"], got %v`, val) + } + + val, err = us.GetAllStrict("randomhost", "IdentityFile") + if err != nil { + t.Errorf("expected nil err, got %v", err) + } + if len(val) != len(defaultProtocol2Identities) { + // TODO: return the right values here. + log.Printf("expected defaults, got %v", val) + } else { + for i, v := range defaultProtocol2Identities { + if val[i] != v { + t.Errorf("invalid %d in val, expected %s got %s", i, v, val[i]) + } } } - } - val, err = us.GetAllStrict("protocol1", "IdentityFile") - if err != nil { - t.Errorf("expected nil err, got %v", err) - } - if len(val) != 1 || val[0] != "~/.ssh/identity" { - t.Errorf("expected [\"~/.ssh/identity\"], got %v", val) - } -} + val, err = us.GetAllStrict("protocol1", "IdentityFile") + if err != nil { + t.Errorf("expected nil err, got %v", err) + } + if len(val) != 1 || val[0] != "~/.ssh/identity" { + t.Errorf("expected [\"~/.ssh/identity\"], got %v", val) + } + }) -func TestGetInvalidPort(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/invalid-port"), - } + t.Run("GetInvalidPort", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/invalid-port")}, + } - val, err := us.GetStrict("test.test", "Port") - if err == nil { - t.Fatalf("expected non-nil err, got nil") - } - if val != "" { - t.Errorf("expected to get '' for val, got %q", val) - } - if err.Error() != `ssh_config: strconv.ParseUint: parsing "notanumber": invalid syntax` { - t.Errorf("wrong error: got %v", err) - } -} + val, err := us.GetStrict("test.test", "Port") + if err == nil { + t.Fatalf("expected non-nil err, got nil") + } + if val != "" { + t.Errorf("expected to get '' for val, got %q", val) + } + if err.Error() != `ssh_config: strconv.ParseUint: parsing "notanumber": invalid syntax` { + t.Errorf("wrong error: got %v", err) + } + }) -func TestGetNotFoundNoDefault(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/config1"), - } + t.Run("GetNotFoundNoDefault", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/config1")}, + } - val, err := us.GetStrict("wap", "CanonicalDomains") - if err != nil { - t.Fatalf("expected nil err, got %v", err) - } - if val != "" { - t.Errorf("expected to get CanonicalDomains '', got %q", val) - } -} + val, err := us.GetStrict("wap", "CanonicalDomains") + if err != nil { + t.Fatalf("expected nil err, got %v", err) + } + if val != "" { + t.Errorf("expected to get CanonicalDomains '', got %q", val) + } + }) -func TestGetAllNotFoundNoDefault(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/config1"), - } + t.Run("GetAllNotFoundNoDefault", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/config1")}, + } - val, err := us.GetAllStrict("wap", "CanonicalDomains") - if err != nil { - t.Fatalf("expected nil err, got %v", err) - } - if len(val) != 0 { - t.Errorf("expected to get CanonicalDomains '', got %q", val) - } -} + val, err := us.GetAllStrict("wap", "CanonicalDomains") + if err != nil { + t.Fatalf("expected nil err, got %v", err) + } + if len(val) != 0 { + t.Errorf("expected to get CanonicalDomains '', got %q", val) + } + }) -func TestGetWildcard(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/config3"), - } + t.Run("GetWildcard", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/config3")}, + } - val := us.Get("bastion.stage.i.us.example.net", "Port") - if val != "22" { - t.Errorf("expected to find Port 22, got %q", val) - } + val := us.Get("bastion.stage.i.us.example.net", "Port") + if val != "22" { + t.Errorf("expected to find Port 22, got %q", val) + } - val = us.Get("bastion.net", "Port") - if val != "25" { - t.Errorf("expected to find Port 24, got %q", val) - } + val = us.Get("bastion.net", "Port") + if val != "25" { + t.Errorf("expected to find Port 24, got %q", val) + } - val = us.Get("10.2.3.4", "Port") - if val != "23" { - t.Errorf("expected to find Port 23, got %q", val) - } - val = us.Get("101.2.3.4", "Port") - if val != "25" { - t.Errorf("expected to find Port 24, got %q", val) - } - val = us.Get("20.20.20.4", "Port") - if val != "24" { - t.Errorf("expected to find Port 24, got %q", val) - } - val = us.Get("20.20.20.20", "Port") - if val != "25" { - t.Errorf("expected to find Port 25, got %q", val) - } -} + val = us.Get("10.2.3.4", "Port") + if val != "23" { + t.Errorf("expected to find Port 23, got %q", val) + } + val = us.Get("101.2.3.4", "Port") + if val != "25" { + t.Errorf("expected to find Port 24, got %q", val) + } + val = us.Get("20.20.20.4", "Port") + if val != "24" { + t.Errorf("expected to find Port 24, got %q", val) + } + val = us.Get("20.20.20.20", "Port") + if val != "25" { + t.Errorf("expected to find Port 25, got %q", val) + } + }) -func TestGetExtraSpaces(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/extraspace"), - } + t.Run("GetExtraSpaces", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/extraspace")}, + } - val := us.Get("test.test", "Port") - if val != "1234" { - t.Errorf("expected to find Port 1234, got %q", val) - } -} + val := us.Get("test.test", "Port") + if val != "1234" { + t.Errorf("expected to find Port 1234, got %q", val) + } + }) -func TestGetCaseInsensitive(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/config1"), - } + t.Run("GetCaseInsensitive", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/config1")}, + } - val := us.Get("wap", "uSER") - if val != "root" { - t.Errorf("expected to find User root, got %q", val) - } -} + val := us.Get("wap", "uSER") + if val != "root" { + t.Errorf("expected to find User root, got %q", val) + } + }) -func TestGetEmpty(t *testing.T) { - us := &UserSettings{ - userConfigFinder: nullConfigFinder, - systemConfigFinder: nullConfigFinder, - } - val, err := us.GetStrict("wap", "User") - if err != nil { - t.Errorf("expected nil error, got %v", err) - } - if val != "" { - t.Errorf("expected to get empty string, got %q", val) - } -} + t.Run("GetEmpty", func(t *testing.T) { + us := &UserSettings{} -func TestGetEqsign(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/eqsign"), - } + val, err := us.GetStrict("wap", "User") + if err != nil { + t.Errorf("expected nil error, got %v", err) + } + if val != "" { + t.Errorf("expected to get empty string, got %q", val) + } + }) - val := us.Get("test.test", "Port") - if val != "1234" { - t.Errorf("expected to find Port 1234, got %q", val) - } - val = us.Get("test.test", "Port2") - if val != "5678" { - t.Errorf("expected to find Port2 5678, got %q", val) - } -} + t.Run("GetEqsign", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/eqsign")}, + } -var includeFile = []byte(` -# This host should not exist, so we can use it for test purposes / it won't -# interfere with any other configurations. -Host kevinburke.ssh_config.test.example.com - Port 4567 -`) + val := us.Get("test.test", "Port") + if val != "1234" { + t.Errorf("expected to find Port 1234, got %q", val) + } + val = us.Get("test.test", "Port2") + if val != "5678" { + t.Errorf("expected to find Port2 5678, got %q", val) + } + }) -func TestInclude(t *testing.T) { - if testing.Short() { - t.Skip("skipping fs write in short mode") - } - testPath := filepath.Join(homedir(), ".ssh", "kevinburke-ssh-config-test-file") - err := os.WriteFile(testPath, includeFile, 0644) - if err != nil { - t.Skipf("couldn't write SSH config file: %v", err.Error()) - } - defer os.Remove(testPath) - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/include"), - } - val := us.Get("kevinburke.ssh_config.test.example.com", "Port") - if val != "4567" { - t.Errorf("expected to find Port=4567 in included file, got %q", val) - } -} + t.Run("MatchUnsupported", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/match-directive")}, + } -func TestIncludeSystem(t *testing.T) { - if testing.Short() { - t.Skip("skipping fs write in short mode") - } - testPath := filepath.Join("/", "etc", "ssh", "kevinburke-ssh-config-test-file") - err := os.WriteFile(testPath, includeFile, 0644) - if err != nil { - t.Skipf("couldn't write SSH config file: %v", err.Error()) - } - defer os.Remove(testPath) - us := &UserSettings{ - systemConfigFinder: testConfigFinder("testdata/include"), - } - val := us.Get("kevinburke.ssh_config.test.example.com", "Port") - if val != "4567" { - t.Errorf("expected to find Port=4567 in included file, got %q", val) - } -} + _, err := us.GetStrict("test.test", "Port") + if err == nil { + t.Fatal("expected Match directive to error, didn't") + } + if !strings.Contains(err.Error(), "ssh_config: Match directive parsing is unsupported") { + t.Errorf("wrong error: %v", err) + } + }) -var recursiveIncludeFile = []byte(` -Host kevinburke.ssh_config.test.example.com - Include kevinburke-ssh-config-recursive-include -`) + t.Run("IndexInRange", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/config4")}, + } -func TestIncludeRecursive(t *testing.T) { - if testing.Short() { - t.Skip("skipping fs write in short mode") - } - testPath := filepath.Join(homedir(), ".ssh", "kevinburke-ssh-config-recursive-include") - err := os.WriteFile(testPath, recursiveIncludeFile, 0644) - if err != nil { - t.Skipf("couldn't write SSH config file: %v", err.Error()) - } - defer os.Remove(testPath) - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/include-recursive"), - } - val, err := us.GetStrict("kevinburke.ssh_config.test.example.com", "Port") - if err != ErrDepthExceeded { - t.Errorf("Recursive include: expected ErrDepthExceeded, got %v", err) - } - if val != "" { - t.Errorf("non-empty string value %s", val) - } + user, err := us.GetStrict("wap", "User") + if err != nil { + t.Fatal(err) + } + if user != "root" { + t.Errorf("expected User to be %q, got %q", "root", user) + } + }) + + t.Run("DosLinesEndingsDecode", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/dos-lines")}, + } + + user, err := us.GetStrict("wap", "User") + if err != nil { + t.Fatal(err) + } + + if user != "root" { + t.Errorf("expected User to be %q, got %q", "root", user) + } + + host, err := us.GetStrict("wap2", "HostName") + if err != nil { + t.Fatal(err) + } + + if host != "8.8.8.8" { + t.Errorf("expected HostName to be %q, got %q", "8.8.8.8", host) + } + }) + + t.Run("NoTrailingNewline", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/config-no-ending-newline")}, + } + + port, err := us.GetStrict("example", "Port") + if err != nil { + t.Fatal(err) + } + + if port != "4242" { + t.Errorf("wrong port: got %q want 4242", port) + } + }) + + t.Run("fallback resolving", func(t *testing.T) { + + finderUser := testConfigFinder("testdata/test_config_fallback_user") + finderLayer2 := testConfigFinder("testdata/test_config_fallback_layer2") + finderLayer1 := testConfigFinder("testdata/test_config_fallback_layer1") + finderBase := testConfigFinder("testdata/test_config_fallback_base") + + t.Run("user", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{finderUser, finderLayer2, finderLayer1, finderBase}, + } + + t.Run("port custom", asserter(us, "custom", "Port", "2300")) + t.Run("port any", asserter(us, "some-host", "Port", "23")) + t.Run("user custom", asserter(us, "custom", "User", "pete")) + t.Run("user any", asserter(us, "some-host", "User", "foo")) + }) + + t.Run("layer2", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{finderLayer2, finderLayer1, finderBase}, + } + + t.Run("port custom", asserter(us, "custom", "Port", "2300")) + t.Run("port any", asserter(us, "some-host", "Port", "23")) + t.Run("user custom", asserter(us, "custom", "User", "root")) + t.Run("user any", asserter(us, "some-host", "User", "foo")) + }) + + t.Run("layer1", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{finderLayer1, finderBase}, + } + + t.Run("port custom", asserter(us, "custom", "Port", "23")) + t.Run("port any", asserter(us, "some-host", "Port", "23")) + t.Run("user custom", asserter(us, "custom", "User", "bar")) + t.Run("user any", asserter(us, "some-host", "User", "foo")) + }) + + t.Run("base", func(t *testing.T) { + us := &UserSettings{ + configFiles: []ConfigFileFinder{finderBase}, + } + + t.Run("port custom", asserter(us, "custom", "Port", "22")) + t.Run("port any", asserter(us, "some-host", "Port", "22")) + t.Run("user custom", asserter(us, "custom", "User", "foo")) + t.Run("user any", asserter(us, "some-host", "User", "foo")) + }) + }) + + t.Run("Include basic", func(t *testing.T) { + + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/include-basic/config")}, + } + + assert(t, us, "kevinburke.ssh_config.test.example.com", "Port", "4567") + assert(t, us, "kevinburke.ssh_config.test.example.com", "User", "foobar") + }) + + t.Run("Include recursive", func(t *testing.T) { + + us := &UserSettings{ + configFiles: []ConfigFileFinder{testConfigFinder("testdata/include-recursive/config")}, + } + + val, err := us.GetStrict("kevinburke.ssh_config.test.example.com", "Port") + if err != ErrDepthExceeded { + t.Errorf("Recursive include: expected ErrDepthExceeded, got %v", err) + } + if val != "" { + t.Errorf("non-empty string value %s", val) + } + }) + + t.Run("IncludeString", func(t *testing.T) { + data, err := os.ReadFile("testdata/include-basic/config") + if err != nil { + log.Fatal(err) + } + c, err := Decode(bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + s := c.String() + if s != string(data) { + t.Errorf("mismatch: got %q\nwant %q", s, string(data)) + } + }) } -func TestIncludeString(t *testing.T) { - if testing.Short() { - t.Skip("skipping fs write in short mode") - } - data, err := os.ReadFile("testdata/include") +//TODO: this is not the way to to this!!! +// +//func TestIncludeSystem(t *testing.T) { +// if testing.Short() { +// t.Skip("skipping fs write in short mode") +// } +// testPath := filepath.Join("/", "etc", "ssh", "kevinburke-ssh-config-test-file") +// err := os.WriteFile(testPath, includeFile, 0644) +// if err != nil { +// t.Skipf("couldn't write SSH config file: %v", err.Error()) +// } +// defer os.Remove(testPath) +// us := &UserSettings{ +// systemConfigFinder: testConfigFinder("testdata/include"), +// } +// val := us.Get("kevinburke.ssh_config.test.example.com", "Port") +// if val != "4567" { +// t.Errorf("expected to find Port=4567 in included file, got %q", val) +// } +//} + +func TestUserHomeConfigFileFinder(t *testing.T) { + + userHome, err := UserHomeConfigFileFinder() + if err != nil { - log.Fatal(err) + t.Fatalf("no error expected; got %v", err) } - c, err := Decode(bytes.NewReader(data)) - if err != nil { - t.Fatal(err) + + if userHome == "" { + t.Errorf("expected a return value; got %q", userHome) } - s := c.String() - if s != string(data) { - t.Errorf("mismatch: got %q\nwant %q", s, string(data)) + + if !strings.HasSuffix(userHome, "/.ssh/config") { + t.Errorf("expected return value to match default windows folders; got %q", userHome) } + } var matchTests = []struct { @@ -387,83 +563,3 @@ func TestMatches(t *testing.T) { } } } - -func TestMatchUnsupported(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/match-directive"), - } - - _, err := us.GetStrict("test.test", "Port") - if err == nil { - t.Fatal("expected Match directive to error, didn't") - } - if !strings.Contains(err.Error(), "ssh_config: Match directive parsing is unsupported") { - t.Errorf("wrong error: %v", err) - } -} - -func TestIndexInRange(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/config4"), - } - - user, err := us.GetStrict("wap", "User") - if err != nil { - t.Fatal(err) - } - if user != "root" { - t.Errorf("expected User to be %q, got %q", "root", user) - } -} - -func TestDosLinesEndingsDecode(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/dos-lines"), - } - - user, err := us.GetStrict("wap", "User") - if err != nil { - t.Fatal(err) - } - - if user != "root" { - t.Errorf("expected User to be %q, got %q", "root", user) - } - - host, err := us.GetStrict("wap2", "HostName") - if err != nil { - t.Fatal(err) - } - - if host != "8.8.8.8" { - t.Errorf("expected HostName to be %q, got %q", "8.8.8.8", host) - } -} - -func TestNoTrailingNewline(t *testing.T) { - us := &UserSettings{ - userConfigFinder: testConfigFinder("testdata/config-no-ending-newline"), - systemConfigFinder: nullConfigFinder, - } - - port, err := us.GetStrict("example", "Port") - if err != nil { - t.Fatal(err) - } - - if port != "4242" { - t.Errorf("wrong port: got %q want 4242", port) - } -} - -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) - } -} diff --git a/example_test.go b/example_test.go index a7c16d6..cb757dc 100644 --- a/example_test.go +++ b/example_test.go @@ -4,9 +4,8 @@ import ( "fmt" "path/filepath" "strings" - - "github.com/kevinburke/ssh_config" ) +import "github.com/kevinburke/ssh_config" func ExampleHost_Matches() { pat, _ := ssh_config.NewPattern("test.*.example.com") @@ -48,11 +47,15 @@ func ExampleDefault() { // } -func ExampleUserSettings_ConfigFinder() { +func ExampleConfigFileFinder() { // This can be used to test SSH config parsing. u := ssh_config.UserSettings{} - u.ConfigFinder(func() string { - return filepath.Join("testdata", "test_config") + u.WithConfigLocations(func() (string, error) { + return filepath.Join("testdata", "test_config"), nil }) - u.Get("example.com", "Host") + + fmt.Println(u.Get("example.com", "HostName")) + // Output: + // wap.example.org + // } diff --git a/parser.go b/parser.go index 2b1e718..39379d5 100644 --- a/parser.go +++ b/parser.go @@ -12,10 +12,8 @@ type sshParser struct { 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 - depth uint8 + cwd string + depth uint8 } type sshParserStateFn func() sshParserStateFn @@ -138,7 +136,7 @@ func (p *sshParser) parseKV() sshParserStateFn { } 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(p.cwd, strings.Split(val.val, " "), hasEquals, key.Position, comment, p.depth+1) if err == ErrDepthExceeded { p.raiseError(val, err) return nil @@ -177,7 +175,7 @@ func (p *sshParser) parseComment() sshParserStateFn { return p.parseStart } -func parseSSH(flow chan token, system bool, depth uint8) *Config { +func parseSSH(flow chan token, cwd string, depth uint8) *Config { // Ensure we consume tokens to completion even if parser exits early defer func() { for range flow { @@ -192,7 +190,7 @@ func parseSSH(flow chan token, system bool, depth uint8) *Config { tokensBuffer: make([]token, 0), currentTable: make([]string, 0), seenTableKeys: make([]string, 0), - system: system, + cwd: cwd, depth: depth, } parser.run() diff --git a/testdata/include b/testdata/include deleted file mode 100644 index ee238dd..0000000 --- a/testdata/include +++ /dev/null @@ -1,4 +0,0 @@ -Host kevinburke.ssh_config.test.example.com - # This file (or files) needs to be found in ~/.ssh or /etc/ssh, depending on - # the test. - Include kevinburke-ssh-config-*-file diff --git a/testdata/include-basic/config b/testdata/include-basic/config new file mode 100644 index 0000000..11a757d --- /dev/null +++ b/testdata/include-basic/config @@ -0,0 +1,2 @@ +Host kevinburke.ssh_config.test.example.com + Include kevinburke-ssh-config-*-file diff --git a/testdata/include-basic/kevinburke-ssh-config-test-file b/testdata/include-basic/kevinburke-ssh-config-test-file new file mode 100644 index 0000000..6c0eccd --- /dev/null +++ b/testdata/include-basic/kevinburke-ssh-config-test-file @@ -0,0 +1,2 @@ +Host kevinburke.ssh_config.test.example.com + Port 4567 diff --git a/testdata/include-basic/kevinburke-ssh-config-test2-file b/testdata/include-basic/kevinburke-ssh-config-test2-file new file mode 100644 index 0000000..530b2ce --- /dev/null +++ b/testdata/include-basic/kevinburke-ssh-config-test2-file @@ -0,0 +1,2 @@ +Host kevinburke.ssh_config.test.example.com + User foobar diff --git a/testdata/include-recursive b/testdata/include-recursive deleted file mode 100644 index 8a3cd3d..0000000 --- a/testdata/include-recursive +++ /dev/null @@ -1,4 +0,0 @@ -Host kevinburke.ssh_config.test.example.com - # This file (or files) needs to be found in ~/.ssh or /etc/ssh, depending on - # the test. It should include itself. - Include kevinburke-ssh-config-recursive-include diff --git a/testdata/include-recursive/config b/testdata/include-recursive/config new file mode 100644 index 0000000..3a6e4bc --- /dev/null +++ b/testdata/include-recursive/config @@ -0,0 +1,2 @@ +Host kevinburke.ssh_config.test.example.com + Include kevinburke-ssh-config-recursive-include diff --git a/testdata/include-recursive/kevinburke-ssh-config-recursive-include b/testdata/include-recursive/kevinburke-ssh-config-recursive-include new file mode 100644 index 0000000..f3cc756 --- /dev/null +++ b/testdata/include-recursive/kevinburke-ssh-config-recursive-include @@ -0,0 +1,2 @@ +Host kevinburke.ssh_config.test.example.com + Include kevinburke-ssh-config-recursive-include diff --git a/testdata/test_config b/testdata/test_config new file mode 100644 index 0000000..c52854b --- /dev/null +++ b/testdata/test_config @@ -0,0 +1,3 @@ +Host example.com + HostName wap.example.org + User root diff --git a/testdata/test_config_fallback_base b/testdata/test_config_fallback_base new file mode 100644 index 0000000..9d5fb37 --- /dev/null +++ b/testdata/test_config_fallback_base @@ -0,0 +1,5 @@ + + +Host * + User foo + Port 22 diff --git a/testdata/test_config_fallback_layer1 b/testdata/test_config_fallback_layer1 new file mode 100644 index 0000000..a4dbddc --- /dev/null +++ b/testdata/test_config_fallback_layer1 @@ -0,0 +1,6 @@ + +Host * + Port 23 + +Host custom + User bar diff --git a/testdata/test_config_fallback_layer2 b/testdata/test_config_fallback_layer2 new file mode 100644 index 0000000..58e5cad --- /dev/null +++ b/testdata/test_config_fallback_layer2 @@ -0,0 +1,4 @@ + +Host custom + User root + Port 2300 \ No newline at end of file diff --git a/testdata/test_config_fallback_user b/testdata/test_config_fallback_user new file mode 100644 index 0000000..d3528b5 --- /dev/null +++ b/testdata/test_config_fallback_user @@ -0,0 +1,3 @@ + +Host custom + User pete