Files
reverse/config/config.go
mleku aba3909038 Add reverse proxy boilerplate with configuration management
- Introduced the main entry point `main.go` with basic configuration loading and error handling.
- Added `config` package to handle environment variables, help printing, and default settings.
- Configured required dependencies in `go.mod` and included their checksums in `go.sum`.
- Created `.gitignore` file for common Go development scenarios.
2025-08-19 16:45:48 +01:00

226 lines
6.8 KiB
Go

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
}