From aba39090382fb04e221ede28a3e931993d4fbb91 Mon Sep 17 00:00:00 2001 From: mleku Date: Tue, 19 Aug 2025 16:45:48 +0100 Subject: [PATCH] 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. --- .gitignore | 110 +++++++++++++++++++++++ config/config.go | 225 +++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 16 ++++ go.sum | 15 ++++ main.go | 21 +++++ 5 files changed, 387 insertions(+) create mode 100644 .gitignore create mode 100644 config/config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02e0cb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,110 @@ +# Allowlisting gitignore template for GO projects prevents us +# from adding various unwanted local files, such as generated +# files, developer configurations or IDE-specific files etc. +# +# Recommended: Go.AllowList.gitignore + +# Ignore everything +* + +# Especially these +.vscode +.vscode/ +.vscode/** +**/.vscode +**/.vscode/** +node_modules +node_modules/ +node_modules/** +**/node_modules +**/node_modules/ +**/node_modules/** +/test* +.idea +.idea/ +.idea/** +/.idea/ +/.idea/** +/.idea +# and others +/go.work.sum +/secp256k1/ + +# But not these files... +!/.gitignore +!*.go +!go.sum +!go.mod +!*.md +!LICENSE +!*.sh +!Makefile +!*.json +!*.pdf +!*.csv +!*.py +!*.mediawiki +!*.did +!*.rs +!*.toml +!*.file +!.gitkeep +!pkg/eth/** +!*.h +!*.c +!*.proto +!bundleData +!*.item +!*.bin +!*.yml +!*.yaml +!*.tmpl +!*.s +!*.asm +!.gitmodules +!*.txt +!*.sum +!pkg/version +!*.service +!*.benc +!*.png +!*.adoc +!*.js +!*.bash +!PATENTS +!*.css +!*.ts +!*.html +!Dockerfile +!*.lock +!*.nix +!license +!readme +!*.ico +!.idea/* +!*.xml +!.name +!.gitignore +!version +!out.jsonl +# ...even if they are in subdirectories +!*/ +/blocklist.json +/gui/gui/main.wasm +/gui/gui/index.html +pkg/database/testrealy +/.idea/workspace.xml +/.idea/dictionaries/project.xml +/.idea/shelf/Add_tombstone_handling__enhance_event_ID_logic__update_imports.xml +/.idea/.gitignore +/.idea/misc.xml +/.idea/modules.xml +/.idea/orly.dev.iml +/.idea/vcs.xml +/.idea/codeStyles/codeStyleConfig.xml +/.idea/material_theme_project_new.xml +/.idea/orly.iml +/.idea/go.imports.xml +/.idea/inspectionProfiles/Project_Default.xml +/.idea/.name +/.idea/reverse.iml diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..f363d48 --- /dev/null +++ b/config/config.go @@ -0,0 +1,225 @@ +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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2c4c1e6 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/mleku/reverse + +go 1.25.0 + +require ( + github.com/mleku/lol v1.0.1 + go-simpler.org/env v0.12.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.35.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8ab869b --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mleku/lol v1.0.1 h1:CMGpeMTh2iE7Xc4/dtJxs0vZqwRmPiGxsqpWxdmOZKc= +github.com/mleku/lol v1.0.1/go.mod h1:daW3rL0XP4ZKscvWn990AJCrJs2Lsu+sdrI9cWbacWE= +go-simpler.org/env v0.12.0 h1:kt/lBts0J1kjWJAnB740goNdvwNxt5emhYngL0Fzufs= +go-simpler.org/env v0.12.0/go.mod h1:cc/5Md9JCUM7LVLtN0HYjPTDcI3Q8TDaPlNTAlDU+WI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/main.go b/main.go new file mode 100644 index 0000000..18a6c4c --- /dev/null +++ b/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "os" + + "github.com/mleku/lol/chk" + "github.com/mleku/reverse/config" +) + +func main() { + var err error + var cfg *config.C + if cfg, err = config.New(); chk.T(err) { + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %s\n\n", err) + } + config.PrintHelp(cfg, os.Stderr) + os.Exit(0) + } +}