- read netssh.yaml from the user config directory only, not from the working directory - refuse addresses from NetBox or the cache that are neither an IP address nor a hostname (ssh read "-oProxyCommand=…" as an option) - resolver chain stops at an API failure; only "no address" falls through to the next strategy - netssh --help and -h show netssh's help - start at most one background cache refresh per 5 minutes - validate the legacy netbox.url - page through all IP addresses of a host - CI: run format check, vet, and tests on pull requests and main - changelog for v0.0.25 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
68 lines
2.4 KiB
Go
68 lines
2.4 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"net/url"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
var instanceName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_-]*$`)
|
|
|
|
// ValidateNetBoxes keeps source names usable as instance/hostname selectors.
|
|
func (c *Config) ValidateNetBoxes() error {
|
|
if len(c.NetBoxes) > 0 && c.NetBox.URL != "" {
|
|
return fmt.Errorf("configure either netbox or netboxes, not both")
|
|
}
|
|
if c.NetBox.URL != "" && !validNetBoxURL(c.NetBox.URL) {
|
|
return fmt.Errorf("netbox.url %q must be an HTTP(S) URL without query or fragment", c.NetBox.URL)
|
|
}
|
|
seen := make(map[string]bool)
|
|
for _, source := range c.NetBoxes {
|
|
if !instanceName.MatchString(source.Name) {
|
|
return fmt.Errorf("netboxes.name %q must contain only letters, digits, underscores, or hyphens", source.Name)
|
|
}
|
|
name := strings.ToLower(source.Name)
|
|
if seen[name] {
|
|
return fmt.Errorf("duplicate NetBox instance name %q", source.Name)
|
|
}
|
|
seen[name] = true
|
|
if !validNetBoxURL(source.URL) {
|
|
return fmt.Errorf("NetBox %q requires an HTTP(S) URL without query or fragment", source.Name)
|
|
}
|
|
if source.TokenVersion < 0 || source.TokenVersion > 2 {
|
|
return fmt.Errorf("NetBox %q: token_version must be 0, 1, or 2", source.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validNetBoxURL accepts base URLs that API paths can be appended to.
|
|
func validNetBoxURL(raw string) bool {
|
|
u, err := url.Parse(raw)
|
|
return err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != "" && u.RawQuery == "" && u.Fragment == ""
|
|
}
|
|
|
|
func (c *Config) HasNetBox() bool { return c.NetBox.URL != "" || len(c.NetBoxes) > 0 }
|
|
|
|
// HasHosts reports whether netssh has anything to connect to: NetBox or static hosts.
|
|
func (c *Config) HasHosts() bool { return c.HasNetBox() || len(c.Hosts) > 0 }
|
|
|
|
// CachePath isolates named sources from legacy caches and from changed endpoints.
|
|
// Rotating a token or reordering the sources does not discard usage history.
|
|
func (c *Config) CachePath() string {
|
|
if len(c.NetBoxes) == 0 {
|
|
return c.Cache.Path
|
|
}
|
|
var identities []string
|
|
for _, source := range c.NetBoxes {
|
|
identities = append(identities, strings.ToLower(source.Name)+"\x00"+strings.TrimRight(source.URL, "/"))
|
|
}
|
|
sort.Strings(identities)
|
|
sum := sha256.Sum256([]byte(strings.Join(identities, "\x00")))
|
|
return filepath.Join(filepath.Dir(c.Cache.Path), fmt.Sprintf("instances-%x", sum[:8]), filepath.Base(c.Cache.Path))
|
|
}
|