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 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{} }