rproxy and servewaf replace the one-process smallwebwaf (closes #172, closes #165)
check / check (push) Failing after 14m44s
check / check (push) Failing after 14m44s
`smallwebwaf` with no command now prints the commands and exits with status 2. The image runs rproxy, or servewaf with SMALLWEBWAF_COMMAND=servewaf. The passing-on code, its settings and its log fields and metric go. internal/proxy keeps the checks, answers, reports and own endpoints; its tests run through the real rproxy. The documents describe the pair. Judgement call: clients.json keeps token_refused, since state files refuse unknown fields. Deviation: script/gitea-requests sets SWWAF_LOG_REQUEST_HEADERS, to label requests. Partially verified: make example-app and make gitea-requests never built the image, as Ubuntu's snapshot service answered 5xx. Model: opus-5-5
This commit was merged in pull request #184.
This commit is contained in:
+20
-138
@@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -23,8 +22,6 @@ const (
|
||||
// how a request carries it.
|
||||
adminSecret = "fedcba9876543210fedcba9876543210"
|
||||
adminBearer = "Bearer " + adminSecret
|
||||
// adminClient is the client the tests' admin sends its requests from.
|
||||
adminClient = "192.0.2.10"
|
||||
// banOtherClient is the body of a request to ban otherClient for an
|
||||
// hour.
|
||||
banOtherClient = `{"netblock": "` + otherClient + `", "duration": "1h", ` +
|
||||
@@ -43,8 +40,7 @@ func TestAdminEndpointsAreOffWhileTheTokenIsUnset(t *testing.T) {
|
||||
// An empty token does not match the unset one either.
|
||||
for _, authorization := range []string{adminBearer, bearer, "Bearer ", ""} {
|
||||
for _, e := range adminEndpoints() {
|
||||
s.adminRequest(adminClient, authorization, e.method, e.path, e.body,
|
||||
http.StatusNotFound, requestlog.ActionAdmin)
|
||||
s.adminRequest(authorization, e.method, e.path, e.body, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +67,8 @@ func TestAdminEndpointsNeedTheAdminToken(t *testing.T) {
|
||||
for _, authorization := range []string{
|
||||
"", bearer, "Bearer " + strings.ToUpper(adminSecret), "Basic " + adminSecret,
|
||||
} {
|
||||
got := s.adminRequest(adminClient, authorization, e.method, e.path, e.body,
|
||||
http.StatusUnauthorized, requestlog.ActionAdmin)
|
||||
got := s.adminRequest(authorization, e.method, e.path, e.body,
|
||||
http.StatusUnauthorized)
|
||||
if got.header.Get("WWW-Authenticate") != "Bearer" {
|
||||
t.Errorf("%s %s with %q was answered without WWW-Authenticate: Bearer",
|
||||
e.method, e.path, authorization)
|
||||
@@ -265,64 +261,12 @@ func TestBanToAddThatCannotBeReadIsRefused(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanToAddOverTheRequestSizeLimitIsRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s, _, server := startWithClock(t, "", map[string]string{
|
||||
adminToken: adminSecret,
|
||||
requestMaxBytes: "16",
|
||||
})
|
||||
|
||||
// Sent in a chunk, its length is not announced, so that it is found
|
||||
// over SWWAF_REQUEST_MAX_BYTES only as it is read.
|
||||
chunk := `{"netblock": "203.0.113.9", "duration": "1h"}`
|
||||
s.adminRequest(adminClient, adminBearer+"\r\nTransfer-Encoding: chunked",
|
||||
http.MethodPost, proxy.BansPath,
|
||||
strconv.FormatInt(int64(len(chunk)), 16)+"\r\n"+chunk+"\r\n0\r\n\r\n",
|
||||
http.StatusRequestEntityTooLarge, requestlog.ActionTooLarge)
|
||||
|
||||
if held := server.Ledger.Snapshot(); len(held) != 0 {
|
||||
t.Errorf("the ledger holds %+v, want no ban", held)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanToAddSlowerThanTheClientRequestTimeoutIsRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s, _, server := startWithClock(t, "", map[string]string{
|
||||
adminToken: adminSecret,
|
||||
metricsToken: token,
|
||||
clientRequestTimeout: shortTimeoutSetting,
|
||||
})
|
||||
|
||||
// The chunk announces 256 bytes and the rest of it never comes, so only
|
||||
// the timeout ends the wait. A hold-up of the test process can only
|
||||
// make the answer later, so the time is checked only for not being
|
||||
// shorter than the timeout.
|
||||
start := time.Now()
|
||||
|
||||
s.adminRequest(adminClient, adminBearer+"\r\nTransfer-Encoding: chunked",
|
||||
http.MethodPost, proxy.BansPath, "100\r\n"+`{"netblock": "203.0.113.9", `,
|
||||
http.StatusRequestTimeout, requestlog.ActionTimedOut)
|
||||
|
||||
if took := time.Since(start); took < shortTimeout {
|
||||
t.Errorf("answered after %s, before the timeout of %s ran out", took, shortTimeout)
|
||||
}
|
||||
|
||||
wantLimitHits(t, s.addr, clientRequestTimeout, 1)
|
||||
|
||||
if held := server.Ledger.Snapshot(); len(held) != 0 {
|
||||
t.Errorf("the ledger holds %+v, want no ban", held)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientEndpointShowsTheClientAndItsBans(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s, clk, _ := startWithClock(t, "", map[string]string{
|
||||
adminToken: adminSecret,
|
||||
rateLimitPerMinute: "2",
|
||||
rateLimitExemptNets: adminClient,
|
||||
adminToken: adminSecret,
|
||||
rateLimitPerMinute: "2",
|
||||
})
|
||||
start := clk.Now()
|
||||
|
||||
@@ -371,61 +315,6 @@ func TestClientEndpointShowsTheClientAndItsBans(t *testing.T) {
|
||||
http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestBannedClientIsRefusedAtTheEndpointsEvenWithTheToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s, _, _ := startWithClock(t, "", map[string]string{adminToken: adminSecret})
|
||||
|
||||
s.admin(http.MethodPost, proxy.BansPath, banOtherClient, http.StatusOK)
|
||||
|
||||
// otherClient cannot lift its own ban either.
|
||||
for _, e := range adminEndpoints() {
|
||||
s.adminRequest(otherClient, adminBearer, e.method, e.path, e.body,
|
||||
http.StatusForbidden, requestlog.ActionBanned)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminRequestsCountTowardTheLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s, _, _ := startWithClock(t, "", map[string]string{
|
||||
adminToken: adminSecret,
|
||||
rateLimitPerMinute: "2",
|
||||
})
|
||||
|
||||
// A request refused for a missing token and one answered count toward
|
||||
// the limit of two a minute, so the next breaks it.
|
||||
s.adminRequest(client, "", http.MethodGet, proxy.BansPath, "",
|
||||
http.StatusUnauthorized, requestlog.ActionAdmin)
|
||||
s.adminRequest(client, adminBearer, http.MethodGet, proxy.BansPath, "",
|
||||
http.StatusOK, requestlog.ActionAdmin)
|
||||
s.adminRequest(client, adminBearer, http.MethodGet, proxy.BansPath, "",
|
||||
http.StatusForbidden, requestlog.ActionRateLimited)
|
||||
}
|
||||
|
||||
func TestClientInAllowNetsSkipsTheChecksButNeedsTheToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const allowed = "192.0.2.60" // in SWWAF_ALLOW_NETS
|
||||
|
||||
s, clk, server := startWithClock(t, "", map[string]string{
|
||||
adminToken: adminSecret,
|
||||
allowNets: allowed,
|
||||
rateLimitPerMinute: "1",
|
||||
})
|
||||
|
||||
// A ban on it refuses nothing, and its requests are not counted.
|
||||
server.Ledger.BanForAdmin(netip.MustParsePrefix(allowed+"/32"), clk.Now(),
|
||||
time.Time{}, "")
|
||||
|
||||
for range 2 {
|
||||
s.adminRequest(allowed, "", http.MethodGet, proxy.BansPath, "",
|
||||
http.StatusUnauthorized, requestlog.ActionAdmin)
|
||||
s.adminRequest(allowed, adminBearer, http.MethodGet, proxy.BansPath, "",
|
||||
http.StatusOK, requestlog.ActionAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminEndpointsNeedTheTokenInObserveMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -435,8 +324,7 @@ func TestAdminEndpointsNeedTheTokenInObserveMode(t *testing.T) {
|
||||
})
|
||||
|
||||
for _, e := range adminEndpoints() {
|
||||
s.adminRequest(adminClient, "", e.method, e.path, e.body,
|
||||
http.StatusUnauthorized, requestlog.ActionAdmin)
|
||||
s.adminRequest("", e.method, e.path, e.body, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
if held := server.Ledger.Snapshot(); len(held) != 0 {
|
||||
@@ -461,39 +349,33 @@ func adminEndpoints() []adminEndpoint {
|
||||
}
|
||||
}
|
||||
|
||||
// admin sends a request with method for path, with body, from
|
||||
// adminClient, with the admin token, and checks that it is answered with
|
||||
// status, its log line's action admin. It returns the answer.
|
||||
// admin sends a request with method for path, with body, to servewaf's own
|
||||
// address, with the admin token, and checks that it is answered with status, its
|
||||
// log line's action admin. It returns the answer.
|
||||
func (s *sender) admin(method, path, body string, status int) answer {
|
||||
s.t.Helper()
|
||||
|
||||
return s.adminRequest(adminClient, adminBearer, method, path, body, status,
|
||||
requestlog.ActionAdmin)
|
||||
return s.adminRequest(adminBearer, method, path, body, status)
|
||||
}
|
||||
|
||||
// adminRequest sends a request with method for path, with body, from the
|
||||
// client at from, with authorization as its Authorization header unless
|
||||
// it is "", and checks its answer's status and its log line's action, as
|
||||
// request does. authorization may end in more header lines. A body that
|
||||
// is not "" has its length announced, unless authorization names
|
||||
// Transfer-Encoding. It returns the answer.
|
||||
// adminRequest is admin with authorization as the request's Authorization header
|
||||
// unless it is "".
|
||||
func (s *sender) adminRequest(
|
||||
from, authorization, method, path, body string, status int, action string,
|
||||
authorization, method, path, body string, status int,
|
||||
) answer {
|
||||
s.t.Helper()
|
||||
|
||||
var header []string
|
||||
|
||||
req := newRequest(s.t, method, s.serveWAFAddr, path, strings.NewReader(body))
|
||||
if authorization != "" {
|
||||
header = append(header, "Authorization: "+authorization)
|
||||
req.Header.Set("Authorization", authorization)
|
||||
}
|
||||
|
||||
if body != "" && !strings.Contains(authorization, "Transfer-Encoding") {
|
||||
header = append(header, "Content-Length: "+strconv.Itoa(len(body)))
|
||||
}
|
||||
got := do(s.t, req)
|
||||
wantStatus(s.t, got, status)
|
||||
|
||||
_, got := s.requestWithBody(method, from, path, strings.Join(header, "\r\n"),
|
||||
body, status, action)
|
||||
line := s.out.endedLines(s.t, s.sent+1)[s.sent]
|
||||
s.sent++
|
||||
wantLine(s.t, line, status, requestlog.ActionAdmin)
|
||||
|
||||
return got
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user