Take in an admin's edits of the state files while running (closes #68)
check / check (push) Successful in 3m33s

smallwebwaf watches SWWAF_STATE_DIR with fsnotify and takes in an edit of
bans.json, clients.json or lookups.json as soon as it is saved, in place
of what it held. It tells its own writes from an admin's by the SHA-256
of what it last read or wrote, and each write takes in an edit made since
first. An edit that does not parse is renamed to <name>.bad at the
file's next write, which writes the file again from memory and logs the
file and where the error is. README.md says how to add and lift a ban.

Judgement call: a broken edit is set aside at the file's next write, not
when seen, since an editor's file can be read half written.
Judgement call: a state file that cannot be read is not written over.

Model: opus-5-5
This commit is contained in:
2026-10-06 07:23:39 +00:00
parent df2c5042d2
commit bc0152445a
12 changed files with 813 additions and 154 deletions
+9 -6
View File
@@ -254,18 +254,21 @@ func (l *Limiter) Snapshot() []Client {
return clients
}
// Load puts clients read from clients.json into a table that holds none
// yet, in the order they were last seen, so that the least recently seen
// is dropped first. Buckets whose time has passed at now are emptied.
// Load puts clients read from clients.json into the table, in place of
// the clients it holds, in the order they were last seen, so that the
// least recently seen is dropped first. Buckets whose time has passed at
// now are emptied.
func (l *Limiter) Load(clients []Client, now time.Time) {
l.mu.Lock()
defer l.mu.Unlock()
clients = slices.Clone(clients)
slices.SortStableFunc(clients, func(a, b Client) int {
return a.History.LastSeen.Compare(b.History.LastSeen)
})
l.mu.Lock()
defer l.mu.Unlock()
l.clients.Purge()
for _, c := range clients {
for i, b := range c.buckets() {
// The window that ends at now covers neither bucket once it