check / check (push) Waiting to run
The package passes nothing to an app since rproxy took that over, so it is named after the command it serves. Its constructor is New, like rproxy's. The rproxy tests call their stand-in for servewaf standIn, since a local servewaf would hide the package. No change of behaviour. Model: opus-5-5
141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
package servewaf_test
|
|
|
|
import (
|
|
"net/http"
|
|
"net/netip"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
)
|
|
|
|
// observe is the value of SWWAF_MODE for observe mode.
|
|
const observe = "observe"
|
|
|
|
func TestObserveModeForwardsWhatEnforceModeRefuses(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
denied = "192.0.2.50" // in SWWAF_DENY_NETS
|
|
banned = otherClient // under a ban read from bans.json
|
|
)
|
|
|
|
for _, tc := range []struct {
|
|
setting string // "" leaves SWWAF_MODE at its default
|
|
observe bool
|
|
}{
|
|
{"", false},
|
|
{"enforce", false},
|
|
{observe, true},
|
|
} {
|
|
t.Run(mode+"="+tc.setting, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
geojsURL, _ := startGeoJS(t)
|
|
env := map[string]string{
|
|
lookupTimeout: "1h",
|
|
rateLimitPerMinute: "1",
|
|
denyNets: denied,
|
|
deniedCountries: "kp",
|
|
}
|
|
|
|
if tc.setting != "" {
|
|
env[mode] = tc.setting
|
|
}
|
|
|
|
s, clk, server := startWithClock(t, geojsURL, env)
|
|
server.Ledger.Load([]bans.Ban{{
|
|
Netblock: netip.MustParsePrefix(banned + "/32"),
|
|
Start: clk.Now(),
|
|
Expires: clk.Now().Add(time.Hour),
|
|
}})
|
|
|
|
// fromDE's first request is within the limit of one a minute,
|
|
// and its second breaks it.
|
|
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
|
|
|
for _, sent := range []struct{ from, refusal string }{
|
|
{denied, requestlog.ActionDenied},
|
|
{banned, requestlog.ActionBanned},
|
|
{fromKP, requestlog.ActionCountryDenied},
|
|
{fromDE, requestlog.ActionRateLimited},
|
|
} {
|
|
if !tc.observe {
|
|
line := s.get(sent.from, http.StatusForbidden, sent.refusal)
|
|
wantWouldAction(t, line, "")
|
|
|
|
continue
|
|
}
|
|
|
|
// Passed to the app, which answered it.
|
|
line := s.get(sent.from, http.StatusOK, requestlog.ActionForward)
|
|
wantWouldAction(t, line, sent.refusal)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestObserveModeMakesNoBanAndKeepsTheBansItHas(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s, clk, server := startWithClock(t, "", map[string]string{
|
|
mode: observe,
|
|
rateLimitPerMinute: "1",
|
|
})
|
|
kept := bans.Ban{
|
|
Netblock: netip.MustParsePrefix(otherClient + "/32"),
|
|
Start: clk.Now(),
|
|
Expires: clk.Now().Add(time.Hour),
|
|
Cause: bans.CauseAdmin,
|
|
}
|
|
server.Ledger.Load([]bans.Ban{kept})
|
|
|
|
// No ban sets client's counters back to zero, so each request after
|
|
// the first breaks the limit of one a minute.
|
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
|
|
|
for range 2 {
|
|
line := s.get(client, http.StatusOK, requestlog.ActionForward)
|
|
wantWouldAction(t, line, requestlog.ActionRateLimited)
|
|
|
|
if line.LimitHit != minute || line.Offence != requestlog.OffenceLimit ||
|
|
line.BanExpires != "" {
|
|
t.Errorf("log line has limit_hit %q, offence %q and ban_expires %q, "+
|
|
"want minute, limit and none", line.LimitHit, line.Offence,
|
|
line.BanExpires)
|
|
}
|
|
}
|
|
|
|
// The ban read from bans.json refuses nothing, and so counts no
|
|
// refusal in its notes, but is kept.
|
|
line := s.get(otherClient, http.StatusOK, requestlog.ActionForward)
|
|
wantWouldAction(t, line, requestlog.ActionBanned)
|
|
|
|
if line.BanExpires != requestlog.FormatTime(kept.Expires) {
|
|
t.Errorf("log line has ban_expires %q, want %s", line.BanExpires,
|
|
requestlog.FormatTime(kept.Expires))
|
|
}
|
|
|
|
got := server.Ledger.Snapshot()
|
|
if len(got) != 1 || !reflect.DeepEqual(got[0], kept) {
|
|
t.Errorf("bans\n%+v\nwant only\n%+v", got, kept)
|
|
}
|
|
}
|
|
|
|
// wantWouldAction checks the request log line's would_action, and that a
|
|
// line that should have none has no such field.
|
|
func wantWouldAction(t *testing.T, line logLine, want string) {
|
|
t.Helper()
|
|
|
|
got, present := line.fields["would_action"]
|
|
|
|
switch {
|
|
case want == "" && present:
|
|
t.Errorf("log line has would_action %v, want none", got)
|
|
case want != "" && got != want:
|
|
t.Errorf("log line has would_action %v, want %s", got, want)
|
|
}
|
|
}
|