check / check (push) Waiting to run
config.New read the number and true/false settings with viper's GetInt, GetFloat64 and GetBool. These return zero or false for a value that does not parse, so a typo silently changed how the server ran. config.New now parses these settings with strconv. It also checks that MESSAGE_MAX_AGE, QUEUE_MAX_AGE and SESSION_IDLE_TIMEOUT are valid Go durations. A value that does not parse makes config.New return an error naming the setting and its value, and startup stops. Unset settings keep their defaults. Model: opus-5-5
215 lines
6.1 KiB
Go
215 lines
6.1 KiB
Go
// Package config provides application configuration via environment and files.
|
|
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/spf13/viper"
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/neoirc/internal/globals"
|
|
"sneak.berlin/go/neoirc/internal/logger"
|
|
|
|
_ "github.com/joho/godotenv/autoload" // loads .env file
|
|
)
|
|
|
|
const defaultMOTD = ` _ __ ___ ___ (_)_ __ ___
|
|
| '_ \ / _ \/ _ \ | | '__/ __|
|
|
| | | | __/ (_) || | | | (__
|
|
|_| |_|\___|\___/ |_|_| \___|
|
|
|
|
Welcome to NeoIRC — IRC semantics over HTTP.
|
|
Type /help for available commands.`
|
|
|
|
// DefaultDBURL is the database URL used when DBURL is not set. Its
|
|
// _pragma parameters, which the SQLite driver runs on every connection
|
|
// it opens, set WAL mode and a 5000 ms busy timeout.
|
|
const DefaultDBURL = "file:///var/lib/neoirc/state.db" +
|
|
"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)"
|
|
|
|
// Params defines the dependencies for creating a Config.
|
|
type Params struct {
|
|
fx.In
|
|
|
|
Globals *globals.Globals
|
|
Logger *logger.Logger
|
|
}
|
|
|
|
// Config holds all application configuration values.
|
|
type Config struct {
|
|
DBURL string
|
|
Debug bool
|
|
MaintenanceMode bool
|
|
MetricsPassword string
|
|
MetricsUsername string
|
|
Port int
|
|
SentryDSN string
|
|
MessageMaxAge string
|
|
MaxMessageSize int
|
|
QueueMaxAge string
|
|
MOTD string
|
|
ServerName string
|
|
FederationKey string
|
|
SessionIdleTimeout string
|
|
HashcashBits int
|
|
OperName string
|
|
OperPassword string
|
|
LoginRateLimit float64
|
|
LoginRateBurst int
|
|
IRCListenAddr string
|
|
params *Params
|
|
log *slog.Logger
|
|
}
|
|
|
|
// New creates a new Config by reading from files and environment variables.
|
|
// It returns an error when a number, true/false or duration setting does
|
|
// not parse.
|
|
func New(
|
|
_ fx.Lifecycle, params Params,
|
|
) (*Config, error) {
|
|
log := params.Logger.Get()
|
|
name := params.Globals.Appname
|
|
|
|
viper.SetConfigName(name)
|
|
viper.SetConfigType("yaml")
|
|
viper.AddConfigPath("/etc/" + name)
|
|
viper.AddConfigPath("$HOME/.config/" + name)
|
|
viper.AutomaticEnv()
|
|
|
|
viper.SetDefault("DEBUG", "false")
|
|
viper.SetDefault("MAINTENANCE_MODE", "false")
|
|
viper.SetDefault("PORT", "8080")
|
|
viper.SetDefault("DBURL", DefaultDBURL)
|
|
viper.SetDefault("SENTRY_DSN", "")
|
|
viper.SetDefault("METRICS_USERNAME", "")
|
|
viper.SetDefault("METRICS_PASSWORD", "")
|
|
viper.SetDefault("MESSAGE_MAX_AGE", "720h")
|
|
viper.SetDefault("MAX_MESSAGE_SIZE", "4096")
|
|
viper.SetDefault("QUEUE_MAX_AGE", "720h")
|
|
viper.SetDefault("MOTD", defaultMOTD)
|
|
viper.SetDefault("SERVER_NAME", "")
|
|
viper.SetDefault("FEDERATION_KEY", "")
|
|
viper.SetDefault("SESSION_IDLE_TIMEOUT", "720h")
|
|
viper.SetDefault("NEOIRC_HASHCASH_BITS", "20")
|
|
viper.SetDefault("NEOIRC_OPER_NAME", "")
|
|
viper.SetDefault("NEOIRC_OPER_PASSWORD", "")
|
|
viper.SetDefault("LOGIN_RATE_LIMIT", "1")
|
|
viper.SetDefault("LOGIN_RATE_BURST", "5")
|
|
viper.SetDefault("IRC_LISTEN_ADDR", ":6667")
|
|
|
|
err := viper.ReadInConfig()
|
|
if err != nil {
|
|
var notFound viper.ConfigFileNotFoundError
|
|
if !errors.As(err, ¬Found) {
|
|
log.Error("config file malformed", "error", err)
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
cfg := &Config{
|
|
DBURL: viper.GetString("DBURL"),
|
|
SentryDSN: viper.GetString("SENTRY_DSN"),
|
|
MetricsUsername: viper.GetString("METRICS_USERNAME"),
|
|
MetricsPassword: viper.GetString("METRICS_PASSWORD"),
|
|
MessageMaxAge: viper.GetString("MESSAGE_MAX_AGE"),
|
|
QueueMaxAge: viper.GetString("QUEUE_MAX_AGE"),
|
|
MOTD: viper.GetString("MOTD"),
|
|
ServerName: viper.GetString("SERVER_NAME"),
|
|
FederationKey: viper.GetString("FEDERATION_KEY"),
|
|
SessionIdleTimeout: viper.GetString("SESSION_IDLE_TIMEOUT"),
|
|
OperName: viper.GetString("NEOIRC_OPER_NAME"),
|
|
OperPassword: viper.GetString("NEOIRC_OPER_PASSWORD"),
|
|
IRCListenAddr: viper.GetString("IRC_LISTEN_ADDR"),
|
|
log: log,
|
|
params: ¶ms,
|
|
}
|
|
|
|
err = parseSettings(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Viper counts an empty environment variable as unset, but an
|
|
// empty IRC_LISTEN_ADDR must turn the IRC listener off.
|
|
ircListenAddr, set := os.LookupEnv("IRC_LISTEN_ADDR")
|
|
if set {
|
|
cfg.IRCListenAddr = ircListenAddr
|
|
}
|
|
|
|
if cfg.Debug {
|
|
params.Logger.EnableDebugLogging()
|
|
cfg.log = params.Logger.Get()
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// parseSettings reads the number and true/false settings into cfg and
|
|
// checks that each duration setting is a valid Go duration. Its error
|
|
// names each setting whose value does not parse.
|
|
func parseSettings(cfg *Config) error {
|
|
return errors.Join(
|
|
parseInt("PORT", &cfg.Port),
|
|
parseInt("MAX_MESSAGE_SIZE", &cfg.MaxMessageSize),
|
|
parseInt("NEOIRC_HASHCASH_BITS", &cfg.HashcashBits),
|
|
parseInt("LOGIN_RATE_BURST", &cfg.LoginRateBurst),
|
|
parseFloat("LOGIN_RATE_LIMIT", &cfg.LoginRateLimit),
|
|
parseBool("DEBUG", &cfg.Debug),
|
|
parseBool("MAINTENANCE_MODE", &cfg.MaintenanceMode),
|
|
checkDuration("MESSAGE_MAX_AGE", cfg.MessageMaxAge),
|
|
checkDuration("QUEUE_MAX_AGE", cfg.QueueMaxAge),
|
|
checkDuration("SESSION_IDLE_TIMEOUT", cfg.SessionIdleTimeout),
|
|
)
|
|
}
|
|
|
|
// parseInt sets *value to the setting key read as a whole number.
|
|
func parseInt(key string, value *int) error {
|
|
parsed, err := strconv.Atoi(viper.GetString(key))
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse %s: %w", key, err)
|
|
}
|
|
|
|
*value = parsed
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseFloat sets *value to the setting key read as a decimal number.
|
|
func parseFloat(key string, value *float64) error {
|
|
parsed, err := strconv.ParseFloat(viper.GetString(key), 64)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse %s: %w", key, err)
|
|
}
|
|
|
|
*value = parsed
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseBool sets *value to the setting key read as true or false.
|
|
func parseBool(key string, value *bool) error {
|
|
parsed, err := strconv.ParseBool(viper.GetString(key))
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse %s: %w", key, err)
|
|
}
|
|
|
|
*value = parsed
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkDuration returns an error naming the setting key when value is
|
|
// not a valid Go duration.
|
|
func checkDuration(key, value string) error {
|
|
_, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse %s: %w", key, err)
|
|
}
|
|
|
|
return nil
|
|
}
|