check / check (push) Waiting to run
script/fmt now runs golines -m 77 after gofmt, repeating golines until it changes nothing, since one pass can leave a line the next splits. script/fmt-check fails when either would change a file. script/bootstrap installs golines v0.13.0 with go install, pinned to its commit, and checks the installed version with go version -m. The tree is reformatted. The longer lines this makes are fixed by hand: nolint directives moved onto their own line above the statement, one string split, and 20 functions over funlen's 80 lines shortened or split, RunWAF and limitPercentages among them. Model: opus-5-5
263 lines
6.9 KiB
Go
263 lines
6.9 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_, every one of them has a default, and most can be
|
|
// given as a file instead. Those of `sww rproxy` start with SWRP_. Each
|
|
// of them but SWRP_WAF_BACKEND has a default, and none can be given as a file,
|
|
// since rproxy never touches disk. SWW_COMMAND chooses which of the two
|
|
// the image runs.
|
|
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// 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_LISTEN_ADDR and
|
|
// SWRP_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(lookupEnv func(string) (string, bool)) (string, error) {
|
|
command, _ := lookupEnv("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.
|
|
type environment struct {
|
|
lookupEnv func(string) (string, bool)
|
|
settings []slog.Attr
|
|
err error
|
|
}
|
|
|
|
// 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 := e.lookupEnv(name)
|
|
fileName := name + "_FILE"
|
|
|
|
path, inFile := e.lookupEnv(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
|
|
}
|