Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c755ee358 |
@@ -1,5 +1,5 @@
|
|||||||
.PHONY: bootstrap setup test lint fmt fmt-check tidy check docker publish \
|
.PHONY: bootstrap setup test lint fmt fmt-check tidy check docker hooks build \
|
||||||
hooks build run example-app gitea-requests gitea-setup
|
run example-app gitea-requests gitea-setup
|
||||||
|
|
||||||
# Makefile targets are thin shims; the implementations live in script/
|
# Makefile targets are thin shims; the implementations live in script/
|
||||||
# per the scripts-to-rule-them-all pattern (see the Entrypoints section
|
# per the scripts-to-rule-them-all pattern (see the Entrypoints section
|
||||||
@@ -7,8 +7,7 @@
|
|||||||
# which test checks. build and run are for working on the code by hand;
|
# which test checks. build and run are for working on the code by hand;
|
||||||
# example-app checks the image with an app built on it; gitea-requests
|
# example-app checks the image with an app built on it; gitea-requests
|
||||||
# reports the gitea requests the Core Rule Set refuses at the defaults;
|
# reports the gitea requests the Core Rule Set refuses at the defaults;
|
||||||
# gitea-setup checks the setup in deploy/gitea; publish builds the image
|
# gitea-setup checks the setup in deploy/gitea.
|
||||||
# on the Docker host and pushes it to the registry.
|
|
||||||
|
|
||||||
bootstrap:
|
bootstrap:
|
||||||
@script/bootstrap
|
@script/bootstrap
|
||||||
@@ -37,9 +36,6 @@ check:
|
|||||||
docker:
|
docker:
|
||||||
@script/docker
|
@script/docker
|
||||||
|
|
||||||
publish:
|
|
||||||
@script/publish
|
|
||||||
|
|
||||||
hooks:
|
hooks:
|
||||||
@script/install-precommit
|
@script/install-precommit
|
||||||
|
|
||||||
|
|||||||
@@ -743,11 +743,6 @@ on the host.
|
|||||||
`make check`.
|
`make check`.
|
||||||
- `script/docker`: builds the image, whose build runs the tests and the linter
|
- `script/docker`: builds the image, whose build runs the tests and the linter
|
||||||
first. `make docker`.
|
first. `make docker`.
|
||||||
- `script/publish`: runs `script/docker` on the Docker host `DOCKER_HOST`
|
|
||||||
(`ssh://root@fsn1app1` unless set) and pushes the image to `PUBLISH_IMAGE`
|
|
||||||
(`git.eeqj.de/sneak/smallwebwaf` unless set) under four tags: the UTC date as
|
|
||||||
`YYYY-MM-DD`, the short commit hash, the branch name and `latest`. Refuses a
|
|
||||||
working tree with uncommitted changes. `make publish`.
|
|
||||||
- `script/cibuild`: what CI runs: `script/bootstrap`, `script/check`, then the
|
- `script/cibuild`: what CI runs: `script/bootstrap`, `script/check`, then the
|
||||||
image build.
|
image build.
|
||||||
- `script/precommit`: run by the git pre-commit hook; runs `script/check`.
|
- `script/precommit`: run by the git pre-commit hook; runs `script/check`.
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ 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
|
||||||
@@ -25,14 +27,22 @@ 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
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ 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=
|
||||||
@@ -34,6 +38,10 @@ 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=
|
||||||
@@ -58,8 +66,24 @@ 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=
|
||||||
|
|||||||
+125
-314
@@ -7,9 +7,6 @@
|
|||||||
// the image runs.
|
// the image runs.
|
||||||
package config
|
package config
|
||||||
|
|
||||||
// FIXME: the rproxy config is in rproxy.go, the servewaf is in config.go -
|
|
||||||
// wrong and inconsistent.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -29,6 +26,8 @@ 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"
|
||||||
@@ -366,14 +365,11 @@ var (
|
|||||||
"is not a header name such as accept-language")
|
"is not a header name such as accept-language")
|
||||||
errHeaderTakenOut = errors.New(
|
errHeaderTakenOut = errors.New(
|
||||||
"is taken out of every request by Go's HTTP server, so it can never " +
|
"is taken out of every request by Go's HTTP server, so it can never " +
|
||||||
"be logged",
|
"be logged")
|
||||||
)
|
|
||||||
errOnBothLists = errors.New("is in SWWAF_DENIED_COUNTRIES too")
|
errOnBothLists = errors.New("is in SWWAF_DENIED_COUNTRIES too")
|
||||||
errNotLookupSource = errors.New("is not geojs, file or off")
|
errNotLookupSource = errors.New("is not geojs, file or off")
|
||||||
errNeedsLookups = errors.New("it needs each client looked up")
|
errNeedsLookups = errors.New("it needs each client looked up")
|
||||||
errNeedsDBPath = errors.New(
|
errNeedsDBPath = errors.New("it names the file to look clients up in")
|
||||||
"it names the file to look clients up in",
|
|
||||||
)
|
|
||||||
errDBPathUnused = errors.New("only file reads it")
|
errDBPathUnused = errors.New("only file reads it")
|
||||||
errOver64K = errors.New(
|
errOver64K = errors.New(
|
||||||
"is more than 64K, the most servewaf gets of a body")
|
"is more than 64K, the most servewaf gets of a body")
|
||||||
@@ -400,29 +396,22 @@ var (
|
|||||||
errNotPathPrefix = errors.New(
|
errNotPathPrefix = errors.New(
|
||||||
"is not a path prefix starting with /, such as /assets/")
|
"is not a path prefix starting with /, such as /assets/")
|
||||||
errNotTrapPath = errors.New(
|
errNotTrapPath = errors.New(
|
||||||
"is not a path starting with / and without a ?, such as /wp-login.php",
|
"is not a path starting with / and without a ?, such as /wp-login.php")
|
||||||
)
|
|
||||||
errNotWAFMode = errors.New("is not off, detect or block")
|
errNotWAFMode = errors.New("is not off, detect or block")
|
||||||
errNotParanoiaLevel = errors.New("is not a paranoia level, from 1 to 4")
|
errNotParanoiaLevel = errors.New("is not a paranoia level, from 1 to 4")
|
||||||
errNotRuleID = errors.New(
|
errNotRuleID = errors.New(
|
||||||
"is not the id of a Core Rule Set rule, a whole number such as 942100",
|
"is not the id of a Core Rule Set rule, a whole number such as 942100")
|
||||||
)
|
|
||||||
errSetupRuleID = errors.New(
|
errSetupRuleID = errors.New(
|
||||||
"is from 900000 to 900999, the ids of the rules that set the Core Rule Set " +
|
"is from 900000 to 900999, the ids of the rules that set the Core Rule Set " +
|
||||||
"up and of smallwebwaf's own, which cannot be switched off",
|
"up and of smallwebwaf's own, which cannot be switched off")
|
||||||
)
|
|
||||||
errNotBoolean = errors.New("is not true or false")
|
errNotBoolean = errors.New("is not true or false")
|
||||||
errNotLogRemoteURL = errors.New(
|
errNotLogRemoteURL = errors.New(
|
||||||
"is not syslog+udp, syslog+tcp or syslog+tls with a host and a port, " +
|
"is not syslog+udp, syslog+tcp or syslog+tls with a host and a port, " +
|
||||||
"and nothing more, such as syslog+tls://logs.example:6514",
|
"and nothing more, such as syslog+tls://logs.example:6514")
|
||||||
)
|
|
||||||
errNoCertificate = errors.New("holds no PEM certificate")
|
errNoCertificate = errors.New("holds no PEM certificate")
|
||||||
errNotFacility = errors.New(
|
errNotFacility = errors.New("is not a syslog facility such as local0 or daemon")
|
||||||
"is not a syslog facility such as local0 or daemon",
|
|
||||||
)
|
|
||||||
errNotAppName = errors.New(
|
errNotAppName = errors.New(
|
||||||
"is not 1 to 48 printable ASCII characters without a space, such as gitea",
|
"is not 1 to 48 printable ASCII characters without a space, such as gitea")
|
||||||
)
|
|
||||||
errSetTwice = errors.New("set only one of them")
|
errSetTwice = errors.New("set only one of them")
|
||||||
errNotWebhookURL = errors.New(
|
errNotWebhookURL = errors.New(
|
||||||
"is not an http or https URL without a user or a fragment, " +
|
"is not an http or https URL without a user or a fragment, " +
|
||||||
@@ -431,135 +420,77 @@ var (
|
|||||||
"is not a header name followed by : and the header's value, " +
|
"is not a header name followed by : and the header's value, " +
|
||||||
"such as Authorization:Bearer <token>")
|
"such as Authorization:Bearer <token>")
|
||||||
errControlCharacter = errors.New(
|
errControlCharacter = errors.New(
|
||||||
"holds a control character, such as the carriage return of a Windows line end",
|
"holds a control character, such as the carriage return of a Windows line end")
|
||||||
)
|
|
||||||
errNotAlertEvent = errors.New(
|
errNotAlertEvent = errors.New(
|
||||||
"is not ban, permanent_ban, waf_block, anomaly, reputation_hit, " +
|
"is not ban, permanent_ban, waf_block, anomaly, reputation_hit, " +
|
||||||
"source_failure or file_error")
|
"source_failure or file_error")
|
||||||
errNotNumberOrOff = errors.New(
|
errNotNumberOrOff = errors.New("is not a whole number above zero, such as 60, or off")
|
||||||
"is not a whole number above zero, such as 60, or off",
|
|
||||||
)
|
|
||||||
errNotUTF8 = errors.New("is not valid UTF-8")
|
errNotUTF8 = errors.New("is not valid UTF-8")
|
||||||
errNotASN = errors.New("is not an AS number such as AS64496")
|
errNotASN = errors.New("is not an AS number such as AS64496")
|
||||||
errNotPercentItem = errors.New(
|
errNotPercentItem = errors.New(
|
||||||
"is not a code, : and a percentage, such as AS64496:50 or cn:25")
|
"is not a code, : and a percentage, such as AS64496:50 or cn:25")
|
||||||
errNotPercent = errors.New(
|
errNotPercent = errors.New("is not a percentage, a whole number from 0 to 100")
|
||||||
"is not a percentage, a whole number from 0 to 100",
|
|
||||||
)
|
|
||||||
errListedTwice = errors.New("is listed twice")
|
errListedTwice = errors.New("is listed twice")
|
||||||
errNotListURL = errors.New(
|
errNotListURL = errors.New(
|
||||||
"is not an http or https URL without a user or a fragment, " +
|
"is not an http or https URL without a user or a fragment, " +
|
||||||
"such as https://www.spamhaus.org/drop/drop.txt")
|
"such as https://www.spamhaus.org/drop/drop.txt")
|
||||||
errInBlocklistURLs = errors.New("is in SWWAF_BLOCKLIST_URLS too")
|
errInBlocklistURLs = errors.New("is in SWWAF_BLOCKLIST_URLS too")
|
||||||
errNotAnHourOrMore = errors.New(
|
errNotAnHourOrMore = errors.New("is not a duration of 1h or more, such as 24h")
|
||||||
"is not a duration of 1h or more, such as 24h",
|
|
||||||
)
|
|
||||||
errNotAction = errors.New(
|
errNotAction = errors.New(
|
||||||
"is not deny, limit:<percent> such as limit:25, or log")
|
"is not deny, limit:<percent> such as limit:25, or log")
|
||||||
errNotZone = errors.New(
|
errNotZone = errors.New("is not a DNS zone such as dnsbl.dronebl.org")
|
||||||
"is not a DNS zone such as dnsbl.dronebl.org",
|
errZoneTooLong = errors.New("is longer than 189 characters, too long for the " +
|
||||||
)
|
"names IPv6 clients are asked about by")
|
||||||
errZoneTooLong = errors.New(
|
errNotResolver = errors.New("is not an IP address with an optional port, " +
|
||||||
"is longer than 189 characters, too long for the " +
|
"such as 192.0.2.53 or [2001:db8::53]:5353")
|
||||||
"names IPv6 clients are asked about by",
|
|
||||||
)
|
|
||||||
errNotResolver = errors.New(
|
|
||||||
"is not an IP address with an optional port, " +
|
|
||||||
"such as 192.0.2.53 or [2001:db8::53]:5353",
|
|
||||||
)
|
|
||||||
errNotLAPIURL = errors.New(
|
errNotLAPIURL = errors.New(
|
||||||
"is not an http or https URL without a user or a fragment, " +
|
"is not an http or https URL without a user or a fragment, " +
|
||||||
"such as http://172.17.0.1:8080")
|
"such as http://172.17.0.1:8080")
|
||||||
errNeedsLAPIKey = errors.New("the engine answers no request without it")
|
errNeedsLAPIKey = errors.New("the engine answers no request without it")
|
||||||
errLAPIKeyUnused = errors.New(
|
errLAPIKeyUnused = errors.New("it is sent only to the engine at that URL")
|
||||||
"it is sent only to the engine at that URL",
|
|
||||||
)
|
|
||||||
errAnotherList = errors.New(
|
errAnotherList = errors.New(
|
||||||
"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 with lookupEnv, normally
|
// FromEnvironment reads the settings through v, normally the viper
|
||||||
// os.LookupEnv. A setting may instead be given as a file: the variable
|
// NewViper returns, bound to the process's environment. A setting may
|
||||||
// named by the setting's name with _FILE added names the file, which is
|
// instead be given as a file: the variable named by the setting's name
|
||||||
// read now (see lookup). A setting that is not set takes its default. A
|
// with _FILE added names the file, which is read now (see lookup). A
|
||||||
// setting that is set but invalid is an error that names it.
|
// setting that is not set takes its default. A setting that is set but
|
||||||
|
// 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(
|
func FromEnvironment(v *viper.Viper) (*Config, error) {
|
||||||
lookupEnv func(string) (string, bool),
|
env := &environment{v: v, readFiles: true}
|
||||||
) (*Config, error) {
|
|
||||||
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(),
|
||||||
Observe: env.observe("SWWAF_MODE", "enforce"),
|
Observe: env.observe("SWWAF_MODE", "enforce"),
|
||||||
IPv6GroupPrefix: env.ipv6GroupPrefix(
|
IPv6GroupPrefix: env.ipv6GroupPrefix("SWWAF_IPV6_GROUP_PREFIX", "64"),
|
||||||
"SWWAF_IPV6_GROUP_PREFIX",
|
MaxTrackedClients: env.numberNotOff("SWWAF_MAX_TRACKED_CLIENTS", "20000"),
|
||||||
"64",
|
|
||||||
),
|
|
||||||
MaxTrackedClients: env.numberNotOff(
|
|
||||||
"SWWAF_MAX_TRACKED_CLIENTS",
|
|
||||||
"20000",
|
|
||||||
),
|
|
||||||
AllowNets: env.netblocks("SWWAF_ALLOW_NETS", ""),
|
AllowNets: env.netblocks("SWWAF_ALLOW_NETS", ""),
|
||||||
RateLimitExemptNets: env.netblocks(
|
RateLimitExemptNets: env.netblocks("SWWAF_RATE_LIMIT_EXEMPT_NETS", ""),
|
||||||
"SWWAF_RATE_LIMIT_EXEMPT_NETS",
|
|
||||||
"",
|
|
||||||
),
|
|
||||||
DenyNets: env.netblocks("SWWAF_DENY_NETS", ""),
|
DenyNets: env.netblocks("SWWAF_DENY_NETS", ""),
|
||||||
RateLimitPerMinute: env.count(
|
RateLimitPerMinute: env.count("SWWAF_RATE_LIMIT_PER_MINUTE", "120"),
|
||||||
"SWWAF_RATE_LIMIT_PER_MINUTE",
|
|
||||||
"120",
|
|
||||||
),
|
|
||||||
RateLimitPerHour: env.count("SWWAF_RATE_LIMIT_PER_HOUR", "1500"),
|
RateLimitPerHour: env.count("SWWAF_RATE_LIMIT_PER_HOUR", "1500"),
|
||||||
RateLimitPerDay: env.count("SWWAF_RATE_LIMIT_PER_DAY", "6000"),
|
RateLimitPerDay: env.count("SWWAF_RATE_LIMIT_PER_DAY", "6000"),
|
||||||
RateLimitExemptPaths: env.pathPrefixes(
|
RateLimitExemptPaths: env.pathPrefixes("SWWAF_RATE_LIMIT_EXEMPT_PATHS", ""),
|
||||||
"SWWAF_RATE_LIMIT_EXEMPT_PATHS",
|
BytesLimitPerMinute: env.size("SWWAF_BYTES_LIMIT_PER_MINUTE", "off"),
|
||||||
"",
|
|
||||||
),
|
|
||||||
BytesLimitPerMinute: env.size(
|
|
||||||
"SWWAF_BYTES_LIMIT_PER_MINUTE",
|
|
||||||
"off",
|
|
||||||
),
|
|
||||||
BytesLimitPerHour: env.size("SWWAF_BYTES_LIMIT_PER_HOUR", "50G"),
|
BytesLimitPerHour: env.size("SWWAF_BYTES_LIMIT_PER_HOUR", "50G"),
|
||||||
BytesLimitPerDay: env.size("SWWAF_BYTES_LIMIT_PER_DAY", "100G"),
|
BytesLimitPerDay: env.size("SWWAF_BYTES_LIMIT_PER_DAY", "100G"),
|
||||||
BytesCount: env.bytesCount("SWWAF_BYTES_COUNT", "both"),
|
BytesCount: env.bytesCount("SWWAF_BYTES_COUNT", "both"),
|
||||||
LookupSource: env.lookupSource(
|
LookupSource: env.lookupSource("SWWAF_LOOKUP_SOURCE", "geojs"),
|
||||||
"SWWAF_LOOKUP_SOURCE",
|
|
||||||
"geojs",
|
|
||||||
),
|
|
||||||
LookupDBPath: env.value("SWWAF_LOOKUP_DB_PATH", ""),
|
LookupDBPath: env.value("SWWAF_LOOKUP_DB_PATH", ""),
|
||||||
LookupTimeout: env.durationNotOff(
|
LookupTimeout: env.durationNotOff("SWWAF_LOOKUP_TIMEOUT", "800ms"),
|
||||||
"SWWAF_LOOKUP_TIMEOUT",
|
AddLookupHeaders: env.boolean("SWWAF_ADD_LOOKUP_HEADERS", "false"),
|
||||||
"800ms",
|
|
||||||
),
|
|
||||||
AddLookupHeaders: env.boolean(
|
|
||||||
"SWWAF_ADD_LOOKUP_HEADERS",
|
|
||||||
"false",
|
|
||||||
),
|
|
||||||
DeniedCountries: env.countries("SWWAF_DENIED_COUNTRIES", ""),
|
DeniedCountries: env.countries("SWWAF_DENIED_COUNTRIES", ""),
|
||||||
ExclusivelyAllowedCountries: env.countries(
|
ExclusivelyAllowedCountries: env.countries(
|
||||||
"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
||||||
ASNLimitPercent: env.percents(
|
ASNLimitPercent: env.percents("SWWAF_ASN_LIMIT_PERCENT", ParseASN),
|
||||||
"SWWAF_ASN_LIMIT_PERCENT",
|
CountryLimitPercent: env.percents("SWWAF_COUNTRY_LIMIT_PERCENT", parseCountry),
|
||||||
ParseASN,
|
ASNBytesPercent: env.percents("SWWAF_ASN_BYTES_PERCENT", ParseASN),
|
||||||
),
|
CountryBytesPercent: env.percents("SWWAF_COUNTRY_BYTES_PERCENT", parseCountry),
|
||||||
CountryLimitPercent: env.percents(
|
UnknownLimitPercent: env.percent("SWWAF_UNKNOWN_LIMIT_PERCENT", "100"),
|
||||||
"SWWAF_COUNTRY_LIMIT_PERCENT",
|
|
||||||
parseCountry,
|
|
||||||
),
|
|
||||||
ASNBytesPercent: env.percents(
|
|
||||||
"SWWAF_ASN_BYTES_PERCENT",
|
|
||||||
ParseASN,
|
|
||||||
),
|
|
||||||
CountryBytesPercent: env.percents(
|
|
||||||
"SWWAF_COUNTRY_BYTES_PERCENT",
|
|
||||||
parseCountry,
|
|
||||||
),
|
|
||||||
UnknownLimitPercent: env.percent(
|
|
||||||
"SWWAF_UNKNOWN_LIMIT_PERCENT",
|
|
||||||
"100",
|
|
||||||
),
|
|
||||||
ASNLimitPercentURL: env.listURL("SWWAF_ASN_LIMIT_PERCENT_URL"),
|
ASNLimitPercentURL: env.listURL("SWWAF_ASN_LIMIT_PERCENT_URL"),
|
||||||
BlocklistURLs: env.listURLs("SWWAF_BLOCKLIST_URLS"),
|
BlocklistURLs: env.listURLs("SWWAF_BLOCKLIST_URLS"),
|
||||||
BlocklistRefresh: env.refresh("SWWAF_BLOCKLIST_REFRESH", "24h"),
|
BlocklistRefresh: env.refresh("SWWAF_BLOCKLIST_REFRESH", "24h"),
|
||||||
@@ -567,78 +498,32 @@ func FromEnvironment(
|
|||||||
DNSBLResolver: env.resolver("SWWAF_DNSBL_RESOLVER"),
|
DNSBLResolver: env.resolver("SWWAF_DNSBL_RESOLVER"),
|
||||||
AbuseIPDBKey: env.secret("SWWAF_ABUSEIPDB_KEY"),
|
AbuseIPDBKey: env.secret("SWWAF_ABUSEIPDB_KEY"),
|
||||||
AbuseIPDBMinScore: env.percent("SWWAF_ABUSEIPDB_MIN_SCORE", "75"),
|
AbuseIPDBMinScore: env.percent("SWWAF_ABUSEIPDB_MIN_SCORE", "75"),
|
||||||
AbuseIPDBDailyBudget: env.numberNotOff(
|
AbuseIPDBDailyBudget: env.numberNotOff("SWWAF_ABUSEIPDB_DAILY_BUDGET", "900"),
|
||||||
"SWWAF_ABUSEIPDB_DAILY_BUDGET",
|
ReputationCacheTTL: env.durationNotOff("SWWAF_REPUTATION_CACHE_TTL", "24h"),
|
||||||
"900",
|
ReputationTimeout: env.durationNotOff("SWWAF_REPUTATION_TIMEOUT", "2s"),
|
||||||
),
|
CrowdSecDecisionsURL: env.crowdSecDecisionsURL("SWWAF_CROWDSEC_LAPI_URL"),
|
||||||
ReputationCacheTTL: env.durationNotOff(
|
|
||||||
"SWWAF_REPUTATION_CACHE_TTL",
|
|
||||||
"24h",
|
|
||||||
),
|
|
||||||
ReputationTimeout: env.durationNotOff(
|
|
||||||
"SWWAF_REPUTATION_TIMEOUT",
|
|
||||||
"2s",
|
|
||||||
),
|
|
||||||
CrowdSecDecisionsURL: env.crowdSecDecisionsURL(
|
|
||||||
"SWWAF_CROWDSEC_LAPI_URL",
|
|
||||||
),
|
|
||||||
CrowdSecKey: env.secret("SWWAF_CROWDSEC_LAPI_KEY"),
|
CrowdSecKey: env.secret("SWWAF_CROWDSEC_LAPI_KEY"),
|
||||||
BanResponse: env.banResponse("SWWAF_BAN_RESPONSE", "403"),
|
BanResponse: env.banResponse("SWWAF_BAN_RESPONSE", "403"),
|
||||||
LimitBanDuration: env.durationNotOff(
|
LimitBanDuration: env.durationNotOff("SWWAF_LIMIT_BAN_DURATION", "1h"),
|
||||||
"SWWAF_LIMIT_BAN_DURATION",
|
LimitBanRepeatWindow: env.durationNotOff("SWWAF_LIMIT_BAN_REPEAT_WINDOW", "24h"),
|
||||||
"1h",
|
MaxBanDuration: env.durationNotOff("SWWAF_MAX_BAN_DURATION", "7d"),
|
||||||
),
|
AttackBanDuration: env.durationNotOff("SWWAF_ATTACK_BAN_DURATION", "7d"),
|
||||||
LimitBanRepeatWindow: env.durationNotOff(
|
|
||||||
"SWWAF_LIMIT_BAN_REPEAT_WINDOW",
|
|
||||||
"24h",
|
|
||||||
),
|
|
||||||
MaxBanDuration: env.durationNotOff(
|
|
||||||
"SWWAF_MAX_BAN_DURATION",
|
|
||||||
"7d",
|
|
||||||
),
|
|
||||||
AttackBanDuration: env.durationNotOff(
|
|
||||||
"SWWAF_ATTACK_BAN_DURATION",
|
|
||||||
"7d",
|
|
||||||
),
|
|
||||||
MaxBans: env.numberNotOff("SWWAF_MAX_BANS", "5000"),
|
MaxBans: env.numberNotOff("SWWAF_MAX_BANS", "5000"),
|
||||||
BanScopeV4Prefix: env.v4Prefix(
|
BanScopeV4Prefix: env.v4Prefix("SWWAF_BAN_SCOPE_V4_PREFIX", "32"),
|
||||||
"SWWAF_BAN_SCOPE_V4_PREFIX",
|
StateDir: env.absolutePath("SWWAF_STATE_DIR", "/var/lib/smallwebwaf"),
|
||||||
"32",
|
StateWriteDelay: env.durationNotOff("SWWAF_STATE_WRITE_DELAY", "10s"),
|
||||||
),
|
StateCounterInterval: env.durationNotOff("SWWAF_STATE_COUNTER_INTERVAL", "15m"),
|
||||||
StateDir: env.absolutePath(
|
LogRequestHeaders: env.headerNames("SWWAF_LOG_REQUEST_HEADERS",
|
||||||
"SWWAF_STATE_DIR",
|
"accept,accept-language,accept-encoding,content-type,origin,range"),
|
||||||
"/var/lib/smallwebwaf",
|
|
||||||
),
|
|
||||||
StateWriteDelay: env.durationNotOff(
|
|
||||||
"SWWAF_STATE_WRITE_DELAY",
|
|
||||||
"10s",
|
|
||||||
),
|
|
||||||
StateCounterInterval: env.durationNotOff(
|
|
||||||
"SWWAF_STATE_COUNTER_INTERVAL",
|
|
||||||
"15m",
|
|
||||||
),
|
|
||||||
LogRequestHeaders: env.headerNames(
|
|
||||||
"SWWAF_LOG_REQUEST_HEADERS",
|
|
||||||
"accept,accept-language,accept-encoding,content-type,origin,range",
|
|
||||||
),
|
|
||||||
LogLevel: env.logLevel("SWWAF_LOG_LEVEL", "info"),
|
LogLevel: env.logLevel("SWWAF_LOG_LEVEL", "info"),
|
||||||
AdminToken: env.token("SWWAF_ADMIN_TOKEN"),
|
AdminToken: env.token("SWWAF_ADMIN_TOKEN"),
|
||||||
MetricsToken: env.token("SWWAF_METRICS_TOKEN"),
|
MetricsToken: env.token("SWWAF_METRICS_TOKEN"),
|
||||||
MetricsTopN: env.numberNotOff("SWWAF_METRICS_TOP_N", "50"),
|
MetricsTopN: env.numberNotOff("SWWAF_METRICS_TOP_N", "50"),
|
||||||
RulesDir: env.value(
|
RulesDir: env.value("SWWAF_RULES_DIR", "/etc/smallwebwaf/rules.d"),
|
||||||
"SWWAF_RULES_DIR",
|
|
||||||
"/etc/smallwebwaf/rules.d",
|
|
||||||
),
|
|
||||||
RulesEnabled: env.boolean("SWWAF_RULES_ENABLED", "true"),
|
RulesEnabled: env.boolean("SWWAF_RULES_ENABLED", "true"),
|
||||||
WAFMode: env.wafMode("SWWAF_WAF_MODE", WAFModeBlock),
|
WAFMode: env.wafMode("SWWAF_WAF_MODE", WAFModeBlock),
|
||||||
WAFParanoiaLevel: env.paranoiaLevel(
|
WAFParanoiaLevel: env.paranoiaLevel("SWWAF_WAF_PARANOIA_LEVEL", "1"),
|
||||||
"SWWAF_WAF_PARANOIA_LEVEL",
|
WAFAnomalyThreshold: env.numberOrOff("SWWAF_WAF_ANOMALY_THRESHOLD", "5"),
|
||||||
"1",
|
|
||||||
),
|
|
||||||
WAFAnomalyThreshold: env.numberOrOff(
|
|
||||||
"SWWAF_WAF_ANOMALY_THRESHOLD",
|
|
||||||
"5",
|
|
||||||
),
|
|
||||||
WAFDisabledRules: env.ruleIDs("SWWAF_WAF_DISABLED_RULES",
|
WAFDisabledRules: env.ruleIDs("SWWAF_WAF_DISABLED_RULES",
|
||||||
"920340,920420,920440,920640,930130,930140"),
|
"920340,920420,920440,920640,930130,930140"),
|
||||||
WAFExemptPaths: env.pathPrefixes("SWWAF_WAF_EXEMPT_PATHS", ""),
|
WAFExemptPaths: env.pathPrefixes("SWWAF_WAF_EXEMPT_PATHS", ""),
|
||||||
@@ -646,46 +531,25 @@ func FromEnvironment(
|
|||||||
TrapPaths: env.trapPaths("SWWAF_TRAP_PATHS"),
|
TrapPaths: env.trapPaths("SWWAF_TRAP_PATHS"),
|
||||||
ErrorBurstThreshold: env.count("SWWAF_ERROR_BURST_THRESHOLD", "30"),
|
ErrorBurstThreshold: env.count("SWWAF_ERROR_BURST_THRESHOLD", "30"),
|
||||||
LogRemoteURL: env.logRemoteURL("SWWAF_LOG_REMOTE_URL"),
|
LogRemoteURL: env.logRemoteURL("SWWAF_LOG_REMOTE_URL"),
|
||||||
LogRemoteTLSCAs: env.certificates(
|
LogRemoteTLSCAs: env.certificates("SWWAF_LOG_REMOTE_TLS_CA_FILE"),
|
||||||
"SWWAF_LOG_REMOTE_TLS_CA_FILE",
|
LogRemoteBuffer: env.numberNotOff("SWWAF_LOG_REMOTE_BUFFER", "10000"),
|
||||||
),
|
LogRemoteFacility: env.facility("SWWAF_LOG_REMOTE_FACILITY", "local0"),
|
||||||
LogRemoteBuffer: env.numberNotOff(
|
|
||||||
"SWWAF_LOG_REMOTE_BUFFER",
|
|
||||||
"10000",
|
|
||||||
),
|
|
||||||
LogRemoteFacility: env.facility(
|
|
||||||
"SWWAF_LOG_REMOTE_FACILITY",
|
|
||||||
"local0",
|
|
||||||
),
|
|
||||||
AlertWebhookURL: env.webhookURL("SWWAF_ALERT_WEBHOOK_URL"),
|
AlertWebhookURL: env.webhookURL("SWWAF_ALERT_WEBHOOK_URL"),
|
||||||
AlertWebhookHeaders: env.webhookHeaders(
|
AlertWebhookHeaders: env.webhookHeaders("SWWAF_ALERT_WEBHOOK_HEADERS"),
|
||||||
"SWWAF_ALERT_WEBHOOK_HEADERS",
|
AlertSlackWebhookURL: env.webhookURL("SWWAF_ALERT_SLACK_WEBHOOK_URL"),
|
||||||
),
|
|
||||||
AlertSlackWebhookURL: env.webhookURL(
|
|
||||||
"SWWAF_ALERT_SLACK_WEBHOOK_URL",
|
|
||||||
),
|
|
||||||
AlertNtfyURL: env.webhookURL("SWWAF_ALERT_NTFY_URL"),
|
AlertNtfyURL: env.webhookURL("SWWAF_ALERT_NTFY_URL"),
|
||||||
AlertNtfyToken: env.secret("SWWAF_ALERT_NTFY_TOKEN"),
|
AlertNtfyToken: env.secret("SWWAF_ALERT_NTFY_TOKEN"),
|
||||||
AlertEvents: env.alertEvents("SWWAF_ALERT_EVENTS",
|
AlertEvents: env.alertEvents("SWWAF_ALERT_EVENTS",
|
||||||
strings.Join(alerts.Events(), ",")),
|
strings.Join(alerts.Events(), ",")),
|
||||||
AlertCooldown: env.duration("SWWAF_ALERT_COOLDOWN", "15m"),
|
AlertCooldown: env.duration("SWWAF_ALERT_COOLDOWN", "15m"),
|
||||||
AlertMaxPerHour: env.numberOrOff(
|
AlertMaxPerHour: env.numberOrOff("SWWAF_ALERT_MAX_PER_HOUR", "60"),
|
||||||
"SWWAF_ALERT_MAX_PER_HOUR",
|
|
||||||
"60",
|
|
||||||
),
|
|
||||||
AnomalyClient: env.thresholds("SWWAF_ANOMALY_CLIENT_"),
|
AnomalyClient: env.thresholds("SWWAF_ANOMALY_CLIENT_"),
|
||||||
AnomalyNet: env.thresholds("SWWAF_ANOMALY_NET_"),
|
AnomalyNet: env.thresholds("SWWAF_ANOMALY_NET_"),
|
||||||
AnomalyASN: env.thresholds("SWWAF_ANOMALY_ASN_"),
|
AnomalyASN: env.thresholds("SWWAF_ANOMALY_ASN_"),
|
||||||
AnomalyTotal: env.thresholds("SWWAF_ANOMALY_TOTAL_"),
|
AnomalyTotal: env.thresholds("SWWAF_ANOMALY_TOTAL_"),
|
||||||
AnomalyWatch: env.thresholds("SWWAF_WATCH_"),
|
AnomalyWatch: env.thresholds("SWWAF_WATCH_"),
|
||||||
AnomalyNetV4Prefix: env.v4Prefix(
|
AnomalyNetV4Prefix: env.v4Prefix("SWWAF_ANOMALY_NET_V4_PREFIX", "24"),
|
||||||
"SWWAF_ANOMALY_NET_V4_PREFIX",
|
AnomalyNetV6Prefix: env.v6Prefix("SWWAF_ANOMALY_NET_V6_PREFIX", "48"),
|
||||||
"24",
|
|
||||||
),
|
|
||||||
AnomalyNetV6Prefix: env.v6Prefix(
|
|
||||||
"SWWAF_ANOMALY_NET_V6_PREFIX",
|
|
||||||
"48",
|
|
||||||
),
|
|
||||||
WatchNets: env.namedNetblocks("SWWAF_WATCH_NETS"),
|
WatchNets: env.namedNetblocks("SWWAF_WATCH_NETS"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,8 +578,8 @@ func FromEnvironment(
|
|||||||
// 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(lookupEnv func(string) (string, bool)) (string, error) {
|
func Command(v *viper.Viper) (string, error) {
|
||||||
command, _ := lookupEnv("SMALLWEBWAF_COMMAND")
|
command, _ := readSetting(v, "SMALLWEBWAF_COMMAND")
|
||||||
|
|
||||||
switch command {
|
switch command {
|
||||||
case "", "rproxy":
|
case "", "rproxy":
|
||||||
@@ -723,21 +587,15 @@ func Command(lookupEnv func(string) (string, bool)) (string, error) {
|
|||||||
case "servewaf":
|
case "servewaf":
|
||||||
return command, nil
|
return command, nil
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf(
|
return "", fmt.Errorf("SMALLWEBWAF_COMMAND: %q %w", command, errNotCommand)
|
||||||
"SMALLWEBWAF_COMMAND: %q %w",
|
|
||||||
command,
|
|
||||||
errNotCommand,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(
|
func ServeWAFListenAddr(v *viper.Viper) (string, error) {
|
||||||
lookupEnv func(string) (string, bool),
|
env := &environment{v: v, readFiles: true}
|
||||||
) (string, error) {
|
|
||||||
env := &environment{lookupEnv: lookupEnv}
|
|
||||||
listenAddr := env.address("SWWAF_LISTEN_ADDR")
|
listenAddr := env.address("SWWAF_LISTEN_ADDR")
|
||||||
|
|
||||||
return listenAddr, env.err
|
return listenAddr, env.err
|
||||||
@@ -747,8 +605,8 @@ func ServeWAFListenAddr(
|
|||||||
// 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(lookupEnv func(string) (string, bool)) string {
|
func InstanceName(v *viper.Viper) string {
|
||||||
env := &environment{lookupEnv: lookupEnv}
|
env := &environment{v: v, readFiles: true}
|
||||||
|
|
||||||
return env.instanceName()
|
return env.instanceName()
|
||||||
}
|
}
|
||||||
@@ -764,13 +622,26 @@ 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.
|
// value for the log, and keeps the first error. It reads every setting
|
||||||
|
// through v, the viper NewViper returns, and, unless readFiles is false,
|
||||||
|
// honours each setting's _FILE form too.
|
||||||
type environment struct {
|
type environment struct {
|
||||||
lookupEnv func(string) (string, bool)
|
v *viper.Viper
|
||||||
|
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 {
|
||||||
@@ -789,28 +660,27 @@ 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 := e.lookupEnv(name)
|
value, set := readSetting(e.v, name)
|
||||||
|
if !e.readFiles {
|
||||||
|
return value, set
|
||||||
|
}
|
||||||
|
|
||||||
fileName := name + "_FILE"
|
fileName := name + "_FILE"
|
||||||
|
|
||||||
path, inFile := e.lookupEnv(fileName)
|
path, inFile := readSetting(e.v, fileName)
|
||||||
if !inFile {
|
if !inFile {
|
||||||
return value, set
|
return value, set
|
||||||
}
|
}
|
||||||
|
|
||||||
if set {
|
if set {
|
||||||
e.check(
|
e.check(name, fmt.Errorf("is set, and so is %s; %w", fileName, errSetTwice))
|
||||||
name,
|
|
||||||
fmt.Errorf("is set, and so is %s; %w", fileName, errSetTwice),
|
|
||||||
)
|
|
||||||
|
|
||||||
return value, set
|
return value, set
|
||||||
}
|
}
|
||||||
|
|
||||||
e.settings = append(e.settings, slog.String(fileName, path))
|
e.settings = append(e.settings, slog.String(fileName, path))
|
||||||
|
|
||||||
contents, err := os.ReadFile(
|
contents, err := os.ReadFile(path) //nolint:gosec // a file the admin names
|
||||||
path,
|
|
||||||
) //nolint:gosec // a file the admin names
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.check(fileName, fmt.Errorf("cannot be read: %w", err))
|
e.check(fileName, fmt.Errorf("cannot be read: %w", err))
|
||||||
|
|
||||||
@@ -1061,10 +931,7 @@ func (e *environment) zones(name string) []string {
|
|||||||
logged[i] = MaskZoneKey(zone)
|
logged[i] = MaskZoneKey(zone)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.settings = append(
|
e.settings = append(e.settings, slog.String(name, strings.Join(logged, ",")))
|
||||||
e.settings,
|
|
||||||
slog.String(name, strings.Join(logged, ",")),
|
|
||||||
)
|
|
||||||
|
|
||||||
return zones
|
return zones
|
||||||
}
|
}
|
||||||
@@ -1116,15 +983,10 @@ func (e *environment) checkLookupDBPath(cfg *Config) {
|
|||||||
switch {
|
switch {
|
||||||
case cfg.LookupSource == fileSource && cfg.LookupDBPath == "":
|
case cfg.LookupSource == fileSource && cfg.LookupDBPath == "":
|
||||||
e.check("SWWAF_LOOKUP_SOURCE", fmt.Errorf(
|
e.check("SWWAF_LOOKUP_SOURCE", fmt.Errorf(
|
||||||
"is file while SWWAF_LOOKUP_DB_PATH is unset; %w",
|
"is file while SWWAF_LOOKUP_DB_PATH is unset; %w", errNeedsDBPath))
|
||||||
errNeedsDBPath,
|
|
||||||
))
|
|
||||||
case cfg.LookupSource != fileSource && cfg.LookupDBPath != "":
|
case cfg.LookupSource != fileSource && cfg.LookupDBPath != "":
|
||||||
e.check("SWWAF_LOOKUP_DB_PATH", fmt.Errorf(
|
e.check("SWWAF_LOOKUP_DB_PATH", fmt.Errorf(
|
||||||
"is set while SWWAF_LOOKUP_SOURCE is %s; %w",
|
"is set while SWWAF_LOOKUP_SOURCE is %s; %w", cfg.LookupSource, errDBPathUnused))
|
||||||
cfg.LookupSource,
|
|
||||||
errDBPathUnused,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1165,11 +1027,8 @@ func (e *environment) checkCountriesAndLookups(cfg *Config) {
|
|||||||
{"SWWAF_ANOMALY_ASN_BYTES_PER_HOUR", cfg.AnomalyASN.BytesPerHour > 0},
|
{"SWWAF_ANOMALY_ASN_BYTES_PER_HOUR", cfg.AnomalyASN.BytesPerHour > 0},
|
||||||
} {
|
} {
|
||||||
if setting.set {
|
if setting.set {
|
||||||
e.check(
|
e.check(setting.name, fmt.Errorf("is set while SWWAF_LOOKUP_SOURCE is off; %w",
|
||||||
setting.name,
|
errNeedsLookups))
|
||||||
fmt.Errorf("is set while SWWAF_LOOKUP_SOURCE is off; %w",
|
|
||||||
errNeedsLookups),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1192,21 +1051,14 @@ func (e *environment) checkCrowdSec(cfg *Config) {
|
|||||||
switch {
|
switch {
|
||||||
case decisionsURL != "" && cfg.CrowdSecKey == "":
|
case decisionsURL != "" && cfg.CrowdSecKey == "":
|
||||||
e.check("SWWAF_CROWDSEC_LAPI_URL", fmt.Errorf(
|
e.check("SWWAF_CROWDSEC_LAPI_URL", fmt.Errorf(
|
||||||
"is set while SWWAF_CROWDSEC_LAPI_KEY is unset; %w",
|
"is set while SWWAF_CROWDSEC_LAPI_KEY is unset; %w", errNeedsLAPIKey))
|
||||||
errNeedsLAPIKey,
|
|
||||||
))
|
|
||||||
case decisionsURL == "" && cfg.CrowdSecKey != "":
|
case decisionsURL == "" && cfg.CrowdSecKey != "":
|
||||||
e.check("SWWAF_CROWDSEC_LAPI_KEY", fmt.Errorf(
|
e.check("SWWAF_CROWDSEC_LAPI_KEY", fmt.Errorf(
|
||||||
"is set while SWWAF_CROWDSEC_LAPI_URL is unset; %w",
|
"is set while SWWAF_CROWDSEC_LAPI_URL is unset; %w", errLAPIKeyUnused))
|
||||||
errLAPIKeyUnused,
|
|
||||||
))
|
|
||||||
case decisionsURL != "" && (slices.Contains(cfg.BlocklistURLs, decisionsURL) ||
|
case decisionsURL != "" && (slices.Contains(cfg.BlocklistURLs, decisionsURL) ||
|
||||||
decisionsURL == cfg.ASNLimitPercentURL):
|
decisionsURL == cfg.ASNLimitPercentURL):
|
||||||
e.check(
|
e.check("SWWAF_CROWDSEC_LAPI_URL", fmt.Errorf("gives the decision list %q, which %w",
|
||||||
"SWWAF_CROWDSEC_LAPI_URL",
|
decisionsURL, errAnotherList))
|
||||||
fmt.Errorf("gives the decision list %q, which %w",
|
|
||||||
decisionsURL, errAnotherList),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1221,9 +1073,7 @@ func (e *environment) headerNames(name, defaultValue string) []string {
|
|||||||
|
|
||||||
// durationNotOff reads a setting that is a duration and, unlike a
|
// durationNotOff reads a setting that is a duration and, unlike a
|
||||||
// timeout, cannot be off.
|
// timeout, cannot be off.
|
||||||
func (e *environment) durationNotOff(
|
func (e *environment) durationNotOff(name, defaultValue string) time.Duration {
|
||||||
name, defaultValue string,
|
|
||||||
) time.Duration {
|
|
||||||
duration, err := ParseDurationNotOff(e.value(name, defaultValue))
|
duration, err := ParseDurationNotOff(e.value(name, defaultValue))
|
||||||
e.check(name, err)
|
e.check(name, err)
|
||||||
|
|
||||||
@@ -1363,7 +1213,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, _ := e.lookupEnv(name)
|
path, _ := readSetting(e.v, name)
|
||||||
e.settings = append(e.settings, slog.String(name, path))
|
e.settings = append(e.settings, slog.String(name, path))
|
||||||
|
|
||||||
if path == "" {
|
if path == "" {
|
||||||
@@ -1403,10 +1253,7 @@ func (e *environment) instanceName() string {
|
|||||||
|
|
||||||
value := e.value("SWWAF_INSTANCE_NAME", hostname)
|
value := e.value("SWWAF_INSTANCE_NAME", hostname)
|
||||||
if !utf8.ValidString(value) {
|
if !utf8.ValidString(value) {
|
||||||
e.check(
|
e.check("SWWAF_INSTANCE_NAME", fmt.Errorf("%q %w", value, errNotUTF8))
|
||||||
"SWWAF_INSTANCE_NAME",
|
|
||||||
fmt.Errorf("%q %w", value, errNotUTF8),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
@@ -1415,10 +1262,7 @@ func (e *environment) instanceName() string {
|
|||||||
// appName reads the setting that is the APP-NAME of the records the log
|
// appName reads the setting that is the APP-NAME of the records the log
|
||||||
// lines are sent in, by default the instance name. Its value is checked
|
// lines are sent in, by default the instance name. Its value is checked
|
||||||
// when it is set, and, while lines are sent, when it is the instance name.
|
// when it is set, and, while lines are sent, when it is the instance name.
|
||||||
func (e *environment) appName(
|
func (e *environment) appName(name, instanceName string, sending bool) string {
|
||||||
name, instanceName string,
|
|
||||||
sending bool,
|
|
||||||
) string {
|
|
||||||
value, set := e.lookup(name)
|
value, set := e.lookup(name)
|
||||||
if !set {
|
if !set {
|
||||||
value = instanceName
|
value = instanceName
|
||||||
@@ -1431,14 +1275,8 @@ func (e *environment) appName(
|
|||||||
case set:
|
case set:
|
||||||
e.check(name, fmt.Errorf("%q %w", value, errNotAppName))
|
e.check(name, fmt.Errorf("%q %w", value, errNotAppName))
|
||||||
case sending:
|
case sending:
|
||||||
e.check(
|
e.check(name, fmt.Errorf("is unset, and SWWAF_INSTANCE_NAME %q, its default, %w",
|
||||||
name,
|
value, errNotAppName))
|
||||||
fmt.Errorf(
|
|
||||||
"is unset, and SWWAF_INSTANCE_NAME %q, its default, %w",
|
|
||||||
value,
|
|
||||||
errNotAppName,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
@@ -1447,16 +1285,11 @@ func (e *environment) appName(
|
|||||||
// checkInstanceNameForNtfy refuses an instance name that holds a control
|
// checkInstanceNameForNtfy refuses an instance name that holds a control
|
||||||
// character while ntfySet, SWWAF_ALERT_NTFY_URL being set: ntfy is sent
|
// character while ntfySet, SWWAF_ALERT_NTFY_URL being set: ntfy is sent
|
||||||
// the instance name in a header, which cannot hold one.
|
// the instance name in a header, which cannot hold one.
|
||||||
func (e *environment) checkInstanceNameForNtfy(
|
func (e *environment) checkInstanceNameForNtfy(instanceName string, ntfySet bool) {
|
||||||
instanceName string,
|
|
||||||
ntfySet bool,
|
|
||||||
) {
|
|
||||||
if ntfySet && strings.ContainsFunc(instanceName, unicode.IsControl) {
|
if ntfySet && strings.ContainsFunc(instanceName, unicode.IsControl) {
|
||||||
e.check("SWWAF_INSTANCE_NAME", fmt.Errorf(
|
e.check("SWWAF_INSTANCE_NAME", fmt.Errorf(
|
||||||
"%q %w, and is sent to ntfy in a header while SWWAF_ALERT_NTFY_URL is set",
|
"%q %w, and is sent to ntfy in a header while SWWAF_ALERT_NTFY_URL is set",
|
||||||
instanceName,
|
instanceName, errControlCharacter))
|
||||||
errControlCharacter,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1776,10 +1609,7 @@ func parseNamedNetblocks(value string) ([]anomaly.NamedNetblock, error) {
|
|||||||
return nil, fmt.Errorf("%q %w", name, errListedTwice)
|
return nil, fmt.Errorf("%q %w", name, errListedTwice)
|
||||||
}
|
}
|
||||||
|
|
||||||
named = append(
|
named = append(named, anomaly.NamedNetblock{Name: name, Netblock: netblock})
|
||||||
named,
|
|
||||||
anomaly.NamedNetblock{Name: name, Netblock: netblock},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return named, nil
|
return named, nil
|
||||||
@@ -2018,11 +1848,8 @@ func parseHeaderNames(value string) ([]string, error) {
|
|||||||
header := strings.ToLower(item)
|
header := strings.ToLower(item)
|
||||||
switch header {
|
switch header {
|
||||||
case "host":
|
case "host":
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf("%q %w; the request's host is the field host",
|
||||||
"%q %w; the request's host is the field host",
|
item, errHeaderTakenOut)
|
||||||
item,
|
|
||||||
errHeaderTakenOut,
|
|
||||||
)
|
|
||||||
case "transfer-encoding":
|
case "transfer-encoding":
|
||||||
return nil, fmt.Errorf("%q %w", item, errHeaderTakenOut)
|
return nil, fmt.Errorf("%q %w", item, errHeaderTakenOut)
|
||||||
}
|
}
|
||||||
@@ -2059,11 +1886,9 @@ func parseUpstreamURL(value string) (*url.URL, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onlySchemeAndHost := (upstream.Scheme == "http" || upstream.Scheme == "https") &&
|
onlySchemeAndHost := (upstream.Scheme == "http" || upstream.Scheme == "https") &&
|
||||||
upstream.Hostname() != "" && upstream.User == nil &&
|
upstream.Hostname() != "" && upstream.User == nil && upstream.Opaque == "" &&
|
||||||
upstream.Opaque == "" &&
|
|
||||||
(upstream.Path == "" || upstream.Path == "/") &&
|
(upstream.Path == "" || upstream.Path == "/") &&
|
||||||
upstream.RawQuery == "" &&
|
upstream.RawQuery == "" && upstream.Fragment == ""
|
||||||
upstream.Fragment == ""
|
|
||||||
if !onlySchemeAndHost {
|
if !onlySchemeAndHost {
|
||||||
return nil, fmt.Errorf("%q %w", value, errNotUpstreamURL)
|
return nil, fmt.Errorf("%q %w", value, errNotUpstreamURL)
|
||||||
}
|
}
|
||||||
@@ -2087,11 +1912,7 @@ func parseLogRemoteURL(value string) (*url.URL, error) {
|
|||||||
return nil, fmt.Errorf("%q %w", value, errNotLogRemoteURL)
|
return nil, fmt.Errorf("%q %w", value, errNotLogRemoteURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
schemes := []string{
|
schemes := []string{remotelog.SchemeUDP, remotelog.SchemeTCP, remotelog.SchemeTLS}
|
||||||
remotelog.SchemeUDP,
|
|
||||||
remotelog.SchemeTCP,
|
|
||||||
remotelog.SchemeTLS,
|
|
||||||
}
|
|
||||||
port, err := strconv.ParseUint(remote.Port(), 10, 16)
|
port, err := strconv.ParseUint(remote.Port(), 10, 16)
|
||||||
|
|
||||||
onlySchemeHostAndPort := slices.Contains(schemes, remote.Scheme) &&
|
onlySchemeHostAndPort := slices.Contains(schemes, remote.Scheme) &&
|
||||||
@@ -2154,9 +1975,7 @@ func isHTTPURL(u *url.URL) bool {
|
|||||||
|
|
||||||
return (u.Scheme == "http" || u.Scheme == "https") && u.Hostname() != "" &&
|
return (u.Scheme == "http" || u.Scheme == "https") && u.Hostname() != "" &&
|
||||||
(u.Port() == "" || (err == nil && port != 0)) &&
|
(u.Port() == "" || (err == nil && port != 0)) &&
|
||||||
u.User == nil &&
|
u.User == nil && u.Opaque == "" && u.Fragment == ""
|
||||||
u.Opaque == "" &&
|
|
||||||
u.Fragment == ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isListURL reports whether value is a URL a list can be fetched from, as
|
// isListURL reports whether value is a URL a list can be fetched from, as
|
||||||
@@ -2223,12 +2042,9 @@ func parseZones(value string) ([]string, error) {
|
|||||||
|
|
||||||
for i, zone := range zones {
|
for i, zone := range zones {
|
||||||
shown := MaskZoneKey(zone)
|
shown := MaskZoneKey(zone)
|
||||||
listedBefore := slices.ContainsFunc(
|
listedBefore := slices.ContainsFunc(zones[:i], func(earlier string) bool {
|
||||||
zones[:i],
|
|
||||||
func(earlier string) bool {
|
|
||||||
return strings.EqualFold(MaskZoneKey(earlier), shown)
|
return strings.EqualFold(MaskZoneKey(earlier), shown)
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case len(zone) > maxZoneLength:
|
case len(zone) > maxZoneLength:
|
||||||
@@ -2315,13 +2131,8 @@ func parseWebhookHeaders(value string) (http.Header, string, error) {
|
|||||||
name, headerValue, found := strings.Cut(item, ":")
|
name, headerValue, found := strings.Cut(item, ":")
|
||||||
name = strings.TrimSpace(name)
|
name = strings.TrimSpace(name)
|
||||||
|
|
||||||
if !found || !IsHeaderName(name) ||
|
if !found || !IsHeaderName(name) || strings.ContainsAny(headerValue, "\r\n\x00") {
|
||||||
strings.ContainsAny(headerValue, "\r\n\x00") {
|
return nil, "", fmt.Errorf("item %d %w", i+1, errNotWebhookHeader)
|
||||||
return nil, "", fmt.Errorf(
|
|
||||||
"item %d %w",
|
|
||||||
i+1,
|
|
||||||
errNotWebhookHeader,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
headers.Add(name, strings.TrimSpace(headerValue))
|
headers.Add(name, strings.TrimSpace(headerValue))
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ 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"
|
||||||
)
|
)
|
||||||
@@ -217,18 +219,22 @@ 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
|
||||||
|
|
||||||
// lookupEnv reads one of the variables, as os.LookupEnv does.
|
// viper returns a viper Set with each of the variables, as the viper
|
||||||
func (e environment) lookupEnv(name string) (string, bool) {
|
// NewViper returns one bound to a process's environment.
|
||||||
value, ok := e[name]
|
func (e environment) viper() *viper.Viper {
|
||||||
|
v := viper.New()
|
||||||
|
for name, value := range e {
|
||||||
|
v.Set(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
return value, ok
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.lookupEnv)
|
cfg, err := config.FromEnvironment(env.viper())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("settings %v: %v", env, err)
|
t.Fatalf("settings %v: %v", env, err)
|
||||||
}
|
}
|
||||||
@@ -455,7 +461,7 @@ func TestPathPrefixNotStartingWithSlashStopsTheStart(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_, err := config.FromEnvironment(
|
_, err := config.FromEnvironment(
|
||||||
environment{rateLimitExemptPaths: "/favicon.ico,assets/"}.lookupEnv)
|
environment{rateLimitExemptPaths: "/favicon.ico,assets/"}.viper())
|
||||||
|
|
||||||
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/`
|
||||||
@@ -510,7 +516,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||||
|
|
||||||
want := tc.name + ": " + tc.want
|
want := tc.name + ": " + tc.want
|
||||||
if err == nil || err.Error() != want {
|
if err == nil || err.Error() != want {
|
||||||
@@ -636,7 +642,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||||
|
|
||||||
want := tc.name + ": " + tc.want
|
want := tc.name + ": " + tc.want
|
||||||
if err == nil || err.Error() != want {
|
if err == nil || err.Error() != want {
|
||||||
@@ -761,7 +767,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -778,7 +784,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}.lookupEnv)
|
_, err = config.FromEnvironment(environment{logRemoteTLSCAFile: caFile}.viper())
|
||||||
|
|
||||||
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 {
|
||||||
@@ -793,7 +799,7 @@ func TestInstanceNameNotAnAppNameStopsTheStartOnlyWhileSending(t *testing.T) {
|
|||||||
|
|
||||||
sending := environment{logRemoteURL: remoteURL, instanceName: spaced}
|
sending := environment{logRemoteURL: remoteURL, instanceName: spaced}
|
||||||
|
|
||||||
_, err := config.FromEnvironment(sending.lookupEnv)
|
_, err := config.FromEnvironment(sending.viper())
|
||||||
|
|
||||||
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 ` +
|
||||||
@@ -823,7 +829,7 @@ func TestAppNameSetStopsTheStartWhileSending(t *testing.T) {
|
|||||||
logRemoteURL: remoteURL,
|
logRemoteURL: remoteURL,
|
||||||
instanceName: instance,
|
instanceName: instance,
|
||||||
logRemoteAppName: "my app",
|
logRemoteAppName: "my app",
|
||||||
}.lookupEnv)
|
}.viper())
|
||||||
|
|
||||||
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`
|
||||||
@@ -954,7 +960,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,
|
||||||
}.lookupEnv)
|
}.viper())
|
||||||
|
|
||||||
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>"
|
||||||
@@ -991,7 +997,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{alertWebhookURL: value}.viper())
|
||||||
|
|
||||||
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"
|
||||||
@@ -1042,7 +1048,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.lookupEnv)
|
_, err := config.FromEnvironment(env.viper())
|
||||||
|
|
||||||
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"
|
||||||
@@ -1059,7 +1065,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",
|
||||||
}.lookupEnv)
|
}.viper())
|
||||||
|
|
||||||
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 ` +
|
||||||
@@ -1084,7 +1090,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.lookupEnv)
|
_, err := config.FromEnvironment(env.viper())
|
||||||
|
|
||||||
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 {
|
||||||
@@ -1099,7 +1105,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",
|
||||||
}.lookupEnv)
|
}.viper())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("ru on both country lists was accepted")
|
t.Fatal("ru on both country lists was accepted")
|
||||||
}
|
}
|
||||||
@@ -1161,7 +1167,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.lookupEnv)
|
_, err := config.FromEnvironment(tc.env.viper())
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -1191,7 +1197,7 @@ func TestSettingNeedingLookupsStopsTheStartWhileTheyAreOff(t *testing.T) {
|
|||||||
|
|
||||||
_, err := config.FromEnvironment(environment{
|
_, err := config.FromEnvironment(environment{
|
||||||
lookupSource: off, name: value,
|
lookupSource: off, name: value,
|
||||||
}.lookupEnv)
|
}.viper())
|
||||||
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)
|
||||||
@@ -1341,7 +1347,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||||
|
|
||||||
want := tc.name + ": " + tc.want
|
want := tc.name + ": " + tc.want
|
||||||
if err == nil || err.Error() != want {
|
if err == nil || err.Error() != want {
|
||||||
@@ -1420,7 +1426,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||||
|
|
||||||
want := tc.name + ": " + tc.want
|
want := tc.name + ": " + tc.want
|
||||||
if err == nil || err.Error() != want {
|
if err == nil || err.Error() != want {
|
||||||
@@ -1517,7 +1523,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||||
|
|
||||||
want := tc.name + ": " + tc.want
|
want := tc.name + ": " + tc.want
|
||||||
if err == nil || err.Error() != want {
|
if err == nil || err.Error() != want {
|
||||||
@@ -1532,7 +1538,7 @@ func TestASNLimitPercentURLThatIsABlocklistStopsTheStart(t *testing.T) {
|
|||||||
|
|
||||||
_, err := config.FromEnvironment(environment{
|
_, err := config.FromEnvironment(environment{
|
||||||
blocklistURLs: dropURL + "," + asnURL, asnLimitPercentURL: asnURL,
|
blocklistURLs: dropURL + "," + asnURL, asnLimitPercentURL: asnURL,
|
||||||
}.lookupEnv)
|
}.viper())
|
||||||
|
|
||||||
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 {
|
||||||
@@ -1659,7 +1665,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||||
|
|
||||||
want := tc.name + ": " + tc.want
|
want := tc.name + ": " + tc.want
|
||||||
if err == nil || err.Error() != want {
|
if err == nil || err.Error() != want {
|
||||||
@@ -1727,7 +1733,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{dnsblZones: tc.value}.viper())
|
||||||
|
|
||||||
want := dnsblZones + ": " + tc.want
|
want := dnsblZones + ": " + tc.want
|
||||||
if err == nil || err.Error() != want {
|
if err == nil || err.Error() != want {
|
||||||
@@ -1780,7 +1786,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||||
|
|
||||||
want := tc.name + ": " + tc.want
|
want := tc.name + ": " + tc.want
|
||||||
if err == nil || err.Error() != want {
|
if err == nil || err.Error() != want {
|
||||||
@@ -1894,7 +1900,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.lookupEnv)
|
_, err := config.FromEnvironment(tc.env.viper())
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -2054,7 +2060,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{tc.name: tc.value}.viper())
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -2079,7 +2085,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{logRequestHeaders: value}.viper())
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -2096,7 +2102,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{name: value}.viper())
|
||||||
|
|
||||||
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 {
|
||||||
@@ -2152,7 +2158,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"),
|
||||||
}.lookupEnv)
|
}.viper())
|
||||||
|
|
||||||
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 {
|
||||||
@@ -2163,7 +2169,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"),
|
||||||
}.lookupEnv)
|
}.viper())
|
||||||
|
|
||||||
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`
|
||||||
@@ -2178,7 +2184,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),
|
||||||
}.lookupEnv)
|
}.viper())
|
||||||
|
|
||||||
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"
|
||||||
@@ -2193,7 +2199,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}.lookupEnv)
|
_, err := config.FromEnvironment(environment{metricsToken + "_FILE": path}.viper())
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
+11
-24
@@ -8,8 +8,9 @@ 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
|
||||||
@@ -58,13 +59,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 with lookupEnv, normally
|
// RProxyFromEnvironment reads rproxy's settings through v, normally the viper
|
||||||
// os.LookupEnv. A setting that is not set takes its default, and
|
// NewViper returns, bound to the process's environment. A setting that is not
|
||||||
// SWRP_WAF_BACKEND, which has none, must be set. A setting that is set but
|
// set takes its default, and SWRP_WAF_BACKEND, which has none, must be set. A
|
||||||
// invalid is an error that names it. Since rproxy never touches disk, its
|
// setting that is set but invalid is an error that names it. Since rproxy
|
||||||
// settings have no _FILE form.
|
// never touches disk, its settings have no _FILE form.
|
||||||
func RProxyFromEnvironment(lookupEnv func(string) (string, bool)) (*RProxy, error) {
|
func RProxyFromEnvironment(v *viper.Viper) (*RProxy, error) {
|
||||||
env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
|
env := &environment{v: v, readFiles: false}
|
||||||
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),
|
||||||
@@ -91,9 +92,9 @@ func RProxyFromEnvironment(lookupEnv func(string) (string, bool)) (*RProxy, erro
|
|||||||
// 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(
|
||||||
lookupEnv func(string) (string, bool),
|
v *viper.Viper,
|
||||||
) (string, *url.URL, error) {
|
) (string, *url.URL, error) {
|
||||||
env := &environment{lookupEnv: withoutFileForms(lookupEnv)}
|
env := &environment{v: v, readFiles: false}
|
||||||
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)
|
||||||
|
|
||||||
@@ -104,20 +105,6 @@ 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 {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,11 +34,12 @@ 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) {
|
||||||
return config.RProxyFromEnvironment(func(name string) (string, bool) {
|
v := viper.New()
|
||||||
value, ok := env[name]
|
for name, value := range env {
|
||||||
|
v.Set(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
return value, ok
|
return config.RProxyFromEnvironment(v)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRProxyDefaults(t *testing.T) {
|
func TestRProxyDefaults(t *testing.T) {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
+6
-28
@@ -16,11 +16,6 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FIXME: you can't call things just "File" - that's super vague.
|
|
||||||
|
|
||||||
// FIXME: "lookup database" - in memory on disk? you must not be so
|
|
||||||
// ambiguous when describing things.
|
|
||||||
|
|
||||||
// quietTime is how long the lookup database must go without a change
|
// quietTime is how long the lookup database must go without a change
|
||||||
// before it is read again, so that a file still being copied in is read
|
// before it is read again, so that a file still being copied in is read
|
||||||
// only once whole.
|
// only once whole.
|
||||||
@@ -138,11 +133,8 @@ func (f *File) Watch(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.params.ProcessLog.Error(
|
f.params.ProcessLog.Error("cannot watch the lookup database for replacements",
|
||||||
"cannot watch the lookup database for replacements",
|
"error", err.Error())
|
||||||
"error",
|
|
||||||
err.Error(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -202,10 +194,7 @@ func (f *File) readAgain() {
|
|||||||
f.params.Alerts.Raise(alerts.Alert{
|
f.params.Alerts.Raise(alerts.Alert{
|
||||||
Event: alerts.EventFileError,
|
Event: alerts.EventFileError,
|
||||||
Reason: kept,
|
Reason: kept,
|
||||||
Detail: map[string]any{
|
Detail: map[string]any{"file": f.params.Path, "error": err.Error()},
|
||||||
"file": f.params.Path,
|
|
||||||
"error": err.Error(),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
f.params.ProcessLog.Error(kept, "error", err.Error())
|
f.params.ProcessLog.Error(kept, "error", err.Error())
|
||||||
|
|
||||||
@@ -223,11 +212,7 @@ func (f *File) use(reader *maxminddb.Reader) {
|
|||||||
f.lastRead = f.params.Now()
|
f.lastRead = f.params.Now()
|
||||||
f.mu.Unlock()
|
f.mu.Unlock()
|
||||||
|
|
||||||
f.params.ProcessLog.Info(
|
f.params.ProcessLog.Info("read the lookup database", "file", f.params.Path)
|
||||||
"read the lookup database",
|
|
||||||
"file",
|
|
||||||
f.params.Path,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// read reads the lookup database at path. The whole file is read into
|
// read reads the lookup database at path. The whole file is read into
|
||||||
@@ -236,19 +221,12 @@ func (f *File) use(reader *maxminddb.Reader) {
|
|||||||
func read(path string) (*maxminddb.Reader, error) {
|
func read(path string) (*maxminddb.Reader, error) {
|
||||||
data, err := os.ReadFile(path) //nolint:gosec // the file the admin names
|
data, err := os.ReadFile(path) //nolint:gosec // the file the admin names
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf("SWWAF_LOOKUP_DB_PATH cannot be read: %w", err)
|
||||||
"SWWAF_LOOKUP_DB_PATH cannot be read: %w",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := maxminddb.OpenBytes(data)
|
reader, err := maxminddb.OpenBytes(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf("SWWAF_LOOKUP_DB_PATH %s is not a .mmdb file: %w", path, err)
|
||||||
"SWWAF_LOOKUP_DB_PATH %s is not a .mmdb file: %w",
|
|
||||||
path,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader, nil
|
return reader, nil
|
||||||
|
|||||||
@@ -19,51 +19,29 @@ const (
|
|||||||
// ActionForward is a request servewaf allowed, which rproxy passes to the
|
// ActionForward is a request servewaf allowed, which rproxy passes to the
|
||||||
// app.
|
// app.
|
||||||
ActionForward = "forward"
|
ActionForward = "forward"
|
||||||
|
|
||||||
//FIXME: "too_large" doesn't tell a reader what that means.
|
|
||||||
//request_header_too_large does. you must not use ambiguous terms.
|
|
||||||
|
|
||||||
// ActionTooLarge is a request over the header limits.
|
// ActionTooLarge is a request over the header limits.
|
||||||
ActionTooLarge = "too_large"
|
ActionTooLarge = "too_large"
|
||||||
|
|
||||||
// ActionRateLimited is a request refused because it took its client
|
// ActionRateLimited is a request refused because it took its client
|
||||||
// over a rate limit, which bans the client.
|
// over a rate limit, which bans the client.
|
||||||
|
|
||||||
//FIXME: better would be "client_is_rate_limited"
|
|
||||||
ActionRateLimited = "rate_limited"
|
ActionRateLimited = "rate_limited"
|
||||||
|
|
||||||
// ActionBanned is a request refused because a ban covers its client,
|
// ActionBanned is a request refused because a ban covers its client,
|
||||||
// or because it matched a ban rule, asked for a trap path or the
|
// or because it matched a ban rule, asked for a trap path or the
|
||||||
// CrowdSec decision list lists its client, each of which bans the
|
// CrowdSec decision list lists its client, each of which bans the
|
||||||
// client.
|
// client.
|
||||||
|
|
||||||
// FIXME: if I saw this, i would assume they are permanently banned. is that
|
|
||||||
// true? if not, there should be TemporarilyBanned and
|
|
||||||
// PermanentlyBanned
|
|
||||||
ActionBanned = "banned"
|
ActionBanned = "banned"
|
||||||
|
|
||||||
// ActionRuleBlocked is a request refused because it matched a block
|
// ActionRuleBlocked is a request refused because it matched a block
|
||||||
// rule.
|
// rule.
|
||||||
|
|
||||||
// FIXME: "blocked_by_block_rule"
|
|
||||||
ActionRuleBlocked = "rule_blocked"
|
ActionRuleBlocked = "rule_blocked"
|
||||||
|
|
||||||
// ActionWAFBlocked is a request refused because the Core Rule Set
|
// ActionWAFBlocked is a request refused because the Core Rule Set
|
||||||
// scored it at or over SWWAF_WAF_ANOMALY_THRESHOLD.
|
// scored it at or over SWWAF_WAF_ANOMALY_THRESHOLD.
|
||||||
ActionWAFBlocked = "waf_blocked"
|
ActionWAFBlocked = "waf_blocked"
|
||||||
|
|
||||||
// ActionDenied is a request refused because its client is in
|
// ActionDenied is a request refused because its client is in
|
||||||
// SWWAF_DENY_NETS, in a blocklist while SWWAF_BLOCKLIST_ACTION is deny,
|
// SWWAF_DENY_NETS, in a blocklist while SWWAF_BLOCKLIST_ACTION is deny,
|
||||||
// or listed by a DNSBL zone, or scored a hit by AbuseIPDB, while
|
// or listed by a DNSBL zone, or scored a hit by AbuseIPDB, while
|
||||||
// SWWAF_REPUTATION_ACTION is deny.
|
// SWWAF_REPUTATION_ACTION is deny.
|
||||||
ActionDenied = "denied"
|
ActionDenied = "denied"
|
||||||
|
|
||||||
// ActionCountryDenied is a request refused for its client's country.
|
// ActionCountryDenied is a request refused for its client's country.
|
||||||
ActionCountryDenied = "country_denied"
|
ActionCountryDenied = "country_denied"
|
||||||
|
|
||||||
// FIXME: you have country_denied and denied. why isn't it
|
|
||||||
// client_denied? ALWAYS BE CONSISTENT AND UNAMBIGUOUS
|
|
||||||
|
|
||||||
// ActionAdmin is a request servewaf answered at one of its own endpoints,
|
// ActionAdmin is a request servewaf answered at one of its own endpoints,
|
||||||
// or a question about a path under /_smallwebwaf/.
|
// or a question about a path under /_smallwebwaf/.
|
||||||
ActionAdmin = "admin"
|
ActionAdmin = "admin"
|
||||||
@@ -267,11 +245,7 @@ func FormatTime(t time.Time) string {
|
|||||||
|
|
||||||
// Milliseconds is d in milliseconds, to the microsecond.
|
// Milliseconds is d in milliseconds, to the microsecond.
|
||||||
func Milliseconds(d time.Duration) float64 {
|
func Milliseconds(d time.Duration) float64 {
|
||||||
return float64(
|
return float64(d.Microseconds()) / float64(time.Millisecond/time.Microsecond)
|
||||||
d.Microseconds(),
|
|
||||||
) / float64(
|
|
||||||
time.Millisecond/time.Microsecond,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProcessLogger returns the logger for the process's own messages:
|
// NewProcessLogger returns the logger for the process's own messages:
|
||||||
@@ -279,25 +253,17 @@ func Milliseconds(d time.Duration) float64 {
|
|||||||
// as a request line's, and instanceName, SWWAF_INSTANCE_NAME, as instance.
|
// as a request line's, and instanceName, SWWAF_INSTANCE_NAME, as instance.
|
||||||
// It writes only the messages at level, SWWAF_LOG_LEVEL, or more severe;
|
// It writes only the messages at level, SWWAF_LOG_LEVEL, or more severe;
|
||||||
// the request lines Write writes are never held back.
|
// the request lines Write writes are never held back.
|
||||||
func NewProcessLogger(
|
func NewProcessLogger(w io.Writer, instanceName string, level slog.Level) *slog.Logger {
|
||||||
w io.Writer,
|
|
||||||
instanceName string,
|
|
||||||
level slog.Level,
|
|
||||||
) *slog.Logger {
|
|
||||||
handler := slog.NewJSONHandler(w, &slog.HandlerOptions{
|
handler := slog.NewJSONHandler(w, &slog.HandlerOptions{
|
||||||
Level: level,
|
Level: level,
|
||||||
ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
|
ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
|
||||||
if attr.Key == slog.TimeKey && len(groups) == 0 {
|
if attr.Key == slog.TimeKey && len(groups) == 0 {
|
||||||
return slog.String(
|
return slog.String(slog.TimeKey, FormatTime(attr.Value.Time()))
|
||||||
slog.TimeKey,
|
|
||||||
FormatTime(attr.Value.Time()),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return attr
|
return attr
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return slog.New(handler).
|
return slog.New(handler).With("type", "process", "instance", instanceName)
|
||||||
With("type", "process", "instance", instanceName)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,9 @@ func announcedLength(size int) string {
|
|||||||
func TestServerHasTheFixedLimits(t *testing.T) {
|
func TestServerHasTheFixedLimits(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
|
cfg, err := config.RProxyFromEnvironment(viperFrom(map[string]string{
|
||||||
if name == "SWRP_WAF_BACKEND" {
|
"SWRP_WAF_BACKEND": nowhere,
|
||||||
return nowhere, true
|
}))
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("settings: %v", err)
|
t.Fatalf("settings: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ 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"
|
||||||
@@ -341,6 +343,17 @@ 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(
|
||||||
@@ -354,11 +367,7 @@ func startServerLogging(
|
|||||||
}
|
}
|
||||||
maps.Copy(settings, env)
|
maps.Copy(settings, env)
|
||||||
|
|
||||||
cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
|
cfg, err := config.RProxyFromEnvironment(viperFrom(settings))
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ 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"
|
||||||
@@ -347,6 +349,17 @@ 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 {
|
||||||
@@ -359,11 +372,7 @@ func startRProxy(
|
|||||||
}
|
}
|
||||||
maps.Copy(settings, env)
|
maps.Copy(settings, env)
|
||||||
|
|
||||||
cfg, err := config.RProxyFromEnvironment(func(name string) (string, bool) {
|
cfg, err := config.RProxyFromEnvironment(viperFrom(settings))
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -423,11 +432,7 @@ func newServeWAFWith(
|
|||||||
|
|
||||||
maps.Copy(settings, env)
|
maps.Copy(settings, env)
|
||||||
|
|
||||||
cfg, err := config.FromEnvironment(func(name string) (string, bool) {
|
cfg, err := config.FromEnvironment(viperFrom(settings))
|
||||||
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,6 +10,8 @@ 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"
|
||||||
)
|
)
|
||||||
@@ -31,7 +33,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, lookupEnv func(string) (string, bool),
|
ctx context.Context, args []string, v *viper.Viper,
|
||||||
stderr io.Writer,
|
stderr io.Writer,
|
||||||
) int {
|
) int {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
@@ -44,15 +46,15 @@ func HealthCheck(
|
|||||||
ctx, cancel := context.WithTimeout(ctx, healthCheckTimeout)
|
ctx, cancel := context.WithTimeout(ctx, healthCheckTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
command, err := config.Command(lookupEnv)
|
command, err := config.Command(v)
|
||||||
|
|
||||||
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, lookupEnv)
|
err = checkServeWAF(ctx, v)
|
||||||
default:
|
default:
|
||||||
err = checkRProxyAndApp(ctx, lookupEnv)
|
err = checkRProxyAndApp(ctx, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -65,8 +67,8 @@ func HealthCheck(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkServeWAF asks servewaf's health endpoint.
|
// checkServeWAF asks servewaf's health endpoint.
|
||||||
func checkServeWAF(ctx context.Context, lookupEnv func(string) (string, bool)) error {
|
func checkServeWAF(ctx context.Context, v *viper.Viper) error {
|
||||||
listenAddr, err := config.ServeWAFListenAddr(lookupEnv)
|
listenAddr, err := config.ServeWAFListenAddr(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid setting: %w", err)
|
return fmt.Errorf("invalid setting: %w", err)
|
||||||
}
|
}
|
||||||
@@ -81,9 +83,9 @@ func checkServeWAF(ctx context.Context, lookupEnv func(string) (string, bool)) e
|
|||||||
|
|
||||||
// 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, lookupEnv func(string) (string, bool),
|
ctx context.Context, v *viper.Viper,
|
||||||
) error {
|
) error {
|
||||||
listenAddr, upstreamURL, err := config.RProxyListenAddrAndUpstreamURL(lookupEnv)
|
listenAddr, upstreamURL, err := config.RProxyListenAddrAndUpstreamURL(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid setting: %w", err)
|
return fmt.Errorf("invalid setting: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,11 +145,7 @@ func TestHealthCheckRefusesAnArgument(t *testing.T) {
|
|||||||
|
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
noSettings := func(string) (string, bool) {
|
got := smallwebwaf.HealthCheck(t.Context(), []string{"now"}, viperFrom(nil), &stderr)
|
||||||
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 {
|
||||||
@@ -166,11 +162,7 @@ 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, func(name string) (string, bool) {
|
got := smallwebwaf.HealthCheck(t.Context(), nil, viperFrom(env), &stderr)
|
||||||
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) ||
|
||||||
|
|||||||
@@ -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.LookupEnv)
|
cfg, err := config.RProxyFromEnvironment(params.Viper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
processLog.Error("invalid setting", "error", err.Error())
|
processLog.Error("invalid setting", "error", err.Error())
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,7 @@ 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,
|
||||||
LookupEnv: func(name string) (string, bool) {
|
Viper: viperFrom(env),
|
||||||
value, ok := env[name]
|
|
||||||
|
|
||||||
return value, ok
|
|
||||||
},
|
|
||||||
Stdout: out,
|
Stdout: out,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ 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"
|
||||||
@@ -58,8 +60,9 @@ 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
|
||||||
// LookupEnv reads an environment variable, normally os.LookupEnv.
|
// Viper reads the settings, normally the viper config.NewViper returns
|
||||||
LookupEnv func(string) (string, bool)
|
// for the process's environment.
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -78,7 +81,7 @@ func Main(version string) int {
|
|||||||
syscall.SIGTERM, os.Interrupt)
|
syscall.SIGTERM, os.Interrupt)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
params := Params{Version: version, LookupEnv: os.LookupEnv, Stdout: os.Stdout}
|
params := Params{Version: version, Viper: config.NewViper(), Stdout: os.Stdout}
|
||||||
|
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "rproxy":
|
case "rproxy":
|
||||||
@@ -86,7 +89,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:], os.LookupEnv, os.Stderr)
|
return HealthCheck(ctx, args[1:], params.Viper, os.Stderr)
|
||||||
default:
|
default:
|
||||||
return printUsage(os.Stderr)
|
return printUsage(os.Stderr)
|
||||||
}
|
}
|
||||||
@@ -106,9 +109,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.LookupEnv), slog.LevelError)
|
config.InstanceName(params.Viper), slog.LevelError)
|
||||||
|
|
||||||
cfg, err := config.FromEnvironment(params.LookupEnv)
|
cfg, err := config.FromEnvironment(params.Viper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
processLog.Error("invalid setting", "error", err.Error())
|
processLog.Error("invalid setting", "error", err.Error())
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ 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"
|
||||||
@@ -122,20 +124,29 @@ 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 {
|
||||||
return smallwebwaf.RunServeWAF(ctx, smallwebwaf.Params{
|
v := viperFrom(env)
|
||||||
Version: testVersion,
|
if _, set := env["SWWAF_LOOKUP_SOURCE"]; !set {
|
||||||
LookupEnv: func(name string) (string, bool) {
|
v.Set("SWWAF_LOOKUP_SOURCE", "off")
|
||||||
value, ok := env[name]
|
|
||||||
if !ok && name == "SWWAF_LOOKUP_SOURCE" {
|
|
||||||
return "off", true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return value, ok
|
return smallwebwaf.RunServeWAF(ctx, smallwebwaf.Params{
|
||||||
},
|
Version: testVersion,
|
||||||
|
Viper: v,
|
||||||
Stdout: out,
|
Stdout: out,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/publish: build the image on the Docker host and push it to the
|
|
||||||
# registry under four tags: the UTC date (YYYY-MM-DD), the short commit
|
|
||||||
# hash, the branch name and latest. The build is script/docker's, so the
|
|
||||||
# tests and the linter run first. The host and the registry name are
|
|
||||||
# environment variables with defaults, for running it against another.
|
|
||||||
#
|
|
||||||
# The working tree must be clean: the hash tag names a commit, and an
|
|
||||||
# image built from uncommitted changes would carry a hash that does not
|
|
||||||
# describe it.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
||||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
|
||||||
|
|
||||||
: "${DOCKER_HOST:=ssh://root@fsn1app1}"
|
|
||||||
: "${PUBLISH_IMAGE:=git.eeqj.de/sneak/smallwebwaf}"
|
|
||||||
export DOCKER_HOST
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
|
||||||
echo "script/publish: the working tree is not clean; commit or stash first" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
date="$(date -u +%Y-%m-%d)"
|
|
||||||
hash="$(git rev-parse --short HEAD)"
|
|
||||||
branch="$(git rev-parse --abbrev-ref HEAD)"
|
|
||||||
if [ "$branch" = "HEAD" ]; then
|
|
||||||
echo "script/publish: detached HEAD; check out a branch first" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# A tag is [A-Za-z0-9_.-]; a branch name such as feature/x is not.
|
|
||||||
branch="$(printf '%s' "$branch" | tr -c 'A-Za-z0-9_.-' '-')"
|
|
||||||
|
|
||||||
local_tag="$("$SCRIPT_DIR/projectname")"
|
|
||||||
"$SCRIPT_DIR/docker"
|
|
||||||
|
|
||||||
for tag in "$date" "$hash" "$branch" latest; do
|
|
||||||
docker tag "$local_tag" "$PUBLISH_IMAGE:$tag"
|
|
||||||
done
|
|
||||||
for tag in "$date" "$hash" "$branch" latest; do
|
|
||||||
docker push "$PUBLISH_IMAGE:$tag"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "pushed $PUBLISH_IMAGE with tags: $date $hash $branch latest"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
Reference in New Issue
Block a user