check / check (push) Waiting to run
Twelve tests in internal/proxy needed the GeoJS stand-in to be asked or to answer within the default SWWAF_LOOKUP_TIMEOUT of one second on the real clock. A hold-up of the test process past it abandoned the request to the stand-in, or left the client unknown. Each now sets SWWAF_LOOKUP_TIMEOUT to an hour, and the comment on startGeoJS asks the same of later tests. Judgement call: set in each test, not as a default in newProxy, where it would change two tests that rely on the default second. Model: opus-5-5
148 lines
4.4 KiB
Go
148 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{
|
|
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.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{}
|
|
}
|