check / check (push) Waiting to run
GET /_smallwebwaf/metrics answers in the Prometheus text format for a request carrying SWWAF_METRICS_TOKEN, 401 without it and 404 while it is unset. Every request under /_smallwebwaf/ but the health check now goes through the checks and is answered where it would be forwarded, 404 for any path but the metrics, so none reaches the app. In the client's history a 401 counts as refused, the metrics and the 404s as neither. SWWAF_METRICS_TOP_N bounds the series by country, the rest counted as other. Deviation: go.mod and go.sum written by hand, as go runs only through make. Deviation: no metrics yet for state files read again after an edit or edits set aside; that work is not merged. Model: opus-5-5
120 lines
2.9 KiB
Go
120 lines
2.9 KiB
Go
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", Refused: true, Status: 403, ResponseBytes: 10, BrokeLimit: true},
|
|
{Forwarded: true, Status: 502, ResponseBytes: 12},
|
|
// Closed without an answer: refused, and no response.
|
|
{Refused: true, Status: 0},
|
|
// Answered 404 at smallwebwaf's own endpoints: neither forwarded
|
|
// nor refused.
|
|
{Status: 404},
|
|
} {
|
|
limiter.AddToHistory(client, start.Add(time.Duration(i)*time.Minute), r)
|
|
}
|
|
|
|
want := ratelimit.History{
|
|
FirstSeen: start,
|
|
LastSeen: start.Add(6 * time.Minute),
|
|
Country: "FR",
|
|
LookedUp: start.Add(3 * time.Minute),
|
|
Requests: 7,
|
|
Forwarded: 4,
|
|
Refused: 2,
|
|
RequestBytes: 15,
|
|
ResponseBytes: 122,
|
|
Responses: ratelimit.Responses{
|
|
Status1xx: 1, Status2xx: 1, Status3xx: 1, Status4xx: 2, 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{}
|
|
}
|