Rename the program to sww and its deciding daemon to discriminator (closes #218)
check / check (push) Failing after 2m47s
check / check (push) Failing after 2m47s
The program is now sww, built from cmd/sww, with two modes: sww rproxy and sww discriminator. servewaf becomes discriminator everywhere it names that daemon: the package (internal/discriminator), its types, functions, files, the discriminator_line log field, and its docs and tests. smallwebwaf is left only where sneak's directive allows it: the documents' product name, the git.eeqj.de/sneak/smallwebwaf URLs, and the Go module path sneak.berlin/go/smallwebwaf. Everywhere else a name containing it takes the sww form: the package (internal/sww), metric names (sww_*), the X-Sww-* headers, the /_sww/ admin endpoints, the OS user, and the state and rules directories. The SWWAF_ environment variables take the SWW_ form. No logic changes. make check passes. Model: deepseek-v4-pro
This commit is contained in:
1 parent
2e2f6f7175
commit
4a030557f3
106 files changed
+3302
-3275
No files matched your search
+25
-25
@@ -1,5 +1,5 @@
|
||||
// Package state keeps smallwebwaf's state in JSON files in
|
||||
// SWWAF_STATE_DIR, as the "Persistent state" section of SPEC.md requires
|
||||
// Package state keeps sww's state in JSON files in
|
||||
// SWW_STATE_DIR, as the "Persistent state" section of SPEC.md requires
|
||||
// and docs/state-files.md lists them:
|
||||
// bans.json holds the bans, clients.json each client's counters and
|
||||
// history, lookups.json GeoJS's answers, reputation.json the last try and
|
||||
@@ -8,7 +8,7 @@
|
||||
// cooldowns, the hour under way, the alerts waiting for each destination
|
||||
// and the anomaly counters. Load
|
||||
// reads them at start, Watch takes in an admin's edit of one while
|
||||
// smallwebwaf runs, and Run and WriteAll write them. The disk is read and
|
||||
// sww runs, and Run and WriteAll write them. The disk is read and
|
||||
// written outside the parts' locks, which are held only to take a
|
||||
// snapshot or to put in what a file holds, so that no request waits on
|
||||
// the disk.
|
||||
@@ -44,7 +44,7 @@ import (
|
||||
// version is the version of the files' format, the only one read.
|
||||
const version = 1
|
||||
|
||||
// fileMode lets the smallwebwaf user alone read and write the files, which
|
||||
// fileMode lets the sww user alone read and write the files, which
|
||||
// hold visitors' addresses.
|
||||
const fileMode = 0o600
|
||||
|
||||
@@ -72,16 +72,16 @@ var (
|
||||
|
||||
// Params are what Load needs.
|
||||
type Params struct {
|
||||
// Dir is the directory of the state files (SWWAF_STATE_DIR).
|
||||
// Dir is the directory of the state files (SWW_STATE_DIR).
|
||||
Dir string
|
||||
// WriteDelay is how long after a ban is made bans.json is written
|
||||
// (SWWAF_STATE_WRITE_DELAY), and CounterInterval how often every file
|
||||
// is (SWWAF_STATE_COUNTER_INTERVAL).
|
||||
// (SWW_STATE_WRITE_DELAY), and CounterInterval how often every file
|
||||
// is (SWW_STATE_COUNTER_INTERVAL).
|
||||
WriteDelay time.Duration
|
||||
CounterInterval time.Duration
|
||||
// Ledger, Limiter, GeoJS, Lists, DNSBL, AbuseIPDB, Alerts and Anomalies
|
||||
// hold the state. Alerts also receive a file_error alert for an edit set
|
||||
// aside, and for a write that fails while smallwebwaf runs.
|
||||
// aside, and for a write that fails while sww runs.
|
||||
Ledger *bans.Ledger
|
||||
Limiter *ratelimit.Limiter
|
||||
GeoJS *lookup.GeoJS
|
||||
@@ -101,7 +101,7 @@ type Params struct {
|
||||
Metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// Files are the state files of a running smallwebwaf.
|
||||
// Files are the state files of a running sww.
|
||||
type Files struct {
|
||||
params Params
|
||||
|
||||
@@ -110,7 +110,7 @@ type Files struct {
|
||||
// it.
|
||||
mu sync.Mutex
|
||||
// sums are the SHA-256 sums of what each file held, by name, when
|
||||
// smallwebwaf last read or wrote it. A file that holds anything else
|
||||
// sww last read or wrote it. A file that holds anything else
|
||||
// has been edited since.
|
||||
sums map[string][sha256.Size]byte
|
||||
}
|
||||
@@ -185,7 +185,7 @@ type stateFile interface {
|
||||
func Load(params Params) (*Files, error) {
|
||||
err := checkWritable(params.Dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SWWAF_STATE_DIR cannot be written: %w", err)
|
||||
return nil, fmt.Errorf("SWW_STATE_DIR cannot be written: %w", err)
|
||||
}
|
||||
|
||||
f := &Files{params: params, sums: map[string][sha256.Size]byte{}}
|
||||
@@ -241,7 +241,7 @@ func (f *Files) Run(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// WriteAll writes every state file, as smallwebwaf stops. A file that
|
||||
// WriteAll writes every state file, as sww stops. A file that
|
||||
// fails does not keep the others from being written.
|
||||
func (f *Files) WriteAll() error {
|
||||
return errors.Join(f.writeFile(bansJSON), f.writeFile(clientsJSON),
|
||||
@@ -250,7 +250,7 @@ func (f *Files) WriteAll() error {
|
||||
|
||||
// Watch watches Dir until ctx is done, and takes in an admin's edit of a
|
||||
// state file as soon as it is saved: what the file holds replaces what
|
||||
// smallwebwaf held for it. An edit that does not parse is left for the
|
||||
// sww held for it. An edit that does not parse is left for the
|
||||
// file's next write, which sets it aside, since a file can be read while
|
||||
// an editor is still writing it. If Dir cannot be watched, that is
|
||||
// logged, and an edit is taken in only before its file is written.
|
||||
@@ -310,7 +310,7 @@ func (f *Files) logFailure(name string, err error) {
|
||||
}
|
||||
|
||||
// fileChanged takes in what the state file name holds, as Watch sees it
|
||||
// change, if that is an edit made since smallwebwaf last read or wrote
|
||||
// change, if that is an edit made since sww last read or wrote
|
||||
// the file. A file that cannot be read or does not parse is left for its
|
||||
// next write.
|
||||
func (f *Files) fileChanged(name string) {
|
||||
@@ -326,7 +326,7 @@ func (f *Files) fileChanged(name string) {
|
||||
}
|
||||
|
||||
// takeInEdit takes in data, an edit of the state file name, as takeIn
|
||||
// does, and counts and logs it. Every edit taken in while smallwebwaf
|
||||
// does, and counts and logs it. Every edit taken in while sww
|
||||
// runs, by Watch or by a write, is taken in here. An edit that does not
|
||||
// parse is neither counted nor logged, and takeIn's error returned.
|
||||
func (f *Files) takeInEdit(name string, data []byte) error {
|
||||
@@ -356,13 +356,13 @@ func (f *Files) read(name string) (int, error) {
|
||||
}
|
||||
|
||||
// readChanged returns what the state file name holds, and whether that
|
||||
// has changed since smallwebwaf last read or wrote the file, as it has
|
||||
// for a file smallwebwaf never read or wrote. A missing file has not
|
||||
// has changed since sww last read or wrote the file, as it has
|
||||
// for a file sww never read or wrote. A missing file has not
|
||||
// changed: it is written again at its next write.
|
||||
func (f *Files) readChanged(name string) ([]byte, bool, error) {
|
||||
path := filepath.Join(f.params.Dir, name)
|
||||
|
||||
data, err := os.ReadFile(path) //nolint:gosec // a state file, in SWWAF_STATE_DIR
|
||||
data, err := os.ReadFile(path) //nolint:gosec // a state file, in SWW_STATE_DIR
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, false, nil
|
||||
}
|
||||
@@ -377,7 +377,7 @@ func (f *Files) readChanged(name string) ([]byte, bool, error) {
|
||||
// takeIn parses data, what the state file name holds, puts it into the
|
||||
// part that keeps that state, in place of what the part held, and returns
|
||||
// how many entries the file holds. edit is whether data is an admin's
|
||||
// edit taken in while smallwebwaf runs, rather than the file read at the
|
||||
// edit taken in while sww runs, rather than the file read at the
|
||||
// start. An error names the file and, where the JSON decoder tells it,
|
||||
// the line and column, or else the entry.
|
||||
func (f *Files) takeIn(name string, data []byte, edit bool) (int, error) {
|
||||
@@ -491,8 +491,8 @@ func (f *Files) takeInAlerts(path string, data []byte) (int, error) {
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// writeFile writes the state file name from what smallwebwaf holds. An
|
||||
// edit made since smallwebwaf last read or wrote the file is taken in
|
||||
// writeFile writes the state file name from what sww holds. An
|
||||
// edit made since sww last read or wrote the file is taken in
|
||||
// first, so that it is not overwritten, or set aside if it does not
|
||||
// parse. A file that cannot be read, or an edit that cannot be set
|
||||
// aside, is left as it is, and the write given up. Every write is counted
|
||||
@@ -561,7 +561,7 @@ func (f *Files) setAside(name string, parseErr error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// encode returns the state file name as smallwebwaf writes it, from a
|
||||
// encode returns the state file name as sww writes it, from a
|
||||
// snapshot of the part that keeps that state.
|
||||
func (f *Files) encode(name string) ([]byte, error) {
|
||||
switch name {
|
||||
@@ -965,7 +965,7 @@ func parse(path string, data []byte, file stateFile) error {
|
||||
|
||||
err := json.Unmarshal(data, &header)
|
||||
if err == nil && header.Version != version {
|
||||
err = fmt.Errorf("%w %d, where this smallwebwaf reads version %d",
|
||||
err = fmt.Errorf("%w %d, where this sww reads version %d",
|
||||
errVersion, header.Version, version)
|
||||
}
|
||||
|
||||
@@ -1035,7 +1035,7 @@ func write(dir, name string, data []byte) error {
|
||||
|
||||
// syncDirectory syncs dir to the disk, so that a rename in it lasts.
|
||||
func syncDirectory(dir string) error {
|
||||
directory, err := os.Open(dir) //nolint:gosec // SWWAF_STATE_DIR itself
|
||||
directory, err := os.Open(dir) //nolint:gosec // SWW_STATE_DIR itself
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1045,7 +1045,7 @@ func syncDirectory(dir string) error {
|
||||
|
||||
// writeSynced writes data to the file at path, and syncs it to the disk.
|
||||
func writeSynced(path string, data []byte) error {
|
||||
//nolint:gosec // a state file's temporary file, in SWWAF_STATE_DIR
|
||||
//nolint:gosec // a state file's temporary file, in SWW_STATE_DIR
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -122,7 +122,7 @@ const filledAlertsJSON = `{
|
||||
{
|
||||
"event": "file_error",
|
||||
"netblock": "",
|
||||
"file": "/var/lib/smallwebwaf/bans.json",
|
||||
"file": "/var/lib/sww/bans.json",
|
||||
"sent": "2026-10-06T00:00:00Z",
|
||||
"suppressed_repeats": 0
|
||||
},
|
||||
@@ -169,7 +169,7 @@ const filledAlertsJSON = `{
|
||||
"reason": "writing the state files failed",
|
||||
"detail": {
|
||||
"error": "no space left on device",
|
||||
"file": "/var/lib/smallwebwaf/bans.json"
|
||||
"file": "/var/lib/sww/bans.json"
|
||||
},
|
||||
"suppressed_repeats": 0
|
||||
}
|
||||
@@ -931,7 +931,7 @@ func TestUnwritableDirectoryStopsTheStart(t *testing.T) {
|
||||
filepath.Join(t.TempDir(), "missing"),
|
||||
notADirectory,
|
||||
} {
|
||||
const want = "SWWAF_STATE_DIR cannot be written: "
|
||||
const want = "SWW_STATE_DIR cannot be written: "
|
||||
|
||||
_, err := state.Load(newParams(dir))
|
||||
if err == nil || !strings.HasPrefix(err.Error(), want) {
|
||||
@@ -1165,13 +1165,13 @@ func TestWritesAreCountedInTheMetrics(t *testing.T) {
|
||||
)
|
||||
|
||||
got := scrape(t, params)
|
||||
wantMetric(t, got, "smallwebwaf_state_file_writes_total"+ofBans, 1)
|
||||
wantMetric(t, got, "smallwebwaf_state_file_writes_total"+ofClients, 1)
|
||||
wantMetric(t, got, "smallwebwaf_state_file_write_failures_total"+ofBans, 0)
|
||||
wantMetric(t, got, "smallwebwaf_state_file_size_bytes"+ofBans,
|
||||
wantMetric(t, got, "sww_state_file_writes_total"+ofBans, 1)
|
||||
wantMetric(t, got, "sww_state_file_writes_total"+ofClients, 1)
|
||||
wantMetric(t, got, "sww_state_file_write_failures_total"+ofBans, 0)
|
||||
wantMetric(t, got, "sww_state_file_size_bytes"+ofBans,
|
||||
float64(len(permanentBansJSON)))
|
||||
|
||||
written := metric(t, got, "smallwebwaf_state_file_last_write_timestamp_seconds"+ofBans)
|
||||
written := metric(t, got, "sww_state_file_last_write_timestamp_seconds"+ofBans)
|
||||
if written < float64(time.Now().Add(-time.Hour).Unix()) {
|
||||
t.Errorf("bans.json was last written at %v, not by that write", written)
|
||||
}
|
||||
@@ -1192,10 +1192,10 @@ func TestWritesAreCountedInTheMetrics(t *testing.T) {
|
||||
}
|
||||
|
||||
got = scrape(t, params)
|
||||
wantMetric(t, got, "smallwebwaf_state_file_writes_total"+ofBans, 2)
|
||||
wantMetric(t, got, "smallwebwaf_state_file_write_failures_total"+ofBans, 1)
|
||||
wantMetric(t, got, "smallwebwaf_state_file_write_failures_total"+ofClients, 0)
|
||||
wantMetric(t, got, "smallwebwaf_state_file_size_bytes"+ofBans,
|
||||
wantMetric(t, got, "sww_state_file_writes_total"+ofBans, 2)
|
||||
wantMetric(t, got, "sww_state_file_write_failures_total"+ofBans, 1)
|
||||
wantMetric(t, got, "sww_state_file_write_failures_total"+ofClients, 0)
|
||||
wantMetric(t, got, "sww_state_file_size_bytes"+ofBans,
|
||||
float64(len(permanentBansJSON)))
|
||||
}
|
||||
|
||||
@@ -1486,7 +1486,7 @@ func TestBanWithoutACauseTakenInAsAnAdmins(t *testing.T) {
|
||||
"and the reason %q", written, err, reason)
|
||||
}
|
||||
|
||||
// Taken in while smallwebwaf runs, a ban on another netblock is an
|
||||
// Taken in while sww runs, a ban on another netblock is an
|
||||
// admin's too, and one made since the start.
|
||||
watch(t, files, lines)
|
||||
edit(t, dir, bansJSON, adminsBansJSON("198.51.100.0/24"))
|
||||
@@ -1549,7 +1549,7 @@ func TestBrokenEditSetAsideAtTheNextWrite(t *testing.T) {
|
||||
|
||||
watch(t, files, lines)
|
||||
|
||||
// While smallwebwaf runs, the broken edit is left as it is: an edit
|
||||
// While sww runs, the broken edit is left as it is: an edit
|
||||
// of clients.json, made after it and taken in, shows that it has been
|
||||
// seen.
|
||||
edit(t, dir, bansJSON, broken)
|
||||
@@ -1558,7 +1558,7 @@ func TestBrokenEditSetAsideAtTheNextWrite(t *testing.T) {
|
||||
wantFiles(t, dir, alertsJSON, bansJSON, clientsJSON, lookupsJSON, reputationJSON)
|
||||
|
||||
// The next write sets it aside, logged with where the error is, and
|
||||
// writes bans.json again from what smallwebwaf still holds.
|
||||
// writes bans.json again from what sww still holds.
|
||||
err = files.WriteAll()
|
||||
if err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
@@ -1613,7 +1613,7 @@ func TestEditsTakenInAreCountedInTheMetrics(t *testing.T) {
|
||||
wantTakenIn(t, lines, dir, bansJSON)
|
||||
|
||||
wantMetric(t, scrape(t, params),
|
||||
`smallwebwaf_state_file_edits_taken_in_total{file="bans.json",instance="app"}`, 2)
|
||||
`sww_state_file_edits_taken_in_total{file="bans.json",instance="app"}`, 2)
|
||||
}
|
||||
|
||||
func TestEditTakenInByAWriteIsLoggedAsWatchLogsIt(t *testing.T) {
|
||||
@@ -1678,7 +1678,7 @@ func TestEditsSetAsideAreCountedInTheMetrics(t *testing.T) {
|
||||
}
|
||||
|
||||
wantMetric(t, scrape(t, params),
|
||||
`smallwebwaf_state_file_edits_set_aside_total{file="bans.json",instance="app"}`, 1)
|
||||
`sww_state_file_edits_set_aside_total{file="bans.json",instance="app"}`, 1)
|
||||
}
|
||||
|
||||
func TestDirectoryThatCannotBeWatchedIsLogged(t *testing.T) {
|
||||
@@ -1854,7 +1854,7 @@ func fill(params state.Params) {
|
||||
params.Alerts.Raise(alerts.Alert{
|
||||
Event: alerts.EventFileError, Reason: "writing the state files failed",
|
||||
Detail: map[string]any{
|
||||
"file": "/var/lib/smallwebwaf/bans.json", "error": "no space left on device",
|
||||
"file": "/var/lib/sww/bans.json", "error": "no space left on device",
|
||||
},
|
||||
})
|
||||
params.Alerts.Raise(alerts.Alert{
|
||||
@@ -2178,7 +2178,7 @@ func scrape(t *testing.T, params state.Params) string {
|
||||
}
|
||||
|
||||
// metric returns the value of series in text, the metrics, such as
|
||||
// smallwebwaf_state_file_writes_total{file="bans.json",instance="app"}, or
|
||||
// sww_state_file_writes_total{file="bans.json",instance="app"}, or
|
||||
// fails the test if there is no such series.
|
||||
func metric(t *testing.T, text, series string) float64 {
|
||||
t.Helper()
|
||||
@@ -2210,8 +2210,8 @@ func wantWriteFailed(t *testing.T, params state.Params, name string) {
|
||||
got := scrape(t, params)
|
||||
file := `{file="` + name + `",instance="app"}`
|
||||
|
||||
wantMetric(t, got, "smallwebwaf_state_file_writes_total"+file, 1)
|
||||
wantMetric(t, got, "smallwebwaf_state_file_write_failures_total"+file, 1)
|
||||
wantMetric(t, got, "sww_state_file_writes_total"+file, 1)
|
||||
wantMetric(t, got, "sww_state_file_write_failures_total"+file, 1)
|
||||
}
|
||||
|
||||
// wantMetric checks the value of series in text, the metrics, as metric
|
||||
|
||||
Reference in new issue
Block a user