Author SHA1 Message Date
sneak f52b1f9ab5 Merge pull request 'next into main' (#205) from next into main
check / check (push) Successful in 3m37s
Reviewed-on: #205
2026-11-07 02:52:39 +01:00
16 changed files with 143 additions and 223 deletions
-10
View File
@@ -10,14 +10,12 @@ require (
github.com/maxmind/mmdbwriter v1.2.0 github.com/maxmind/mmdbwriter v1.2.0
github.com/oschwald/maxminddb-golang/v2 v2.7.0 github.com/oschwald/maxminddb-golang/v2 v2.7.0
github.com/prometheus/client_golang v1.24.1 github.com/prometheus/client_golang v1.24.1
github.com/spf13/viper v1.21.0
) )
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/corazawaf/libinjection-go v0.3.3 // indirect github.com/corazawaf/libinjection-go v0.3.3 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect
@@ -27,22 +25,14 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/magefile/mage v1.17.0 // indirect github.com/magefile/mage v1.17.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 // indirect github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/procfs v0.21.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/net v0.58.0 // indirect golang.org/x/net v0.58.0 // indirect
golang.org/x/sync v0.23.0 // indirect golang.org/x/sync v0.23.0 // indirect
-24
View File
@@ -12,12 +12,8 @@ github.com/corazawaf/libinjection-go v0.3.3 h1:NhbXKRfRpqKzBMzv8zpCcnjyEw7BCVhBO
github.com/corazawaf/libinjection-go v0.3.3/go.mod h1:Ik/+w3UmTWH9yn366RgS9D95K3y7Atb5m/H/gXzzPCk= github.com/corazawaf/libinjection-go v0.3.3/go.mod h1:Ik/+w3UmTWH9yn366RgS9D95K3y7Atb5m/H/gXzzPCk=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
@@ -38,10 +34,6 @@ github.com/kaptinlin/jsonschema v0.4.6 h1:vOSFg5tjmfkOdKg+D6Oo4fVOM/pActWu/ntkPs
github.com/kaptinlin/jsonschema v0.4.6/go.mod h1:1DUd7r5SdyB2ZnMtyB7uLv64dE3zTFTiYytDCd+AEL0= github.com/kaptinlin/jsonschema v0.4.6/go.mod h1:1DUd7r5SdyB2ZnMtyB7uLv64dE3zTFTiYytDCd+AEL0=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/magefile/mage v1.17.0 h1:dS4tkq997Ism03akafC8509iqDjeE7TNTexI25Y7sXM= github.com/magefile/mage v1.17.0 h1:dS4tkq997Ism03akafC8509iqDjeE7TNTexI25Y7sXM=
@@ -66,24 +58,8 @@ github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+18 -38
View File
@@ -26,8 +26,6 @@ import (
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/anomaly" "sneak.berlin/go/smallwebwaf/internal/anomaly"
"sneak.berlin/go/smallwebwaf/internal/remotelog" "sneak.berlin/go/smallwebwaf/internal/remotelog"
@@ -452,16 +450,15 @@ var (
"is in SWWAF_BLOCKLIST_URLS or is SWWAF_ASN_LIMIT_PERCENT_URL too") "is in SWWAF_BLOCKLIST_URLS or is SWWAF_ASN_LIMIT_PERCENT_URL too")
) )
// FromEnvironment reads the settings through v, normally the viper // FromEnvironment reads the settings with lookupEnv, normally
// NewViper returns, bound to the process's environment. A setting may // os.LookupEnv. A setting may instead be given as a file: the variable
// instead be given as a file: the variable named by the setting's name // named by the setting's name with _FILE added names the file, which is
// with _FILE added names the file, which is read now (see lookup). A // read now (see lookup). A setting that is not set takes its default. A
// setting that is not set takes its default. A setting that is set but // setting that is set but invalid is an error that names it.
// invalid is an error that names it.
// //
//nolint:funlen // one line for each setting, a list that grows with them //nolint:funlen // one line for each setting, a list that grows with them
func FromEnvironment(v *viper.Viper) (*Config, error) { func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
env := &environment{v: v, readFiles: true} env := &environment{lookupEnv: lookupEnv}
cfg := &Config{ cfg := &Config{
ListenAddr: env.address("SWWAF_LISTEN_ADDR"), ListenAddr: env.address("SWWAF_LISTEN_ADDR"),
InstanceName: env.instanceName(), InstanceName: env.instanceName(),
@@ -578,8 +575,8 @@ func FromEnvironment(v *viper.Viper) (*Config, error) {
// Command reads only SMALLWEBWAF_COMMAND, the command the image runs and its // Command reads only SMALLWEBWAF_COMMAND, the command the image runs and its
// health check checks: rproxy when it is unset or empty, or servewaf. It cannot // health check checks: rproxy when it is unset or empty, or servewaf. It cannot
// be given as a file. Any other value is an error that names the setting. // be given as a file. Any other value is an error that names the setting.
func Command(v *viper.Viper) (string, error) { func Command(lookupEnv func(string) (string, bool)) (string, error) {
command, _ := readSetting(v, "SMALLWEBWAF_COMMAND") command, _ := lookupEnv("SMALLWEBWAF_COMMAND")
switch command { switch command {
case "", "rproxy": case "", "rproxy":
@@ -594,8 +591,8 @@ func Command(v *viper.Viper) (string, error) {
// ServeWAFListenAddr reads only SWWAF_LISTEN_ADDR, which may be given as a // ServeWAFListenAddr reads only SWWAF_LISTEN_ADDR, which may be given as a
// file, as FromEnvironment does. The health check in servewaf's container needs // file, as FromEnvironment does. The health check in servewaf's container needs
// no other setting, so it reads no other, nor a file that another names. // no other setting, so it reads no other, nor a file that another names.
func ServeWAFListenAddr(v *viper.Viper) (string, error) { func ServeWAFListenAddr(lookupEnv func(string) (string, bool)) (string, error) {
env := &environment{v: v, readFiles: true} env := &environment{lookupEnv: lookupEnv}
listenAddr := env.address("SWWAF_LISTEN_ADDR") listenAddr := env.address("SWWAF_LISTEN_ADDR")
return listenAddr, env.err return listenAddr, env.err
@@ -605,8 +602,8 @@ func ServeWAFListenAddr(v *viper.Viper) (string, error) {
// file, as FromEnvironment does, so that the line saying a setting is // file, as FromEnvironment does, so that the line saying a setting is
// invalid carries it too. A file that cannot be read gives the default // invalid carries it too. A file that cannot be read gives the default
// here, and FromEnvironment then stops the start over it. // here, and FromEnvironment then stops the start over it.
func InstanceName(v *viper.Viper) string { func InstanceName(lookupEnv func(string) (string, bool)) string {
env := &environment{v: v, readFiles: true} env := &environment{lookupEnv: lookupEnv}
return env.instanceName() return env.instanceName()
} }
@@ -622,26 +619,13 @@ func (c *Config) LogValue() slog.Value {
} }
// environment is where FromEnvironment reads the settings: it notes each // environment is where FromEnvironment reads the settings: it notes each
// value for the log, and keeps the first error. It reads every setting // value for the log, and keeps the first error.
// through v, the viper NewViper returns, and, unless readFiles is false,
// honours each setting's _FILE form too.
type environment struct { type environment struct {
v *viper.Viper lookupEnv func(string) (string, bool)
readFiles bool
settings []slog.Attr settings []slog.Attr
err error 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, // value returns a setting's value, or its default when it is not set,
// and notes it for the log. // and notes it for the log.
func (e *environment) value(name, defaultValue string) string { func (e *environment) value(name, defaultValue string) string {
@@ -660,14 +644,10 @@ func (e *environment) value(name, defaultValue string) string {
// names, less one newline at their end. It notes that file's path for the // 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. // log. Both variables set, or a file that cannot be read, is an error.
func (e *environment) lookup(name string) (string, bool) { func (e *environment) lookup(name string) (string, bool) {
value, set := readSetting(e.v, name) value, set := e.lookupEnv(name)
if !e.readFiles {
return value, set
}
fileName := name + "_FILE" fileName := name + "_FILE"
path, inFile := readSetting(e.v, fileName) path, inFile := e.lookupEnv(fileName)
if !inFile { if !inFile {
return value, set return value, set
} }
@@ -1213,7 +1193,7 @@ func (e *environment) logRemoteURL(name string) *url.URL {
// certificates. Unset or empty, it is nil. Its value names a file // certificates. Unset or empty, it is nil. Its value names a file
// already, so, unlike the other settings, it has no _FILE form. // already, so, unlike the other settings, it has no _FILE form.
func (e *environment) certificates(name string) *x509.CertPool { func (e *environment) certificates(name string) *x509.CertPool {
path, _ := readSetting(e.v, name) path, _ := e.lookupEnv(name)
e.settings = append(e.settings, slog.String(name, path)) e.settings = append(e.settings, slog.String(name, path))
if path == "" { if path == "" {
+35 -41
View File
@@ -17,8 +17,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/anomaly" "sneak.berlin/go/smallwebwaf/internal/anomaly"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
) )
@@ -219,22 +217,18 @@ const (
// environment is a set of environment variables, for FromEnvironment. // environment is a set of environment variables, for FromEnvironment.
type environment map[string]string type environment map[string]string
// viper returns a viper Set with each of the variables, as the viper // lookupEnv reads one of the variables, as os.LookupEnv does.
// NewViper returns one bound to a process's environment. func (e environment) lookupEnv(name string) (string, bool) {
func (e environment) viper() *viper.Viper { value, ok := e[name]
v := viper.New()
for name, value := range e {
v.Set(name, value)
}
return v return value, ok
} }
// fromEnvironment reads the settings from env, which must be valid. // fromEnvironment reads the settings from env, which must be valid.
func fromEnvironment(t *testing.T, env environment) *config.Config { func fromEnvironment(t *testing.T, env environment) *config.Config {
t.Helper() t.Helper()
cfg, err := config.FromEnvironment(env.viper()) cfg, err := config.FromEnvironment(env.lookupEnv)
if err != nil { if err != nil {
t.Fatalf("settings %v: %v", env, err) t.Fatalf("settings %v: %v", env, err)
} }
@@ -461,7 +455,7 @@ func TestPathPrefixNotStartingWithSlashStopsTheStart(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment( _, err := config.FromEnvironment(
environment{rateLimitExemptPaths: "/favicon.ico,assets/"}.viper()) environment{rateLimitExemptPaths: "/favicon.ico,assets/"}.lookupEnv)
want := rateLimitExemptPaths + `: "assets/" is not a path prefix ` + want := rateLimitExemptPaths + `: "assets/" is not a path prefix ` +
`starting with /, such as /assets/` `starting with /, such as /assets/`
@@ -516,7 +510,7 @@ func TestInvalidTrapPathOrErrorBurstThresholdStopsTheStart(t *testing.T) {
t.Run(tc.name+"="+tc.value, func(t *testing.T) { t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper()) _, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
want := tc.name + ": " + tc.want want := tc.name + ": " + tc.want
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -642,7 +636,7 @@ func TestInvalidCoreRuleSetSettingStopsTheStart(t *testing.T) {
t.Run(tc.name+"="+tc.value, func(t *testing.T) { t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper()) _, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
want := tc.name + ": " + tc.want want := tc.name + ": " + tc.want
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -767,7 +761,7 @@ func TestInvalidRemoteLogSettingStopsTheStart(t *testing.T) {
{logRemoteAppName, ""}, {logRemoteAppName, "my app"}, {logRemoteAppName, ""}, {logRemoteAppName, "my app"},
{logRemoteAppName, "gitéa"}, {logRemoteAppName, strings.Repeat("a", 49)}, {logRemoteAppName, "gitéa"}, {logRemoteAppName, strings.Repeat("a", 49)},
} { } {
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper()) _, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
if err == nil || !strings.HasPrefix(err.Error(), tc.name+": ") { if err == nil || !strings.HasPrefix(err.Error(), tc.name+": ") {
t.Errorf("%s=%q: error %v, want one naming it", tc.name, tc.value, err) t.Errorf("%s=%q: error %v, want one naming it", tc.name, tc.value, err)
} }
@@ -784,7 +778,7 @@ func TestRemoteLogCAFileWithoutCertificateStopsTheStart(t *testing.T) {
t.Fatalf("write %s: %v", caFile, err) t.Fatalf("write %s: %v", caFile, err)
} }
_, err = config.FromEnvironment(environment{logRemoteTLSCAFile: caFile}.viper()) _, err = config.FromEnvironment(environment{logRemoteTLSCAFile: caFile}.lookupEnv)
want := logRemoteTLSCAFile + `: "` + caFile + `" holds no PEM certificate` want := logRemoteTLSCAFile + `: "` + caFile + `" holds no PEM certificate`
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -799,7 +793,7 @@ func TestInstanceNameNotAnAppNameStopsTheStartOnlyWhileSending(t *testing.T) {
sending := environment{logRemoteURL: remoteURL, instanceName: spaced} sending := environment{logRemoteURL: remoteURL, instanceName: spaced}
_, err := config.FromEnvironment(sending.viper()) _, err := config.FromEnvironment(sending.lookupEnv)
want := logRemoteAppName + `: is unset, and ` + instanceName + want := logRemoteAppName + `: is unset, and ` + instanceName +
` "fsn1 app1", its default, is not 1 to 48 printable ASCII characters ` + ` "fsn1 app1", its default, is not 1 to 48 printable ASCII characters ` +
@@ -829,7 +823,7 @@ func TestAppNameSetStopsTheStartWhileSending(t *testing.T) {
logRemoteURL: remoteURL, logRemoteURL: remoteURL,
instanceName: instance, instanceName: instance,
logRemoteAppName: "my app", logRemoteAppName: "my app",
}.viper()) }.lookupEnv)
want := logRemoteAppName + `: "my app" is not 1 to 48 printable ASCII ` + want := logRemoteAppName + `: "my app" is not 1 to 48 printable ASCII ` +
`characters without a space, such as gitea` `characters without a space, such as gitea`
@@ -960,7 +954,7 @@ func TestWebhookHeadersAreLoggedMaskedAndNeverShown(t *testing.T) {
// An item that is not a header is named by its place, not shown. // An item that is not a header is named by its place, not shown.
_, err := config.FromEnvironment(environment{ _, err := config.FromEnvironment(environment{
alertWebhookHeaders: "X-Team:ops," + secret, alertWebhookHeaders: "X-Team:ops," + secret,
}.viper()) }.lookupEnv)
want := alertWebhookHeaders + ": item 2 is not a header name followed by : " + want := alertWebhookHeaders + ": item 2 is not a header name followed by : " +
"and the header's value, such as Authorization:Bearer <token>" "and the header's value, such as Authorization:Bearer <token>"
@@ -997,7 +991,7 @@ func TestWebhookURLIsLoggedWithoutItsPathOrQueryAndNeverShown(t *testing.T) {
"ftp://hooks.example/services/" + secret, "ftp://hooks.example/services/" + secret,
"https://hooks.example/services/%zz" + secret, "https://hooks.example/services/%zz" + secret,
} { } {
_, err := config.FromEnvironment(environment{alertWebhookURL: value}.viper()) _, err := config.FromEnvironment(environment{alertWebhookURL: value}.lookupEnv)
want := alertWebhookURL + ": is not an http or https URL without a user or " + want := alertWebhookURL + ": is not an http or https URL without a user or " +
"a fragment, such as https://alerts.example/smallwebwaf" "a fragment, such as https://alerts.example/smallwebwaf"
@@ -1048,7 +1042,7 @@ func TestNtfyTokenWithAControlCharacterStopsTheStartWithoutShowingIt(t *testing.
"set": {alertNtfyToken: token + "\r"}, "set": {alertNtfyToken: token + "\r"},
"in a file": {alertNtfyToken + "_FILE": writeFile(t, token+"\r\n")}, "in a file": {alertNtfyToken + "_FILE": writeFile(t, token+"\r\n")},
} { } {
_, err := config.FromEnvironment(env.viper()) _, err := config.FromEnvironment(env.lookupEnv)
want := alertNtfyToken + ": holds a control character, such as the " + want := alertNtfyToken + ": holds a control character, such as the " +
"carriage return of a Windows line end" "carriage return of a Windows line end"
@@ -1065,7 +1059,7 @@ func TestInstanceNameWithAControlCharacterStopsTheStartOnlyWithNtfySet(t *testin
_, err := config.FromEnvironment(environment{ _, err := config.FromEnvironment(environment{
instanceName: name, alertNtfyURL: "https://ntfy.example/smallwebwaf-alerts", instanceName: name, alertNtfyURL: "https://ntfy.example/smallwebwaf-alerts",
}.viper()) }.lookupEnv)
want := instanceName + `: "fsn1app1\r" holds a control character, such as the ` + want := instanceName + `: "fsn1app1\r" holds a control character, such as the ` +
`carriage return of a Windows line end, and is sent to ntfy in a header ` + `carriage return of a Windows line end, and is sent to ntfy in a header ` +
@@ -1090,7 +1084,7 @@ func TestInstanceNameNotUTF8StopsTheStart(t *testing.T) {
"set": {instanceName: latin1}, "set": {instanceName: latin1},
"in a file": {instanceName + "_FILE": writeFile(t, latin1+"\n")}, "in a file": {instanceName + "_FILE": writeFile(t, latin1+"\n")},
} { } {
_, err := config.FromEnvironment(env.viper()) _, err := config.FromEnvironment(env.lookupEnv)
want := instanceName + `: "caf\xe9" is not valid UTF-8` want := instanceName + `: "caf\xe9" is not valid UTF-8`
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -1105,7 +1099,7 @@ func TestCodeOnBothCountryListsStopsTheStart(t *testing.T) {
_, err := config.FromEnvironment(environment{ _, err := config.FromEnvironment(environment{
deniedCountries: "cn,ru", deniedCountries: "cn,ru",
allowedCountries: "de,RU", allowedCountries: "de,RU",
}.viper()) }.lookupEnv)
if err == nil { if err == nil {
t.Fatal("ru on both country lists was accepted") t.Fatal("ru on both country lists was accepted")
} }
@@ -1167,7 +1161,7 @@ func TestLookupDBPathGoesWithTheFileSourceAlone(t *testing.T) {
lookupDBPath + ": is set while " + lookupSource + " is off; only file reads it", lookupDBPath + ": is set while " + lookupSource + " is off; only file reads it",
}, },
} { } {
_, err := config.FromEnvironment(tc.env.viper()) _, err := config.FromEnvironment(tc.env.lookupEnv)
if err == nil || err.Error() != tc.want { if err == nil || err.Error() != tc.want {
t.Errorf("settings %v: error %v, want %s", tc.env, err, tc.want) t.Errorf("settings %v: error %v, want %s", tc.env, err, tc.want)
} }
@@ -1197,7 +1191,7 @@ func TestSettingNeedingLookupsStopsTheStartWhileTheyAreOff(t *testing.T) {
_, err := config.FromEnvironment(environment{ _, err := config.FromEnvironment(environment{
lookupSource: off, name: value, lookupSource: off, name: value,
}.viper()) }.lookupEnv)
if err == nil || !strings.HasPrefix(err.Error(), name+": ") || if err == nil || !strings.HasPrefix(err.Error(), name+": ") ||
!strings.Contains(err.Error(), lookupSource+" is off") { !strings.Contains(err.Error(), lookupSource+" is off") {
t.Errorf("error %v, want one naming %s and %s", err, name, lookupSource) t.Errorf("error %v, want one naming %s and %s", err, name, lookupSource)
@@ -1347,7 +1341,7 @@ func TestInvalidAnomalySettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
t.Run(tc.name+"="+tc.value, func(t *testing.T) { t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper()) _, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
want := tc.name + ": " + tc.want want := tc.name + ": " + tc.want
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -1426,7 +1420,7 @@ func TestInvalidBiasedThresholdStopsTheStartSayingWhatIsWrong(t *testing.T) {
t.Run(tc.name+"="+tc.value, func(t *testing.T) { t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper()) _, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
want := tc.name + ": " + tc.want want := tc.name + ": " + tc.want
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -1523,7 +1517,7 @@ func TestInvalidReputationSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
t.Run(tc.name+"="+tc.value, func(t *testing.T) { t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper()) _, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
want := tc.name + ": " + tc.want want := tc.name + ": " + tc.want
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -1538,7 +1532,7 @@ func TestASNLimitPercentURLThatIsABlocklistStopsTheStart(t *testing.T) {
_, err := config.FromEnvironment(environment{ _, err := config.FromEnvironment(environment{
blocklistURLs: dropURL + "," + asnURL, asnLimitPercentURL: asnURL, blocklistURLs: dropURL + "," + asnURL, asnLimitPercentURL: asnURL,
}.viper()) }.lookupEnv)
want := asnLimitPercentURL + `: "` + asnURL + `" is in SWWAF_BLOCKLIST_URLS too` want := asnLimitPercentURL + `: "` + asnURL + `" is in SWWAF_BLOCKLIST_URLS too`
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -1665,7 +1659,7 @@ func TestInvalidDNSBLSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
t.Run(tc.name+"="+tc.value, func(t *testing.T) { t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper()) _, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
want := tc.name + ": " + tc.want want := tc.name + ": " + tc.want
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -1733,7 +1727,7 @@ func TestDNSBLZoneKeyIsLoggedMaskedAndNeverShown(t *testing.T) {
{spamhaus + "," + otherZone, `"` + spamhausMasked + `" is listed twice`}, {spamhaus + "," + otherZone, `"` + spamhausMasked + `" is listed twice`},
{spamhaus + ",,", "has an empty item in its list"}, {spamhaus + ",,", "has an empty item in its list"},
} { } {
_, err := config.FromEnvironment(environment{dnsblZones: tc.value}.viper()) _, err := config.FromEnvironment(environment{dnsblZones: tc.value}.lookupEnv)
want := dnsblZones + ": " + tc.want want := dnsblZones + ": " + tc.want
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -1786,7 +1780,7 @@ func TestInvalidAbuseIPDBSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
t.Run(tc.name+"="+tc.value, func(t *testing.T) { t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper()) _, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
want := tc.name + ": " + tc.want want := tc.name + ": " + tc.want
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -1900,7 +1894,7 @@ func TestInvalidCrowdSecSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment(tc.env.viper()) _, err := config.FromEnvironment(tc.env.lookupEnv)
if err == nil || err.Error() != tc.want { if err == nil || err.Error() != tc.want {
t.Errorf("error %v, want %s", err, tc.want) t.Errorf("error %v, want %s", err, tc.want)
} }
@@ -2060,7 +2054,7 @@ func wantStartStopped(t *testing.T, invalid []struct{ name, value string }) {
t.Run(tc.name+"="+tc.value, func(t *testing.T) { t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper()) _, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
if err == nil { if err == nil {
t.Fatalf("%s=%q was accepted", tc.name, tc.value) t.Fatalf("%s=%q was accepted", tc.name, tc.value)
} }
@@ -2085,7 +2079,7 @@ func TestHostOrTransferEncodingStopsTheStart(t *testing.T) {
t.Run(value, func(t *testing.T) { t.Run(value, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment(environment{logRequestHeaders: value}.viper()) _, err := config.FromEnvironment(environment{logRequestHeaders: value}.lookupEnv)
if err == nil || err.Error() != logRequestHeaders+": "+want { if err == nil || err.Error() != logRequestHeaders+": "+want {
t.Errorf("error %v, want %s: %s", err, logRequestHeaders, want) t.Errorf("error %v, want %s: %s", err, logRequestHeaders, want)
} }
@@ -2102,7 +2096,7 @@ func TestShortTokenStopsTheStartWithoutShowingIt(t *testing.T) {
t.Run(name+"="+value, func(t *testing.T) { t.Run(name+"="+value, func(t *testing.T) {
t.Parallel() t.Parallel()
_, err := config.FromEnvironment(environment{name: value}.viper()) _, err := config.FromEnvironment(environment{name: value}.lookupEnv)
want := name + ": is shorter than 32 characters" want := name + ": is shorter than 32 characters"
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -2158,7 +2152,7 @@ func TestSettingFromFileIsCheckedAsTheSettingItself(t *testing.T) {
_, err := config.FromEnvironment(environment{ _, err := config.FromEnvironment(environment{
bytesLimitPerMinute + "_FILE": writeFile(t, "lots\n"), bytesLimitPerMinute + "_FILE": writeFile(t, "lots\n"),
}.viper()) }.lookupEnv)
want := bytesLimitPerMinute + `: "lots" is not a size such as 512K, 100M or 5G, or off` want := bytesLimitPerMinute + `: "lots" is not a size such as 512K, 100M or 5G, or off`
if err == nil || err.Error() != want { if err == nil || err.Error() != want {
@@ -2169,7 +2163,7 @@ func TestSettingFromFileIsCheckedAsTheSettingItself(t *testing.T) {
logRemoteURL: remoteURL, logRemoteURL: remoteURL,
instanceName: instance, instanceName: instance,
logRemoteAppName + "_FILE": writeFile(t, "my app\n"), logRemoteAppName + "_FILE": writeFile(t, "my app\n"),
}.viper()) }.lookupEnv)
want = logRemoteAppName + `: "my app" is not 1 to 48 printable ASCII ` + want = logRemoteAppName + `: "my app" is not 1 to 48 printable ASCII ` +
`characters without a space, such as gitea` `characters without a space, such as gitea`
@@ -2184,7 +2178,7 @@ func TestSettingAndItsFileBothSetStopsTheStart(t *testing.T) {
_, err := config.FromEnvironment(environment{ _, err := config.FromEnvironment(environment{
metricsToken: token, metricsToken: token,
metricsToken + "_FILE": writeFile(t, token), metricsToken + "_FILE": writeFile(t, token),
}.viper()) }.lookupEnv)
want := metricsToken + ": is set, and so is " + metricsToken + want := metricsToken + ": is set, and so is " + metricsToken +
"_FILE; set only one of them" "_FILE; set only one of them"
@@ -2199,7 +2193,7 @@ func TestUnreadableSettingFileStopsTheStart(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
for _, path := range []string{filepath.Join(dir, "missing"), dir} { for _, path := range []string{filepath.Join(dir, "missing"), dir} {
_, err := config.FromEnvironment(environment{metricsToken + "_FILE": path}.viper()) _, err := config.FromEnvironment(environment{metricsToken + "_FILE": path}.lookupEnv)
want := metricsToken + "_FILE: cannot be read: " want := metricsToken + "_FILE: cannot be read: "
if err == nil || !strings.HasPrefix(err.Error(), want) { if err == nil || !strings.HasPrefix(err.Error(), want) {
+24 -11
View File
@@ -8,9 +8,8 @@ import (
"net/netip" "net/netip"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/spf13/viper"
) )
// RProxy is the settings of `smallwebwaf rproxy`. A size or time limit of zero // RProxy is the settings of `smallwebwaf rproxy`. A size or time limit of zero
@@ -59,13 +58,13 @@ var (
"is not a whole number of milliseconds above zero, such as 60000, or off") "is not a whole number of milliseconds above zero, such as 60000, or off")
) )
// RProxyFromEnvironment reads rproxy's settings through v, normally the viper // RProxyFromEnvironment reads rproxy's settings with lookupEnv, normally
// NewViper returns, bound to the process's environment. A setting that is not // os.LookupEnv. A setting that is not set takes its default, and
// set takes its default, and SWRP_WAF_BACKEND, which has none, must be set. A // SWRP_WAF_BACKEND, which has none, must be set. A setting that is set but
// setting that is set but invalid is an error that names it. Since rproxy // invalid is an error that names it. Since rproxy never touches disk, its
// never touches disk, its settings have no _FILE form. // settings have no _FILE form.
func RProxyFromEnvironment(v *viper.Viper) (*RProxy, error) { func RProxyFromEnvironment(lookupEnv func(string) (string, bool)) (*RProxy, error) {
env := &environment{v: v, readFiles: false} env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
cfg := &RProxy{ cfg := &RProxy{
ListenAddr: env.address("SWRP_LISTEN_ADDR"), ListenAddr: env.address("SWRP_LISTEN_ADDR"),
UpstreamURL: env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL), UpstreamURL: env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL),
@@ -92,9 +91,9 @@ func RProxyFromEnvironment(v *viper.Viper) (*RProxy, error) {
// SWRP_UPSTREAM_URL. The health check in rproxy's container needs no other // SWRP_UPSTREAM_URL. The health check in rproxy's container needs no other
// setting, so it reads no other. // setting, so it reads no other.
func RProxyListenAddrAndUpstreamURL( func RProxyListenAddrAndUpstreamURL(
v *viper.Viper, lookupEnv func(string) (string, bool),
) (string, *url.URL, error) { ) (string, *url.URL, error) {
env := &environment{v: v, readFiles: false} env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
listenAddr := env.address("SWRP_LISTEN_ADDR") listenAddr := env.address("SWRP_LISTEN_ADDR")
upstreamURL := env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL) upstreamURL := env.appURL("SWRP_UPSTREAM_URL", defaultUpstreamURL)
@@ -105,6 +104,20 @@ func RProxyListenAddrAndUpstreamURL(
return listenAddr, upstreamURL, nil return listenAddr, upstreamURL, nil
} }
// withoutFileForms returns lookupEnv finding no setting named with _FILE, since
// rproxy never touches disk.
func withoutFileForms(
lookupEnv func(string) (string, bool),
) func(string) (string, bool) {
return func(name string) (string, bool) {
if strings.HasSuffix(name, "_FILE") {
return "", false
}
return lookupEnv(name)
}
}
// LogValue makes an RProxy log as each setting's name with the value it was // LogValue makes an RProxy log as each setting's name with the value it was
// given, or its default. // given, or its default.
func (c *RProxy) LogValue() slog.Value { func (c *RProxy) LogValue() slog.Value {
+4 -7
View File
@@ -9,8 +9,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
) )
@@ -34,12 +32,11 @@ const (
// rproxyConfig reads rproxy's settings from env. // rproxyConfig reads rproxy's settings from env.
func rproxyConfig(env map[string]string) (*config.RProxy, error) { func rproxyConfig(env map[string]string) (*config.RProxy, error) {
v := viper.New() return config.RProxyFromEnvironment(func(name string) (string, bool) {
for name, value := range env { value, ok := env[name]
v.Set(name, value)
}
return config.RProxyFromEnvironment(v) return value, ok
})
} }
func TestRProxyDefaults(t *testing.T) { func TestRProxyDefaults(t *testing.T) {
-16
View File
@@ -1,16 +0,0 @@
package config
import "github.com/spf13/viper"
// NewViper returns the viper that reads the settings: each setting's
// environment variable, bound to its own name. Every setting of servewaf and
// rproxy is read through it, and none is read with os.Getenv or os.LookupEnv
// outside it. An environment variable that is set but empty is set, as one
// that holds a value is, which viper does not take for granted.
func NewViper() *viper.Viper {
v := viper.New()
v.AutomaticEnv()
v.AllowEmptyEnv(true)
return v
}
+7 -3
View File
@@ -33,9 +33,13 @@ func announcedLength(size int) string {
func TestServerHasTheFixedLimits(t *testing.T) { func TestServerHasTheFixedLimits(t *testing.T) {
t.Parallel() t.Parallel()
cfg, err := config.RProxyFromEnvironment(viperFrom(map[string]string{ cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
"SWRP_WAF_BACKEND": nowhere, if name == "SWRP_WAF_BACKEND" {
})) return nowhere, true
}
return "", false
})
if err != nil { if err != nil {
t.Fatalf("settings: %v", err) t.Fatalf("settings: %v", err)
} }
+5 -14
View File
@@ -18,8 +18,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
"sneak.berlin/go/smallwebwaf/internal/rproxy" "sneak.berlin/go/smallwebwaf/internal/rproxy"
@@ -343,17 +341,6 @@ func startServer(
return addr, out, server return addr, out, server
} }
// viperFrom returns a viper Set with each of settings, as the viper
// config.NewViper returns one bound to a process's environment.
func viperFrom(settings map[string]string) *viper.Viper {
v := viper.New()
for name, value := range settings {
v.Set(name, value)
}
return v
}
// startServerLogging is startServer with rproxy's request lines going to // startServerLogging is startServer with rproxy's request lines going to
// requestLog and its process lines to processLog. // requestLog and its process lines to processLog.
func startServerLogging( func startServerLogging(
@@ -367,7 +354,11 @@ func startServerLogging(
} }
maps.Copy(settings, env) maps.Copy(settings, env)
cfg, err := config.RProxyFromEnvironment(viperFrom(settings)) cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
value, ok := settings[name]
return value, ok
})
if err != nil { if err != nil {
t.Fatalf("settings %v: %v", settings, err) t.Fatalf("settings %v: %v", settings, err)
} }
+10 -15
View File
@@ -15,8 +15,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/lookup" "sneak.berlin/go/smallwebwaf/internal/lookup"
@@ -349,17 +347,6 @@ func serve(t *testing.T, server *servewaf.Server) {
// at serveWAFAddr, with the settings in env on top of the defaults, and returns // at serveWAFAddr, with the settings in env on top of the defaults, and returns
// where it listens. What it writes is dropped. SWRP_TIMEOUT_MS is a minute unless // where it listens. What it writes is dropped. SWRP_TIMEOUT_MS is a minute unless
// env sets it, so that on a busy host SWRP_DEFAULT_DISPOSITION does not decide. // env sets it, so that on a busy host SWRP_DEFAULT_DISPOSITION does not decide.
// viperFrom returns a viper Set with each of settings, as the viper
// config.NewViper returns one bound to a process's environment.
func viperFrom(settings map[string]string) *viper.Viper {
v := viper.New()
for name, value := range settings {
v.Set(name, value)
}
return v
}
func startRProxy( func startRProxy(
t *testing.T, serveWAFAddr, appURL string, env map[string]string, t *testing.T, serveWAFAddr, appURL string, env map[string]string,
) string { ) string {
@@ -372,7 +359,11 @@ func startRProxy(
} }
maps.Copy(settings, env) maps.Copy(settings, env)
cfg, err := config.RProxyFromEnvironment(viperFrom(settings)) cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
value, ok := settings[name]
return value, ok
})
if err != nil { if err != nil {
t.Fatalf("settings %v: %v", settings, err) t.Fatalf("settings %v: %v", settings, err)
} }
@@ -432,7 +423,11 @@ func newServeWAFWith(
maps.Copy(settings, env) maps.Copy(settings, env)
cfg, err := config.FromEnvironment(viperFrom(settings)) cfg, err := config.FromEnvironment(func(name string) (string, bool) {
value, ok := settings[name]
return value, ok
})
if err != nil { if err != nil {
t.Fatalf("settings %v: %v", settings, err) t.Fatalf("settings %v: %v", settings, err)
} }
+8 -10
View File
@@ -10,8 +10,6 @@ import (
"net/url" "net/url"
"time" "time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/servewaf" "sneak.berlin/go/smallwebwaf/internal/servewaf"
) )
@@ -33,7 +31,7 @@ var errHealthEndpoint = errors.New("the health endpoint answered")
// args are the arguments after `healthcheck`; it takes none, and given // args are the arguments after `healthcheck`; it takes none, and given
// one it names it on stderr and returns 1 without checking anything. // one it names it on stderr and returns 1 without checking anything.
func HealthCheck( func HealthCheck(
ctx context.Context, args []string, v *viper.Viper, ctx context.Context, args []string, lookupEnv func(string) (string, bool),
stderr io.Writer, stderr io.Writer,
) int { ) int {
if len(args) > 0 { if len(args) > 0 {
@@ -46,15 +44,15 @@ func HealthCheck(
ctx, cancel := context.WithTimeout(ctx, healthCheckTimeout) ctx, cancel := context.WithTimeout(ctx, healthCheckTimeout)
defer cancel() defer cancel()
command, err := config.Command(v) command, err := config.Command(lookupEnv)
switch { switch {
case err != nil: case err != nil:
err = fmt.Errorf("invalid setting: %w", err) err = fmt.Errorf("invalid setting: %w", err)
case command == "servewaf": case command == "servewaf":
err = checkServeWAF(ctx, v) err = checkServeWAF(ctx, lookupEnv)
default: default:
err = checkRProxyAndApp(ctx, v) err = checkRProxyAndApp(ctx, lookupEnv)
} }
if err != nil { if err != nil {
@@ -67,8 +65,8 @@ func HealthCheck(
} }
// checkServeWAF asks servewaf's health endpoint. // checkServeWAF asks servewaf's health endpoint.
func checkServeWAF(ctx context.Context, v *viper.Viper) error { func checkServeWAF(ctx context.Context, lookupEnv func(string) (string, bool)) error {
listenAddr, err := config.ServeWAFListenAddr(v) listenAddr, err := config.ServeWAFListenAddr(lookupEnv)
if err != nil { if err != nil {
return fmt.Errorf("invalid setting: %w", err) return fmt.Errorf("invalid setting: %w", err)
} }
@@ -83,9 +81,9 @@ func checkServeWAF(ctx context.Context, v *viper.Viper) error {
// checkRProxyAndApp asks rproxy's health endpoint, and connects to the app. // checkRProxyAndApp asks rproxy's health endpoint, and connects to the app.
func checkRProxyAndApp( func checkRProxyAndApp(
ctx context.Context, v *viper.Viper, ctx context.Context, lookupEnv func(string) (string, bool),
) error { ) error {
listenAddr, upstreamURL, err := config.RProxyListenAddrAndUpstreamURL(v) listenAddr, upstreamURL, err := config.RProxyListenAddrAndUpstreamURL(lookupEnv)
if err != nil { if err != nil {
return fmt.Errorf("invalid setting: %w", err) return fmt.Errorf("invalid setting: %w", err)
} }
+10 -2
View File
@@ -145,7 +145,11 @@ func TestHealthCheckRefusesAnArgument(t *testing.T) {
var stderr bytes.Buffer var stderr bytes.Buffer
got := smallwebwaf.HealthCheck(t.Context(), []string{"now"}, viperFrom(nil), &stderr) noSettings := func(string) (string, bool) {
return "", false
}
got := smallwebwaf.HealthCheck(t.Context(), []string{"now"}, noSettings, &stderr)
want := "smallwebwaf healthcheck: unexpected argument \"now\"\n" want := "smallwebwaf healthcheck: unexpected argument \"now\"\n"
if got != 1 || stderr.String() != want { if got != 1 || stderr.String() != want {
@@ -162,7 +166,11 @@ func wantHealthCheck(t *testing.T, env map[string]string, status int, message st
var stderr bytes.Buffer var stderr bytes.Buffer
got := smallwebwaf.HealthCheck(t.Context(), nil, viperFrom(env), &stderr) got := smallwebwaf.HealthCheck(t.Context(), nil, func(name string) (string, bool) {
value, ok := env[name]
return value, ok
}, &stderr)
wrote := stderr.String() wrote := stderr.String()
if got != status || !strings.HasPrefix(wrote, message) || if got != status || !strings.HasPrefix(wrote, message) ||
+1 -1
View File
@@ -23,7 +23,7 @@ func RunRProxy(ctx context.Context, params Params) int {
hostname, _ := os.Hostname() // "" when the host has no name to give hostname, _ := os.Hostname() // "" when the host has no name to give
processLog := requestlog.NewProcessLogger(params.Stdout, hostname, slog.LevelInfo) processLog := requestlog.NewProcessLogger(params.Stdout, hostname, slog.LevelInfo)
cfg, err := config.RProxyFromEnvironment(params.Viper) cfg, err := config.RProxyFromEnvironment(params.LookupEnv)
if err != nil { if err != nil {
processLog.Error("invalid setting", "error", err.Error()) processLog.Error("invalid setting", "error", err.Error())
+5 -1
View File
@@ -43,7 +43,11 @@ const (
func runRProxy(ctx context.Context, env map[string]string, out *output) int { func runRProxy(ctx context.Context, env map[string]string, out *output) int {
return smallwebwaf.RunRProxy(ctx, smallwebwaf.Params{ return smallwebwaf.RunRProxy(ctx, smallwebwaf.Params{
Version: testVersion, Version: testVersion,
Viper: viperFrom(env), LookupEnv: func(name string) (string, bool) {
value, ok := env[name]
return value, ok
},
Stdout: out, Stdout: out,
}) })
} }
+6 -9
View File
@@ -20,8 +20,6 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/alerts" "sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/config" "sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/lookup" "sneak.berlin/go/smallwebwaf/internal/lookup"
@@ -60,9 +58,8 @@ commands:
type Params struct { type Params struct {
// Version is the version of the binary, set when it is built. // Version is the version of the binary, set when it is built.
Version string Version string
// Viper reads the settings, normally the viper config.NewViper returns // LookupEnv reads an environment variable, normally os.LookupEnv.
// for the process's environment. LookupEnv func(string) (string, bool)
Viper *viper.Viper
// Stdout receives the request log and the process's own messages. // Stdout receives the request log and the process's own messages.
Stdout io.Writer Stdout io.Writer
} }
@@ -81,7 +78,7 @@ func Main(version string) int {
syscall.SIGTERM, os.Interrupt) syscall.SIGTERM, os.Interrupt)
defer stop() defer stop()
params := Params{Version: version, Viper: config.NewViper(), Stdout: os.Stdout} params := Params{Version: version, LookupEnv: os.LookupEnv, Stdout: os.Stdout}
switch args[0] { switch args[0] {
case "rproxy": case "rproxy":
@@ -89,7 +86,7 @@ func Main(version string) int {
case "servewaf": case "servewaf":
return RunServeWAF(ctx, params) return RunServeWAF(ctx, params)
case "healthcheck": case "healthcheck":
return HealthCheck(ctx, args[1:], params.Viper, os.Stderr) return HealthCheck(ctx, args[1:], os.LookupEnv, os.Stderr)
default: default:
return printUsage(os.Stderr) return printUsage(os.Stderr)
} }
@@ -109,9 +106,9 @@ func RunServeWAF(ctx context.Context, params Params) int {
// Until the settings are read, the one message is an invalid setting's // Until the settings are read, the one message is an invalid setting's
// error, which every SWWAF_LOG_LEVEL lets through. // error, which every SWWAF_LOG_LEVEL lets through.
processLog := requestlog.NewProcessLogger(params.Stdout, processLog := requestlog.NewProcessLogger(params.Stdout,
config.InstanceName(params.Viper), slog.LevelError) config.InstanceName(params.LookupEnv), slog.LevelError)
cfg, err := config.FromEnvironment(params.Viper) cfg, err := config.FromEnvironment(params.LookupEnv)
if err != nil { if err != nil {
processLog.Error("invalid setting", "error", err.Error()) processLog.Error("invalid setting", "error", err.Error())
+8 -19
View File
@@ -19,8 +19,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest" "sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
"sneak.berlin/go/smallwebwaf/internal/servewaf" "sneak.berlin/go/smallwebwaf/internal/servewaf"
"sneak.berlin/go/smallwebwaf/internal/smallwebwaf" "sneak.berlin/go/smallwebwaf/internal/smallwebwaf"
@@ -124,29 +122,20 @@ func (o *output) text() string {
return o.buf.String() return o.buf.String()
} }
// viperFrom returns a viper Set with each of settings, as the viper
// config.NewViper returns one bound to a process's environment.
func viperFrom(settings map[string]string) *viper.Viper {
v := viper.New()
for name, value := range settings {
v.Set(name, value)
}
return v
}
// run runs `smallwebwaf servewaf` with the settings in env until ctx is done, // run runs `smallwebwaf servewaf` with the settings in env until ctx is done,
// and returns its exit status. SWWAF_LOOKUP_SOURCE is off unless env sets it, // and returns its exit status. SWWAF_LOOKUP_SOURCE is off unless env sets it,
// so that no test sends GeoJS its clients' addresses. // so that no test sends GeoJS its clients' addresses.
func run(ctx context.Context, env map[string]string, out *output) int { func run(ctx context.Context, env map[string]string, out *output) int {
v := viperFrom(env)
if _, set := env["SWWAF_LOOKUP_SOURCE"]; !set {
v.Set("SWWAF_LOOKUP_SOURCE", "off")
}
return smallwebwaf.RunServeWAF(ctx, smallwebwaf.Params{ return smallwebwaf.RunServeWAF(ctx, smallwebwaf.Params{
Version: testVersion, Version: testVersion,
Viper: v, LookupEnv: func(name string) (string, bool) {
value, ok := env[name]
if !ok && name == "SWWAF_LOOKUP_SOURCE" {
return "off", true
}
return value, ok
},
Stdout: out, Stdout: out,
}) })
} }