check / check (push) Waiting to run
The IPv6 group that is one client, the size of the table of clients and the level of the process's own lines become settings. clientGroup reads the group length from them, so limits, bans, history, lookups, AbuseIPDB scores and per-client anomaly counters all follow it; ratelimit.New takes the table size; the process logger takes the level once the settings are read, and request lines, written apart from it, are never held back. Judgement call: SWWAF_IPV6_GROUP_PREFIX accepts 32 to 128, the issue's example range. Model: opus-5-5
147 lines
4.4 KiB
Go
147 lines
4.4 KiB
Go
package proxy_test
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/proxy"
|
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
)
|
|
|
|
func TestHistoryKeepsEachRequestOfTheClient(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
geojsURL, _ := startGeoJS(t)
|
|
s, clk, server := startWithClock(t, geojsURL, map[string]string{
|
|
rateLimitPerMinute: "2",
|
|
deniedCountries: "kp",
|
|
})
|
|
start := clk.Now()
|
|
|
|
// Two let through, one over the limit, which bans the client, and one
|
|
// refused under that ban, for which the client is not looked up. GeoJS
|
|
// answers about the client at its first request, and its later ones
|
|
// use that answer.
|
|
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
|
clk.advance(time.Second)
|
|
s.get(fromDE, http.StatusOK, requestlog.ActionForward)
|
|
s.get(fromDE, http.StatusForbidden, requestlog.ActionRateLimited)
|
|
clk.advance(time.Second)
|
|
s.get(fromDE, http.StatusForbidden, requestlog.ActionBanned)
|
|
|
|
want := ratelimit.History{
|
|
FirstSeen: start,
|
|
LastSeen: start.Add(2 * time.Second),
|
|
ASN: asnDE,
|
|
ASName: asNameDE,
|
|
Country: "DE",
|
|
LookedUp: start,
|
|
Requests: 4,
|
|
Forwarded: 2,
|
|
Refused: 2,
|
|
// The app answers with no body, smallwebwaf with its status text.
|
|
ResponseBytes: 2 * int64(len("Forbidden\n")),
|
|
Responses: ratelimit.Responses{Status2xx: 2, Status4xx: 2},
|
|
Offences: ratelimit.Offences{Limit: 1},
|
|
}
|
|
|
|
got := historyOf(t, server, fromDE)
|
|
if got != want {
|
|
t.Errorf("history\n%+v\nwant\n%+v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestTableOfClientsHoldsAtMostMaxTrackedClients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s, _, server := startWithClock(t, "", map[string]string{maxTrackedClients: "2"})
|
|
|
|
// The third client drops the least recently seen, the first, with its
|
|
// history.
|
|
for _, from := range []string{"192.0.2.1", "192.0.2.2", "192.0.2.3"} {
|
|
s.get(from, http.StatusOK, requestlog.ActionForward)
|
|
}
|
|
|
|
_, held := server.Limiter.Client(netip.MustParsePrefix("192.0.2.1/32"))
|
|
if server.Limiter.Len() != 2 || held {
|
|
t.Errorf("the table holds %d clients, the first among them: %t; want 2, "+
|
|
"without it", server.Limiter.Len(), held)
|
|
}
|
|
}
|
|
|
|
func TestHistoryCountsTheBodiesEachWay(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = io.Copy(io.Discard, r.Body)
|
|
_, _ = io.WriteString(w, "hello")
|
|
})
|
|
addr, out, server := startProxyWithClock(t, app.URL, "", time.Now, nil)
|
|
|
|
got := do(t, newRequest(t, http.MethodPost, addr, "/", strings.NewReader("abc")))
|
|
wantStatus(t, got, http.StatusOK)
|
|
out.requestLine(t)
|
|
|
|
history := historyOf(t, server, localhost)
|
|
if history.RequestBytes != 3 || history.ResponseBytes != 5 {
|
|
t.Errorf("history counts %d bytes in and %d out, want 3 and 5",
|
|
history.RequestBytes, history.ResponseBytes)
|
|
}
|
|
}
|
|
|
|
func TestHealthEndpointIsNotInTheHistory(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
|
addr, out, server := startProxyWithClock(t, app.URL, "", time.Now, nil)
|
|
|
|
wantStatus(t, get(t, addr, proxy.HealthPath), http.StatusOK)
|
|
out.requestLine(t)
|
|
|
|
if clients := server.Limiter.Snapshot(); len(clients) != 0 {
|
|
t.Errorf("the table holds %+v, want no client", clients)
|
|
}
|
|
}
|
|
|
|
func TestRequestForSmallwebwafIsRefusedOnlyWithoutTheToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
|
addr, out, server := startProxyWithClock(t, app.URL, "", time.Now,
|
|
map[string]string{metricsToken: token})
|
|
|
|
// The metrics and the 404 are neither forwarded nor refused; the 401
|
|
// is refused.
|
|
scrape(t, addr)
|
|
wantStatus(t, get(t, addr, "/_smallwebwaf/nothing"), http.StatusNotFound)
|
|
wantStatus(t, get(t, addr, proxy.MetricsPath), http.StatusUnauthorized)
|
|
out.requestLines(t, 3)
|
|
|
|
history := historyOf(t, server, localhost)
|
|
if history.Requests != 3 || history.Forwarded != 0 || history.Refused != 1 {
|
|
t.Errorf("history counts %d requests, %d forwarded and %d refused, "+
|
|
"want 3, 0 and 1", history.Requests, history.Forwarded, history.Refused)
|
|
}
|
|
}
|
|
|
|
// historyOf returns the history of the client at addr.
|
|
func historyOf(t *testing.T, server *proxy.Server, addr string) ratelimit.History {
|
|
t.Helper()
|
|
|
|
client := netip.MustParsePrefix(addr + "/32")
|
|
for _, c := range server.Limiter.Snapshot() {
|
|
if c.Client == client {
|
|
return c.History
|
|
}
|
|
}
|
|
|
|
t.Fatalf("%s is not in the table", client)
|
|
|
|
return ratelimit.History{}
|
|
}
|