package config import ( "fmt" "io" "os" "reflect" "sort" "strings" "time" "github.com/mleku/lol/chk" "go-simpler.org/env" ) type C struct { LogLevel string `env:"REVERSE_LOG_LEVEL" default:"info" usage:"Log level: fatal error warn info debug trace"` Listen string `env:"REVERSE_LISTEN" usage:"Listen address for reverse proxy" default:":443"` Mapping string `env:"REVERSE_MAPPING" usage:"file containing domain/target mappings for reverse proxy" default:"~/.config/reverse/mapping.conf"` Cache string `env:"REVERSE_CERTCACHE" usage:"directory where certificates from letsencrypt are stored" default:"~/.cache/reverse"` HSTS bool `env:"REVERSE_HSTS" usage:"add Strict-TransportSecurity header" default:"false"` Email string `env:"REVERSE_EMAIL" usage:"email address presented to letsencrypt CA"` HTTP string `env:"REVERSE_HTTP" usage:"http address for HTTP->HTTPS upgrades" default:":80"` Idle time.Duration `env:"REVERSE_IDLE" usage:"how long idle connection is kept before closing" default:"1m"` } func New() (cfg *C, err error) { cfg = new(C) if err = env.Load(cfg, &env.Options{SliceSep: ","}); chk.T(err) { return } if GetEnv() { PrintEnv(cfg, os.Stdout) os.Exit(0) } if HelpRequested() { PrintHelp(cfg, os.Stderr) os.Exit(0) } return } // HelpRequested determines if the command line arguments indicate a request for help // // # Return Values // // - help: A boolean value indicating true if a help flag was detected in the // command line arguments, false otherwise // // # Expected Behaviour // // The function checks the first command line argument for common help flags and // returns true if any of them are present. Returns false if no help flag is found func HelpRequested() (help bool) { if len(os.Args) > 1 { switch strings.ToLower(os.Args[1]) { case "help", "-h", "--h", "-help", "--help", "?": help = true } } return } // GetEnv checks if the first command line argument is "env" and returns // whether the environment configuration should be printed. // // # Return Values // // - requested: A boolean indicating true if the 'env' argument was // provided, false otherwise. // // # Expected Behaviour // // The function returns true when the first command line argument is "env" // (case-insensitive), signalling that the environment configuration should be // printed. Otherwise, it returns false. func GetEnv() (requested bool) { if len(os.Args) > 1 { switch strings.ToLower(os.Args[1]) { case "env": requested = true } } return } // KV is a key/value pair. type KV struct{ Key, Value string } // KVSlice is a sortable slice of key/value pairs, designed for managing // configuration data and enabling operations like merging and sorting based on // keys. type KVSlice []KV func (kv KVSlice) Len() int { return len(kv) } func (kv KVSlice) Less(i, j int) bool { return kv[i].Key < kv[j].Key } func (kv KVSlice) Swap(i, j int) { kv[i], kv[j] = kv[j], kv[i] } // Compose merges two KVSlice instances into a new slice where key-value pairs // from the second slice override any duplicate keys from the first slice. // // # Parameters // // - kv2: The second KVSlice whose entries will be merged with the receiver. // // # Return Values // // - out: A new KVSlice containing all entries from both slices, with keys // from kv2 taking precedence over keys from the receiver. // // # Expected Behaviour // // The method returns a new KVSlice that combines the contents of the receiver // and kv2. If any key exists in both slices, the value from kv2 is used. The // resulting slice remains sorted by keys as per the KVSlice implementation. func (kv KVSlice) Compose(kv2 KVSlice) (out KVSlice) { // duplicate the initial KVSlice for _, p := range kv { out = append(out, p) } out: for i, p := range kv2 { for j, q := range out { // if the key is repeated, replace the value if p.Key == q.Key { out[j].Value = kv2[i].Value continue out } } out = append(out, p) } return } // EnvKV generates key/value pairs from a configuration object's struct tags // // # Parameters // // - cfg: A configuration object whose struct fields are processed for env tags // // # Return Values // // - m: A KVSlice containing key/value pairs derived from the config's env tags // // # Expected Behaviour // // Processes each field of the config object, extracting values tagged with // "env" and converting them to strings. Skips fields without an "env" tag. // Handles various value types including strings, integers, booleans, durations, // and string slices by joining elements with commas. func EnvKV(cfg any) (m KVSlice) { t := reflect.TypeOf(cfg) for i := 0; i < t.NumField(); i++ { k := t.Field(i).Tag.Get("env") v := reflect.ValueOf(cfg).Field(i).Interface() var val string switch v.(type) { case string: val = v.(string) case int, bool, time.Duration: val = fmt.Sprint(v) case []string: arr := v.([]string) if len(arr) > 0 { val = strings.Join(arr, ",") } } // this can happen with embedded structs if k == "" { continue } m = append(m, KV{k, val}) } return } // PrintEnv outputs sorted environment key/value pairs from a configuration object // to the provided writer // // # Parameters // // - cfg: Pointer to the configuration object containing env tags // // - printer: Destination for the output, typically an io.Writer implementation // // # Expected Behaviour // // Outputs each environment variable derived from the config's struct tags in // sorted order, formatted as "key=value\n" to the specified writer func PrintEnv(cfg *C, printer io.Writer) { kvs := EnvKV(*cfg) sort.Sort(kvs) for _, v := range kvs { _, _ = fmt.Fprintf(printer, "%s=%s\n", v.Key, v.Value) } } // PrintHelp prints help information including application version, environment // variable configuration, and details about .env file handling to the provided // writer // // # Parameters // // - cfg: Configuration object containing app name and config directory path // // - printer: Output destination for the help text // // # Expected Behaviour // // Prints application name and version followed by environment variable // configuration details, explains .env file behaviour including automatic // loading and custom path options, and displays current configuration values // using PrintEnv. Outputs all information to the specified writer func PrintHelp(cfg *C, printer io.Writer) { _, _ = fmt.Fprintf(printer, "reverse usage:\n\n") env.Usage(cfg, printer, &env.Options{SliceSep: ","}) _, _ = fmt.Fprintf( printer, "\nCLI parameter 'help' also prints this information\n"+ "use the parameter 'env' to print out the current configuration to the terminal\n\n", ) _, _ = fmt.Fprintf(printer, "current configuration:\n\n") PrintEnv(cfg, printer) return }