Keep the bans, the clients and GeoJS's answers in state files (closes #17)
check / check (push) Successful in 3m24s
check / check (push) Successful in 3m24s
smallwebwaf now copies its state to bans.json, clients.json and lookups.json in SWWAF_STATE_DIR, as "Persistent state" in SPEC.md describes, and reads them back at start, so a restart lifts no ban and gives no client a fresh allowance. Each client gains a history, and a ban's notes count the netblock's requests. bans.json is written SWWAF_STATE_WRITE_DELAY after a ban, and every file every SWWAF_STATE_COUNTER_INTERVAL and at the stop. A ban read back is masked to its netblock and refuses every client in it. A file that does not parse, an unknown version, an entry without a field it needs, or an unwritable directory stops the start. Deviation: no AS number or name, and no ban cause, reason or lifting yet. Model: opus-5-5
This commit was merged in pull request #72.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
package ratelimit_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||
)
|
||||
|
||||
func TestHistoryKeepsEveryRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
limiter := ratelimit.New(ratelimit.Limits{})
|
||||
client := netip.MustParsePrefix("203.0.113.9/32")
|
||||
start := midnight()
|
||||
|
||||
for i, r := range []ratelimit.Request{
|
||||
{Country: "DE", Forwarded: true, Status: 200, RequestBytes: 10, ResponseBytes: 100},
|
||||
{Forwarded: true, Status: 101},
|
||||
{Forwarded: true, Status: 304, RequestBytes: 5},
|
||||
{Country: "FR", Status: 403, ResponseBytes: 10, BrokeLimit: true},
|
||||
{Forwarded: true, Status: 502, ResponseBytes: 12},
|
||||
// Closed without an answer: refused, and no response.
|
||||
{Status: 0},
|
||||
} {
|
||||
limiter.AddToHistory(client, start.Add(time.Duration(i)*time.Minute), r)
|
||||
}
|
||||
|
||||
want := ratelimit.History{
|
||||
FirstSeen: start,
|
||||
LastSeen: start.Add(5 * time.Minute),
|
||||
Country: "FR",
|
||||
LookedUp: start.Add(3 * time.Minute),
|
||||
Requests: 6,
|
||||
Forwarded: 4,
|
||||
Refused: 2,
|
||||
RequestBytes: 15,
|
||||
ResponseBytes: 122,
|
||||
Responses: ratelimit.Responses{
|
||||
Status1xx: 1, Status2xx: 1, Status3xx: 1, Status4xx: 1, Status5xx: 1,
|
||||
},
|
||||
Offences: ratelimit.Offences{Limit: 1},
|
||||
}
|
||||
|
||||
got := historyOf(t, limiter, client)
|
||||
if got != want {
|
||||
t.Errorf("history\n%+v\nwant\n%+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetKeepsTheHistory(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
limiter := ratelimit.New(ratelimit.Limits{PerMinute: limit})
|
||||
client := netip.MustParsePrefix("203.0.113.9/32")
|
||||
start := midnight()
|
||||
|
||||
for range limit {
|
||||
wantCount(t, limiter, client, start, "")
|
||||
limiter.AddToHistory(client, start, ratelimit.Request{Forwarded: true})
|
||||
}
|
||||
|
||||
limiter.Reset(client)
|
||||
|
||||
if got := historyOf(t, limiter, client).Requests; got != limit {
|
||||
t.Errorf("the history counts %d requests, want %d", got, limit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestsAddsUpTheClientsInsideTheNetblock(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
limiter := ratelimit.New(ratelimit.Limits{})
|
||||
|
||||
for client, requests := range map[string]int{
|
||||
"198.51.100.9/32": 2,
|
||||
"198.51.100.10/32": 3,
|
||||
"192.0.2.1/32": 5,
|
||||
"2001:db8:5::/64": 7,
|
||||
} {
|
||||
for range requests {
|
||||
limiter.AddToHistory(netip.MustParsePrefix(client), midnight(),
|
||||
ratelimit.Request{})
|
||||
}
|
||||
}
|
||||
|
||||
for netblock, want := range map[string]int64{
|
||||
"198.51.100.9/32": 2,
|
||||
"198.51.100.0/24": 5,
|
||||
"2001:db8:5::/64": 7,
|
||||
"203.0.113.0/24": 0,
|
||||
} {
|
||||
got := limiter.Requests(netip.MustParsePrefix(netblock))
|
||||
if got != want {
|
||||
t.Errorf("%s has sent %d requests, want %d", netblock, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// historyOf returns client's history.
|
||||
func historyOf(
|
||||
t *testing.T, limiter *ratelimit.Limiter, client netip.Prefix,
|
||||
) ratelimit.History {
|
||||
t.Helper()
|
||||
|
||||
for _, c := range limiter.Snapshot() {
|
||||
if c.Client == client {
|
||||
return c.History
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("%s is not in the table", client)
|
||||
|
||||
return ratelimit.History{}
|
||||
}
|
||||
Reference in New Issue
Block a user