Files
dpsapi/config.go
T
bryanpedini 88d7d6d2b9 feat(core): add configuration, logging and sqlite persistence
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.
2026-10-06 10:52:25 +02:00

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
}