check / check (push) Failing after 1m55s
Every setting of waf and rproxy is read through viper, in place of the os.LookupEnv function Main passed down. NewViper turns AutomaticEnv and AllowEmptyEnv on, so a setting that is set but empty reads as set. A setting may also be given as a file, the _FILE form, which rproxy's settings now have too; rproxy still writes nothing to disk. The settings are renamed: waf's take the prefix SWW_WAF_, rproxy's take SWW_RPROXY_, and the shared SWW_COMMAND keeps SWW_. The Core Rule Set's settings take SWW_WAF_CORE_RULE_SET_ rather than SWW_WAF_WAF_. Model: deepseek-v4-pro (first version); opus-5-5 (rework)
276 lines
7.2 KiB
Go
276 lines
7.2 KiB
Go
// Package config reads sww's settings, and is the one place they are
|
|
// read. Every setting is an environment variable. The settings of `sww
|
|
// waf` start with SWW_WAF_, every one of them has a default, and most can be
|
|
// given as a file instead. Those of `sww rproxy` start with SWW_RPROXY_. Each
|
|
// of them but SWW_RPROXY_WAF_BACKEND has a default, and each can be given as a file
|
|
// too. SWW_COMMAND chooses which of the two the image runs.
|
|
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// off is the value that switches a timeout, a size limit or a rate limit
|
|
// off.
|
|
const off = "off"
|
|
|
|
const (
|
|
kibibyte = 1 << 10
|
|
mebibyte = 1 << 20
|
|
gibibyte = 1 << 30
|
|
// defaultListenAddr is the default of SWW_WAF_LISTEN_ADDR and
|
|
// SWW_RPROXY_LISTEN_ADDR.
|
|
defaultListenAddr = ":8080"
|
|
)
|
|
|
|
var (
|
|
errNotSize = errors.New(
|
|
"is not a size such as 512K, 100M or 5G, or off")
|
|
errNotPositive = errors.New("must be more than zero, or off")
|
|
errEmptyItem = errors.New("has an empty item in its list")
|
|
errNotNetblock = errors.New(
|
|
"is not a netblock such as 10.0.0.0/8, or an address")
|
|
errNotListenAddr = errors.New(
|
|
"is not an address to listen on, such as :8080")
|
|
errNotCommand = errors.New("is not rproxy or waf")
|
|
errSetTwice = errors.New("set only one of them")
|
|
)
|
|
|
|
// Command reads only SWW_COMMAND, the command the image runs and its
|
|
// health check checks: rproxy when it is unset or empty, or waf. It cannot
|
|
// be given as a file. Any other value is an error that names the setting.
|
|
func Command(v *viper.Viper) (string, error) {
|
|
command, _ := readSetting(v, "SWW_COMMAND")
|
|
|
|
switch command {
|
|
case "", "rproxy":
|
|
return "rproxy", nil
|
|
case "waf":
|
|
return command, nil
|
|
default:
|
|
return "", fmt.Errorf("SWW_COMMAND: %q %w", command, errNotCommand)
|
|
}
|
|
}
|
|
|
|
// environment is where FromEnvironment reads the settings: it notes each
|
|
// value for the log, and keeps the first error. It reads every setting
|
|
// through v, the viper NewViper returns, and honours each setting's _FILE
|
|
// form too.
|
|
type environment struct {
|
|
v *viper.Viper
|
|
settings []slog.Attr
|
|
err error
|
|
}
|
|
|
|
// readSetting returns a setting's value and whether it is set, reading it
|
|
// through v.
|
|
func readSetting(v *viper.Viper, name string) (string, bool) {
|
|
if !v.IsSet(name) {
|
|
return "", false
|
|
}
|
|
|
|
return v.GetString(name), true
|
|
}
|
|
|
|
// value returns a setting's value, or its default when it is not set,
|
|
// and notes it for the log.
|
|
func (e *environment) value(name, defaultValue string) string {
|
|
value, set := e.lookup(name)
|
|
if !set {
|
|
value = defaultValue
|
|
}
|
|
|
|
e.settings = append(e.settings, slog.String(name, value))
|
|
|
|
return value
|
|
}
|
|
|
|
// lookup returns a setting's value and whether it is set: the value of the
|
|
// variable name, or the contents of the file that the variable name_FILE
|
|
// names, less one newline at their end. It notes that file's path for the
|
|
// log. Both variables set, or a file that cannot be read, is an error.
|
|
func (e *environment) lookup(name string) (string, bool) {
|
|
value, set := readSetting(e.v, name)
|
|
fileName := name + "_FILE"
|
|
|
|
path, inFile := readSetting(e.v, fileName)
|
|
if !inFile {
|
|
return value, set
|
|
}
|
|
|
|
if set {
|
|
e.check(
|
|
name,
|
|
fmt.Errorf("is set, and so is %s; %w", fileName, errSetTwice),
|
|
)
|
|
|
|
return value, set
|
|
}
|
|
|
|
e.settings = append(e.settings, slog.String(fileName, path))
|
|
|
|
//nolint:gosec // a file the admin names
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
e.check(fileName, fmt.Errorf("cannot be read: %w", err))
|
|
|
|
return "", false
|
|
}
|
|
|
|
return strings.TrimSuffix(string(contents), "\n"), true
|
|
}
|
|
|
|
// check keeps the first error, naming the setting it is about.
|
|
func (e *environment) check(name string, err error) {
|
|
if err != nil && e.err == nil {
|
|
e.err = fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
}
|
|
|
|
// address reads a setting that is an address to listen on, by default
|
|
// defaultListenAddr.
|
|
func (e *environment) address(name string) string {
|
|
address, err := parseListenAddr(e.value(name, defaultListenAddr))
|
|
e.check(name, err)
|
|
|
|
return address
|
|
}
|
|
|
|
// netblocks reads a setting that is a list of netblocks.
|
|
func (e *environment) netblocks(name, defaultValue string) []netip.Prefix {
|
|
netblocks, err := parseNetblocks(e.value(name, defaultValue))
|
|
e.check(name, err)
|
|
|
|
return netblocks
|
|
}
|
|
|
|
// size reads a setting that is a number of bytes.
|
|
func (e *environment) size(name, defaultValue string) int64 {
|
|
size, err := parseSize(e.value(name, defaultValue))
|
|
e.check(name, err)
|
|
|
|
return size
|
|
}
|
|
|
|
// parseSize reads a number of bytes with an optional K, M or G suffix, in
|
|
// powers of 1024 (1K is 1024 bytes), or off.
|
|
func parseSize(value string) (int64, error) {
|
|
if value == off {
|
|
return 0, nil
|
|
}
|
|
|
|
number, unit := splitUnit(value)
|
|
|
|
n, err := strconv.ParseInt(number, 10, 64)
|
|
if err != nil || n > math.MaxInt64/unit {
|
|
return 0, fmt.Errorf("%q %w", value, errNotSize)
|
|
}
|
|
|
|
if n <= 0 {
|
|
return 0, fmt.Errorf("%q %w", value, errNotPositive)
|
|
}
|
|
|
|
return n * unit, nil
|
|
}
|
|
|
|
// splitUnit splits a size into its number and the bytes its suffix
|
|
// stands for.
|
|
func splitUnit(value string) (string, int64) {
|
|
switch {
|
|
case strings.HasSuffix(value, "K"):
|
|
return strings.TrimSuffix(value, "K"), kibibyte
|
|
case strings.HasSuffix(value, "M"):
|
|
return strings.TrimSuffix(value, "M"), mebibyte
|
|
case strings.HasSuffix(value, "G"):
|
|
return strings.TrimSuffix(value, "G"), gibibyte
|
|
default:
|
|
return value, 1
|
|
}
|
|
}
|
|
|
|
// parseList splits a comma-separated list and trims the spaces around
|
|
// each item. An empty value is an empty list.
|
|
func parseList(value string) ([]string, error) {
|
|
if strings.TrimSpace(value) == "" {
|
|
return []string{}, nil
|
|
}
|
|
|
|
items := strings.Split(value, ",")
|
|
for i, item := range items {
|
|
items[i] = strings.TrimSpace(item)
|
|
if items[i] == "" {
|
|
return nil, fmt.Errorf("%q %w", value, errEmptyItem)
|
|
}
|
|
}
|
|
|
|
return items, nil
|
|
}
|
|
|
|
// parseNetblocks reads a comma-separated list of netblocks.
|
|
func parseNetblocks(value string) ([]netip.Prefix, error) {
|
|
items, err := parseList(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
netblocks := make([]netip.Prefix, 0, len(items))
|
|
|
|
for _, item := range items {
|
|
netblock, err := ParseNetblock(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
netblocks = append(netblocks, netblock)
|
|
}
|
|
|
|
return netblocks, nil
|
|
}
|
|
|
|
// ParseNetblock reads a netblock in CIDR form, such as 10.0.0.0/8. A bare
|
|
// address is a netblock of that address alone, a /32 or a /128. A
|
|
// blocklist's lines are read with it too.
|
|
func ParseNetblock(value string) (netip.Prefix, error) {
|
|
if strings.Contains(value, "/") {
|
|
netblock, err := netip.ParsePrefix(value)
|
|
if err != nil {
|
|
return netip.Prefix{}, fmt.Errorf("%q %w", value, errNotNetblock)
|
|
}
|
|
|
|
return netblock.Masked(), nil
|
|
}
|
|
|
|
addr, err := netip.ParseAddr(value)
|
|
if err != nil || addr.Zone() != "" {
|
|
return netip.Prefix{}, fmt.Errorf("%q %w", value, errNotNetblock)
|
|
}
|
|
|
|
return netip.PrefixFrom(addr, addr.BitLen()), nil
|
|
}
|
|
|
|
// parseListenAddr checks an address to listen on: an optional host and a
|
|
// port number.
|
|
func parseListenAddr(value string) (string, error) {
|
|
_, port, err := net.SplitHostPort(value)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%q %w", value, errNotListenAddr)
|
|
}
|
|
|
|
_, err = strconv.ParseUint(port, 10, 16)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%q %w", value, errNotListenAddr)
|
|
}
|
|
|
|
return value, nil
|
|
}
|