Files
smallwebwaf/internal/config/rproxy.go
sneak 3fa1f4b732
check / check (push) Successful in 3m47s
Read every setting through viper (closes #211)
servewaf's and rproxy's settings are read through viper, bound to the same environment variable names as before, 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, as it did. 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 tests pass set into a viper instead of a lookup function.

Model: deepseek-v4-pro
2026-11-07 05:29:14 +00:00

181 lines
5.8 KiB
Go

package config
import (
"errors"
"fmt"
"log/slog"
"math"
"net/netip"
"net/url"
"strconv"
"time"
"github.com/spf13/viper"
)
// RProxy is the settings of `smallwebwaf rproxy`. A size or time limit of zero
// is off.
type RProxy struct {
// ListenAddr is where rproxy listens (SWRP_LISTEN_ADDR).
ListenAddr string
// UpstreamURL is the app (SWRP_UPSTREAM_URL).
UpstreamURL *url.URL
// TrustedProxies are the netblocks whose X-Forwarded-For is believed
// (SWRP_TRUSTED_PROXIES).
TrustedProxies []netip.Prefix
// WAFBackend is servewaf's URL (SWRP_WAF_BACKEND).
WAFBackend *url.URL
// Timeout is how long rproxy waits for servewaf's answer to a question,
// and to a report (SWRP_TIMEOUT_MS). It cannot be off.
Timeout time.Duration
// DefaultDisposition is what is done with a request when servewaf cannot be
// connected to, does not answer at all, or does not answer within Timeout:
// allow or deny (SWRP_DEFAULT_DISPOSITION).
DefaultDisposition string
// MaxRequestSize is the largest request body (SWRP_MAX_REQUEST_SIZE), and
// MaxResponseSize the largest response body (SWRP_MAX_RESPONSE_SIZE).
MaxRequestSize int64
MaxResponseSize int64
// MaxRequestTime bounds sending the whole request: the visitor's request
// line and headers, and then, from their end, its body on its way to the
// app (SWRP_MAX_REQUEST_TIME_MS). MaxResponseTime bounds the answer, from
// the end of the request to its last byte reaching the visitor
// (SWRP_MAX_RESPONSE_TIME_MS).
MaxRequestTime time.Duration
MaxResponseTime time.Duration
// settings are the values read, as given or by default, for the log line
// at start.
settings []slog.Attr
}
var (
errNeedsWAFBackend = errors.New(
"is required: it is servewaf's URL, such as http://servewaf:8080")
errNotDisposition = errors.New("is not allow or deny")
errNotMilliseconds = errors.New(
"is not a whole number of milliseconds above zero, such as 2000")
errNotMillisecondsOrOff = errors.New(
"is not a whole number of milliseconds above zero, such as 60000, or off")
)
// RProxyFromEnvironment reads rproxy's settings through v, normally the viper
// NewViper returns, bound to the process's environment. A setting that is not
// set takes its default, and SWRP_WAF_BACKEND, which has none, must be set. A
// setting may instead be given as a file: the variable named by the setting's
// name with _FILE added names the file, which is read now. A setting that is
// set but invalid is an error that names it.
func RProxyFromEnvironment(v *viper.Viper) (*RProxy, error) {
env := &environment{v: v, readFiles: true}
cfg := &RProxy{
ListenAddr: env.address("SWRP_LISTEN_ADDR"),
UpstreamURL: env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL),
TrustedProxies: env.netblocks("SWRP_TRUSTED_PROXIES", privateRanges),
WAFBackend: env.wafBackend("SWRP_WAF_BACKEND"),
Timeout: env.milliseconds("SWRP_TIMEOUT_MS", "2000"),
DefaultDisposition: env.disposition("SWRP_DEFAULT_DISPOSITION", "allow"),
MaxRequestSize: env.size("SWRP_MAX_REQUEST_SIZE", "100M"),
MaxResponseSize: env.size("SWRP_MAX_RESPONSE_SIZE", "1G"),
MaxRequestTime: env.millisecondsOrOff("SWRP_MAX_REQUEST_TIME_MS", "60000"),
MaxResponseTime: env.millisecondsOrOff("SWRP_MAX_RESPONSE_TIME_MS", "1800000"),
}
if env.err != nil {
return nil, env.err
}
cfg.settings = env.settings
return cfg, nil
}
// RProxyListenAddrAndUpstreamURL reads only SWRP_LISTEN_ADDR and
// SWRP_UPSTREAM_URL. The health check in rproxy's container needs no other
// setting, so it reads no other.
func RProxyListenAddrAndUpstreamURL(
v *viper.Viper,
) (string, *url.URL, error) {
env := &environment{v: v, readFiles: true}
listenAddr := env.address("SWRP_LISTEN_ADDR")
upstreamURL := env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL)
if env.err != nil {
return "", nil, env.err
}
return listenAddr, upstreamURL, nil
}
// LogValue makes an RProxy log as each setting's name with the value it was
// given, or its default.
func (c *RProxy) LogValue() slog.Value {
return slog.GroupValue(c.settings...)
}
// wafBackend reads the setting that is servewaf's URL, which must be set.
func (e *environment) wafBackend(name string) *url.URL {
value := e.value(name, "")
if value == "" {
e.check(name, errNeedsWAFBackend)
return nil
}
backend, err := parseUpstreamURL(value)
e.check(name, err)
return backend
}
// disposition reads the setting that is what is done with a request when servewaf
// cannot be connected to, does not answer at all, or does not answer in time:
// allow or deny.
func (e *environment) disposition(name, defaultValue string) string {
value := e.value(name, defaultValue)
if value != "allow" && value != "deny" {
e.check(name, fmt.Errorf("%q %w", value, errNotDisposition))
}
return value
}
// milliseconds reads a setting that is a whole number of milliseconds above
// zero.
func (e *environment) milliseconds(name, defaultValue string) time.Duration {
value := e.value(name, defaultValue)
duration, valid := parseMilliseconds(value)
if !valid {
e.check(name, fmt.Errorf("%q %w", value, errNotMilliseconds))
}
return duration
}
// millisecondsOrOff reads a setting that is a whole number of milliseconds
// above zero, or off, which is 0.
func (e *environment) millisecondsOrOff(name, defaultValue string) time.Duration {
value := e.value(name, defaultValue)
if value == off {
return 0
}
duration, valid := parseMilliseconds(value)
if !valid {
e.check(name, fmt.Errorf("%q %w", value, errNotMillisecondsOrOff))
}
return duration
}
// parseMilliseconds reads a whole number of milliseconds above zero, and
// reports whether it is one.
func parseMilliseconds(value string) (time.Duration, bool) {
n, err := strconv.ParseInt(value, 10, 64)
if err != nil || n <= 0 || n > math.MaxInt64/int64(time.Millisecond) {
return 0, false
}
return time.Duration(n) * time.Millisecond, true
}