check / check (push) Failing after 3m27s
Every setting of discriminator and rproxy is read through viper, in place of the os.LookupEnv function Main passed down. NewViper turns AutomaticEnv and AllowEmptyEnv on, so a setting that is set but empty reads as set. A setting may also be given as a file, the _FILE form, which rproxy's settings now have too; rproxy still writes nothing to disk. The settings are renamed. Discriminator's take the prefix SWW_WAF_, rproxy's take SWW_RPROXY_, and the shared SWW_COMMAND keeps SWW_. The Core Rule Set's settings take SWW_WAF_CORE_RULE_SET_ rather than SWW_WAF_WAF_. Every place a name appeared changed: the code, the viper keys, tests, documents, examples, the README's settings table and the image files. Model: deepseek-v4-pro
173 lines
5.3 KiB
Go
173 lines
5.3 KiB
Go
package discriminator_test
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
)
|
|
|
|
// The rate limits count an IPv6 client by its /64, so these two addresses
|
|
// are one client for them. The static lists match each address on its own,
|
|
// and the tests list listedAddr alone.
|
|
const (
|
|
listedAddr = "2001:db8::1"
|
|
unlistedAddr = "2001:db8::2"
|
|
)
|
|
|
|
func TestAllowNetsSkipEveryCheck(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var calls atomic.Int32
|
|
|
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
|
calls.Add(1)
|
|
})
|
|
geojsURL, asked := startGeoJS(t)
|
|
// fromKP is in SWW_WAF_ALLOW_NETS, and in SWW_WAF_DENY_NETS too, which
|
|
// comes after it.
|
|
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
|
|
trustedProxies: trustLocalhost,
|
|
allowNets: "198.51.100.0/24",
|
|
denyNets: fromKP,
|
|
deniedCountries: "kp",
|
|
rateLimitPerMinute: "1",
|
|
})
|
|
|
|
// Neither SWW_WAF_DENY_NETS, the country lists nor the limit of one
|
|
// request a minute refuses the client, and its country is not looked
|
|
// up.
|
|
wantAnswers(t, addr, out, []sentRequest{
|
|
{fromKP, http.StatusOK, requestlog.ActionForward},
|
|
{fromKP, http.StatusOK, requestlog.ActionForward},
|
|
})
|
|
|
|
if len(asked()) != 0 {
|
|
t.Errorf("GeoJS was asked about %v, want nothing", asked())
|
|
}
|
|
|
|
if calls.Load() != 2 {
|
|
t.Errorf("the app was called %d times, want 2", calls.Load())
|
|
}
|
|
}
|
|
|
|
func TestRequestFromAllowNetsIsNotCounted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
|
addr, out := startProxy(t, app.URL, map[string]string{
|
|
trustedProxies: trustLocalhost,
|
|
allowNets: listedAddr,
|
|
rateLimitPerMinute: "1",
|
|
})
|
|
|
|
// listedAddr's requests are not counted, so the first request from
|
|
// unlistedAddr is within the limit of one a minute.
|
|
wantAnswers(t, addr, out, []sentRequest{
|
|
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
|
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
|
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
|
|
{unlistedAddr, http.StatusForbidden, requestlog.ActionRateLimited},
|
|
})
|
|
}
|
|
|
|
func TestDenyNetsRefuseBeforeTheLookup(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var calls atomic.Int32
|
|
|
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
|
calls.Add(1)
|
|
})
|
|
geojsURL, asked := startGeoJS(t)
|
|
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
|
|
trustedProxies: trustLocalhost,
|
|
denyNets: "203.0.113.0/24",
|
|
deniedCountries: "kp",
|
|
})
|
|
|
|
req := newRequest(t, http.MethodPost, addr, "/", strings.NewReader("a body"))
|
|
req.Header.Set(forwardedFor, fromDE)
|
|
wantStatus(t, do(t, req), http.StatusForbidden)
|
|
|
|
line := out.endedLine(t)
|
|
wantLine(t, line, http.StatusForbidden, requestlog.ActionDenied)
|
|
|
|
if len(asked()) != 0 {
|
|
t.Errorf("GeoJS was asked about %v, want nothing", asked())
|
|
}
|
|
|
|
if calls.Load() != 0 {
|
|
t.Errorf("the app was called %d times, want none", calls.Load())
|
|
}
|
|
}
|
|
|
|
func TestRequestRefusedByDenyNetsIsNotCounted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
|
addr, out := startProxy(t, app.URL, map[string]string{
|
|
trustedProxies: trustLocalhost,
|
|
denyNets: listedAddr,
|
|
rateLimitPerMinute: "1",
|
|
})
|
|
|
|
// listedAddr's refused requests are not counted, so the first request
|
|
// from unlistedAddr is within the limit of one a minute.
|
|
wantAnswers(t, addr, out, []sentRequest{
|
|
{listedAddr, http.StatusForbidden, requestlog.ActionDenied},
|
|
{listedAddr, http.StatusForbidden, requestlog.ActionDenied},
|
|
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
|
|
{unlistedAddr, http.StatusForbidden, requestlog.ActionRateLimited},
|
|
})
|
|
}
|
|
|
|
func TestRateLimitExemptNetsAreNeitherCountedNorRefused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
|
geojsURL, _ := startGeoJS(t)
|
|
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
|
|
trustedProxies: trustLocalhost,
|
|
lookupTimeout: "1h",
|
|
rateLimitExemptNets: listedAddr + "," + fromKP,
|
|
deniedCountries: "kp",
|
|
rateLimitPerMinute: "1",
|
|
})
|
|
|
|
// listedAddr's requests are neither refused nor counted, so the first
|
|
// request from unlistedAddr is within the limit of one a minute. The
|
|
// country lists still refuse an exempt client.
|
|
wantAnswers(t, addr, out, []sentRequest{
|
|
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
|
{listedAddr, http.StatusOK, requestlog.ActionForward},
|
|
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
|
|
{unlistedAddr, http.StatusForbidden, requestlog.ActionRateLimited},
|
|
{fromKP, http.StatusForbidden, requestlog.ActionCountryDenied},
|
|
})
|
|
}
|
|
|
|
// sentRequest is a GET request from client, as X-Forwarded-For names it,
|
|
// and the status and log line action it should get.
|
|
type sentRequest struct {
|
|
client string
|
|
status int
|
|
action string
|
|
}
|
|
|
|
// wantAnswers sends requests to sww at addr one after another and
|
|
// checks each one's answer and log line. They must be the first requests
|
|
// sww is sent, since the log lines are matched to them in order.
|
|
func wantAnswers(t *testing.T, addr string, out *output, requests []sentRequest) {
|
|
t.Helper()
|
|
|
|
for i, sent := range requests {
|
|
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)
|
|
req.Header.Set(forwardedFor, sent.client)
|
|
wantStatus(t, do(t, req), sent.status)
|
|
wantLine(t, out.endedLines(t, i+1)[i], sent.status, sent.action)
|
|
}
|
|
}
|