// Package config loads est tunnel configuration and command-line options. package config import ( "errors" "os" "time" "github.com/BurntSushi/toml" ) // ProxyDirection identifies the direction of an SSH port forward. type ProxyDirection string const ( // RemoteToLocal creates a local SSH forward with the -L option. RemoteToLocal ProxyDirection = "rtl" // LocalToRemote creates a remote SSH forward with the -R option. LocalToRemote ProxyDirection = "ltr" ) const ( // DefaultRetryCount is the number of reconnect attempts used when retry is // not specified. DefaultRetryCount = 5 // DefaultRetryDelay is the wait between reconnect attempts when retry is // not specified. DefaultRetryDelay = 5 * time.Second ) var ( // ErrInvalidRetryCount is returned when retry count is less than -1. ErrInvalidRetryCount = errors.New("retry count must be -1 or greater") // ErrInvalidRetryDelay is returned when retry delay is invalid or not positive. ErrInvalidRetryDelay = errors.New("retry delay must be a positive duration") ) // RetryConfig configures tunnel reconnection behavior. // Count is the number of attempts after the initial SSH process exits; -1 // retries indefinitely. Delay uses time.ParseDuration syntax, such as "5s". type RetryConfig struct { Count *int `toml:"count"` Delay string `toml:"delay"` } // RetrySettings is a validated retry configuration ready for execution. type RetrySettings struct { Count int Delay time.Duration } // Settings resolves RetryConfig fields with est's default retry values. func (retry RetryConfig) Settings() (RetrySettings, error) { settings := RetrySettings{Count: DefaultRetryCount, Delay: DefaultRetryDelay} if retry.Count != nil { settings.Count = *retry.Count } if settings.Count < -1 { return RetrySettings{}, ErrInvalidRetryCount } if retry.Delay != "" { delay, err := time.ParseDuration(retry.Delay) if err != nil || delay <= 0 { return RetrySettings{}, ErrInvalidRetryDelay } settings.Delay = delay } return settings, nil } // ProxyEntry defines one SSH tunnel. type ProxyEntry struct { Direction ProxyDirection `toml:"direction"` RemotePort uint16 `toml:"remotePort"` RemoteIP string `toml:"remoteIP"` LocalPort uint16 `toml:"localPort"` LocalIP string `toml:"localIP"` Host string `toml:"host"` IdentityFile string `toml:"identityFile,omitempty"` Retry *RetryConfig `toml:"retry"` } // Config is the TOML configuration used by est. type Config struct { SSHConfig string `toml:"sshConfig"` Retry *RetryConfig `toml:"retry"` Entries []ProxyEntry `toml:"entry"` } // RetrySettings resolves the retry configuration for entry. An entry retry // block fully overrides the top-level retry block. func (cfg Config) RetrySettings(entry ProxyEntry) (RetrySettings, error) { if entry.Retry != nil { return entry.Retry.Settings() } if cfg.Retry != nil { return cfg.Retry.Settings() } return RetryConfig{}.Settings() } // LoadConfig decodes a TOML configuration file from path. func LoadConfig(path string) (Config, error) { var config Config file, err := os.Open(path) if err != nil { return config, err } defer func() { _ = file.Close() }() if _, err := toml.NewDecoder(file).Decode(&config); err != nil { return config, err } return config, nil }