// 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 }