Admin endpoints for bans and clients on the single listener (closes #27)
check / check (push) Successful in 4m29s

SWWAF_ADMIN_TOKEN, or its _FILE form, opens GET and POST
/_smallwebwaf/bans, DELETE /_smallwebwaf/bans/<client> and GET
/_smallwebwaf/clients/<ip>. Unset, they answer 404; a missing or wrong
token gets 401, in observe mode too. They go through every check, as
the metrics do. POST takes a netblock or a client's address, a duration
or permanent, and a reason, and makes an admin ban even while another
lasts. DELETE lifts every active ban covering the address, kept and
marked lifted. Bans come back as bans.json entries; a client as
clients.json holds it, with its bans.

Judgement call: answers leave out bans.json's version field.
Judgement call: DELETE takes an address, not a netblock.
Rule suppressed: gosec G304 on a test reading bans.json.

Model: opus-5-5
This commit is contained in:
2026-10-06 22:32:15 +00:00
parent bff65f4e2f
commit 9633ce99d2
15 changed files with 1269 additions and 104 deletions
+17 -3
View File
@@ -417,14 +417,28 @@ func (s *sender) requestWithHeader(
) (logLine, string) {
s.t.Helper()
line, got := s.requestWithBody(http.MethodGet, from, path, header, "", status, action)
return line, string(got.body)
}
// requestWithBody is requestWithHeader for a request with method, whose
// body is sent as it is after the headers, header holding its
// Content-Length or Transfer-Encoding. header may hold several lines,
// separated by "\r\n". It returns the whole answer.
func (s *sender) requestWithBody(
method, from, path, header, body string, status int, action string,
) (logLine, answer) {
s.t.Helper()
if header != "" {
header += "\r\n"
}
conn := dial(s.t, s.addr)
send(s.t, conn, "GET "+path+" HTTP/1.1\r\nHost: "+appHost+
send(s.t, conn, method+" "+path+" HTTP/1.1\r\nHost: "+appHost+
"\r\nUser-Agent: "+userAgent+"\r\n"+forwardedFor+": "+from+"\r\n"+
header+"\r\n")
header+"\r\n"+body)
err := conn.SetReadDeadline(time.Now().Add(waitLimit))
if err != nil {
@@ -453,5 +467,5 @@ func (s *sender) requestWithHeader(
s.sent++
wantLine(s.t, line, status, action)
return line, string(got.body)
return line, got
}