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
143 lines
4.2 KiB
Go
143 lines
4.2 KiB
Go
package servewaf_test
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
"sneak.berlin/go/smallwebwaf/internal/servewaf"
|
|
)
|
|
|
|
func TestHistoryKeepsEachRequestOfTheClient(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
geojsURL, _ := startGeoJS(t)
|
|
s, clk, server := startWithClock(t, geojsURL, map[string]string{
|
|
lookupTimeout: "1h",
|
|
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.endedLine(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 TestOwnEndpointsAreNotInTheHistory(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s, _, server := startWithClock(t, "", map[string]string{metricsToken: token})
|
|
|
|
s.adminRequest("", http.MethodGet, servewaf.HealthPath, "", http.StatusOK)
|
|
s.adminRequest("", http.MethodGet, servewaf.MetricsPath, "", http.StatusUnauthorized)
|
|
s.scrape()
|
|
|
|
if clients := server.Limiter.Snapshot(); len(clients) != 0 {
|
|
t.Errorf("the table holds %+v, want no client", clients)
|
|
}
|
|
}
|
|
|
|
func TestPathUnderTheOwnPrefixIsNeitherForwardedNorRefused(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, "/_smallwebwaf/nothing"), http.StatusNotFound)
|
|
out.endedLine(t)
|
|
|
|
history := historyOf(t, server, localhost)
|
|
if history.Requests != 1 || history.Forwarded != 0 || history.Refused != 0 {
|
|
t.Errorf("history counts %d requests, %d forwarded and %d refused, "+
|
|
"want 1, 0 and 0", history.Requests, history.Forwarded, history.Refused)
|
|
}
|
|
}
|
|
|
|
// historyOf returns the history of the client at addr.
|
|
func historyOf(t *testing.T, server *servewaf.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{}
|
|
}
|