From 4e504dcd0a644944cc1e7d162968a00ba93daf97 Mon Sep 17 00:00:00 2001 From: "Ronald G. Minnich" Date: Wed, 27 Apr 2022 09:15:49 -0700 Subject: [PATCH] Implement percent operators for config files There are still one or two missing; consider this a draft until they are finished. Also, fix one or two nits found by golint. This change introduces no new findings by golint, go vet, and golangci-lint. Signed-off-by: Ronald G. Minnich --- config.go | 102 +++++++++++++++++++++++++++++++++++++++++++++++-- config_test.go | 35 +++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/config.go b/config.go index 00d815c..4e93ba7 100644 --- a/config.go +++ b/config.go @@ -65,9 +65,8 @@ func homedir() string { user, err := osuser.Current() if err == nil { return user.HomeDir - } else { - return os.Getenv("HOME") } + return os.Getenv("HOME") } func userConfigFinder() string { @@ -333,6 +332,102 @@ type Config struct { position Position } +// %% A literal `%'. +// %C Shorthand for %l%h%p%r. +// %d Local user's home directory. +// %h The remote hostname. +// %i The local user ID. +// %L The local hostname. +// %l The local hostname, including the domain name. +// %n The original remote hostname, as given on the command line. +// %p The remote port. +// %r The remote username. +// %u The local username. +func (host *Host) percent(alias, val string) string { + var ( + b bytes.Buffer + sawPercent bool + ) + for _, c := range val { + if sawPercent { + sawPercent = false + switch c { + case 'd': + b.WriteString(homedir()) + case 'h': + b.WriteString(host.tryKV(alias, "HostName")) + case 'i': + b.WriteString(fmt.Sprintf("%d", os.Getuid())) + case 'L': + if h, err := os.Hostname(); err != nil { + b.WriteString(fmt.Sprintf("%%!L(%v)", err)) + } else { + b.WriteString(h) + } + case 'n': + b.WriteString(alias) + case 'p': + b.WriteString(host.tryKV(alias, "Port")) + case 'r': + b.WriteString(host.tryKV(alias, "User")) + case 'u': + b.WriteString(os.Getenv("USER")) + case '%': + b.WriteString("%") + default: + // In the event of a bad format char, fmt returns + // the mangled string and no error. + // It may be best to follow that practice, as + // it gives you a much better idea where things + // went wrong. + b.WriteString(`%!` + string(c)) + } + continue + } + if c != '%' { + b.WriteByte(byte(c)) + continue + } + sawPercent = true + } + if sawPercent { + b.WriteString("%!(NOVERB)") + } + return b.String() +} + +func (host *Host) findKV(alias, key string) (string, error) { + lowerKey := strings.ToLower(key) + for _, node := range host.Nodes { + switch t := node.(type) { + case *Empty: + continue + case *KV: + // "keys are case insensitive" per the spec + lkey := strings.ToLower(t.Key) + if lkey == "match" { + panic("can't handle Match directives") + } + if lkey == lowerKey { + return t.Value, nil + } + case *Include: + val := t.Get(alias, key) + if val != "" { + return val, nil + } + default: + return "", fmt.Errorf("unknown Node type %v", t) + } + } + return "", fmt.Errorf("%v has no key %v", alias, key) +} + +func (host *Host) tryKV(alias, key string) string { + v, _ := host.findKV(alias, key) + return v +} + // Get finds the first value in the configuration that matches the alias and // contains key. Get returns the empty string if no value was found, or if the // Config contains an invalid conditional Include value. @@ -411,6 +506,7 @@ func (c Config) String() string { return marshal(c).String() } +// MarshalText implements Marshal func (c Config) MarshalText() ([]byte, error) { return marshal(c).Bytes(), nil } @@ -792,7 +888,7 @@ func init() { func newConfig() *Config { return &Config{ Hosts: []*Host{ - &Host{ + { implicit: true, Patterns: []*Pattern{matchAll}, Nodes: make([]Node, 0), diff --git a/config_test.go b/config_test.go index 0bd350f..5891aaf 100644 --- a/config_test.go +++ b/config_test.go @@ -455,3 +455,38 @@ func TestNoTrailingNewline(t *testing.T) { t.Errorf("wrong port: got %q want 4242", port) } } + +func TestPercent(t *testing.T) { + b := bytes.NewBufferString(`Host wap + HostName wap.example.org + Port 22 + User root + KexAlgorithms diffie-hellman-group1-sha1 +`) + cfg, err := Decode(b) + if err != nil { + t.Fatal(err) + } + host := cfg.Hosts[1] + t.Logf("cfg is %v, %d hosts, Hosts %v, host %v", cfg, len(cfg.Hosts), cfg.Hosts, host) + home := os.Getenv("HOME") + user := os.Getenv("USER") + + for _, tt := range []struct { + in string + out string + }{ + {"hi", "hi"}, + {"%dhi", home + "hi"}, + {"%uhi", user + "hi"}, + {"%h.%n.%p.%r.%u", "wap.example.org.wap.22.root." + user}, + {"%Z", "%!Z"}, + {"%", "%!(NOVERB)"}, + {"%d%", home + "%!(NOVERB)"}, + } { + o := host.percent("wap", tt.in) + if o != tt.out { + t.Errorf("%q: got %q, want %q", tt.in, o, tt.out) + } + } +}