check / check (push) Successful in 4m12s
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. SWWAF_METRICS_TOP_N bounds the series by country, the rest counted as other. Judgement call: a request answered at smallwebwaf's own endpoints is neither forwarded nor refused in the client's history. Deviation: go.mod and go.sum written by hand from the module proxy and sum.golang.org, 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
122 lines
3.5 KiB
Go
122 lines
3.5 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 country is not looked up.
|
|
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),
|
|
Country: "DE",
|
|
LookedUp: start.Add(time.Second),
|
|
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 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 TestRequestForSmallwebwafIsNeitherForwardedNorRefused(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})
|
|
|
|
scrape(t, addr)
|
|
wantStatus(t, get(t, addr, "/_smallwebwaf/nothing"), http.StatusNotFound)
|
|
out.requestLines(t, 2)
|
|
|
|
history := historyOf(t, server, localhost)
|
|
if history.Requests != 2 || history.Forwarded != 0 || history.Refused != 0 {
|
|
t.Errorf("history counts %d requests, %d forwarded and %d refused, "+
|
|
"want 2, 0 and 0", 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{}
|
|
}
|