DB_PATH, HTTP_ADDR, HTTP_PORT and LOG_LEVEL parsing with validation; slog text logger with a TRACE level and token redaction; embedded idempotent schema on CGO-free SQLite (WAL, busy timeout); token store (create, list, deactivate, authenticate) and append-only audit store with ordered, limited and windowed queries; shared unit test helpers.
135 lines
3.5 KiB
Go
135 lines
3.5 KiB
Go
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"strconv"
|
|
)
|
|
|
|
// Environment variable names
|
|
const (
|
|
envDBPath = "DB_PATH"
|
|
envHTTPAddr = "HTTP_ADDR"
|
|
envHTTPPort = "HTTP_PORT"
|
|
envLogLevel = "LOG_LEVEL"
|
|
)
|
|
|
|
// Default values for the optional environment variables
|
|
const (
|
|
defaultHTTPAddr = "0.0.0.0"
|
|
defaultHTTPPort = 80
|
|
defaultLogLevel = slog.LevelInfo
|
|
)
|
|
|
|
// errConfig marks configuration errors (exit code 2)
|
|
var errConfig = errors.New("configuration error")
|
|
|
|
// Config is the fully validated runtime configuration
|
|
type Config struct {
|
|
DBPath string
|
|
HTTPAddr string
|
|
HTTPPort int
|
|
LogLevel slog.Level
|
|
}
|
|
|
|
// ListenAddress returns the host:port pair the HTTP server binds to
|
|
func (c Config) ListenAddress() string {
|
|
return net.JoinHostPort(c.HTTPAddr, strconv.Itoa(c.HTTPPort))
|
|
}
|
|
|
|
// ListenNetwork returns "tcp4" for IPv4 addresses (so 0.0.0.0 binds IPv4 only)
|
|
// and "tcp" for IPv6 addresses (so :: binds dual-stack where the OS allows it)
|
|
func (c Config) ListenNetwork() string {
|
|
if ip := net.ParseIP(c.HTTPAddr); ip != nil && ip.To4() != nil {
|
|
return "tcp4"
|
|
}
|
|
return "tcp"
|
|
}
|
|
|
|
// configErrorf builds an error wrapping errConfig
|
|
func configErrorf(format string, args ...any) error {
|
|
return fmt.Errorf("%w: "+format, append([]any{errConfig}, args...)...)
|
|
}
|
|
|
|
// LoadConfig reads and validates the configuration from the given environment lookup function
|
|
// Variables are validated in the order DB_PATH, HTTP_ADDR, HTTP_PORT, LOG_LEVEL; the first error wins
|
|
func LoadConfig(lookupEnv func(string) (string, bool)) (Config, error) {
|
|
cfg := Config{
|
|
HTTPAddr: defaultHTTPAddr,
|
|
HTTPPort: defaultHTTPPort,
|
|
LogLevel: defaultLogLevel,
|
|
}
|
|
|
|
// DB_PATH is mandatory
|
|
v, ok := lookupEnv(envDBPath)
|
|
if !ok {
|
|
return Config{}, configErrorf("%s is required", envDBPath)
|
|
}
|
|
if v == "" {
|
|
return Config{}, configErrorf("%s must not be empty", envDBPath)
|
|
}
|
|
cfg.DBPath = v
|
|
|
|
// HTTP_ADDR must be a literal IPv4 or IPv6 address
|
|
if v, ok := lookupEnv(envHTTPAddr); ok {
|
|
if v == "" {
|
|
return Config{}, configErrorf("%s must not be empty", envHTTPAddr)
|
|
}
|
|
if net.ParseIP(v) == nil {
|
|
return Config{}, configErrorf("invalid %s %q: must be an IPv4 or IPv6 address", envHTTPAddr, v)
|
|
}
|
|
cfg.HTTPAddr = v
|
|
}
|
|
|
|
// HTTP_PORT must be a decimal integer in 1-65535
|
|
if v, ok := lookupEnv(envHTTPPort); ok {
|
|
if v == "" {
|
|
return Config{}, configErrorf("%s must not be empty", envHTTPPort)
|
|
}
|
|
port, err := parsePort(v)
|
|
if err != nil {
|
|
return Config{}, configErrorf("invalid %s %q: must be an integer between 1 and 65535", envHTTPPort, v)
|
|
}
|
|
cfg.HTTPPort = port
|
|
}
|
|
|
|
// LOG_LEVEL must be one of the five supported names
|
|
if v, ok := lookupEnv(envLogLevel); ok {
|
|
if v == "" {
|
|
return Config{}, configErrorf("%s must not be empty", envLogLevel)
|
|
}
|
|
level, err := ParseLogLevel(v)
|
|
if err != nil {
|
|
return Config{}, configErrorf("invalid %s %q: must be one of ERROR, WARN, INFO, DEBUG, TRACE", envLogLevel, v)
|
|
}
|
|
cfg.LogLevel = level
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// parsePort accepts only ASCII decimal digits (no sign, no whitespace) in the range 1-65535
|
|
func parsePort(s string) (int, error) {
|
|
if s == "" {
|
|
return 0, errors.New("empty port")
|
|
}
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] < '0' || s[i] > '9' {
|
|
return 0, errors.New("port is not a decimal integer")
|
|
}
|
|
}
|
|
port, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if port < 1 || port > 65535 {
|
|
return 0, errors.New("port out of range")
|
|
}
|
|
return port, nil
|
|
}
|