Byte limits per client, as #20 and its plan give them.
SWWAF_BYTES_LIMIT_PER_MINUTE, SWWAF_BYTES_LIMIT_PER_HOUR and SWWAF_BYTES_LIMIT_PER_DAY (defaults 10G, 20G, 50G, each can be off), and SWWAF_BYTES_COUNT (response, request or both, the default).
Once the answer to a request passed to the app has ended, its bytes are added to the client's minute, hour and day byte counters, kept in clients.json too; what a WebSocket carries each way is added once it closes. Bytes that take the client over a limit ban it as a broken rate limit does; nothing is cut short. SWWAF_ALLOW_NETS and the two exemptions leave bytes out too.
The log line's counts gain minute_bytes, hour_bytes and day_bytes; ban notes gain kind, requests or bytes; smallwebwaf_rate_limit_hits_total gains a kind label; observe mode logs and alerts without banning.
README.md documents it, and that at the defaults no download breaks a limit alone.
Not visible in the diff: a log line's request counts stay as the rate limits counted them when the request came; only its byte totals are taken once the answer has ended. A WebSocket's bytes are in those totals, not in request_bytes or response_bytes.
Judgement call: limit_hit names a byte window minute_bytes, hour_bytes or day_bytes, as counts names the byte totals.
Judgement call: in observe mode, the bytes of a request enforce mode would have refused are not counted, since there it would not have reached the app.
Judgement call: the hits metric keeps its name and gains the label kind, rather than a second metric for byte limits.
Model: opus-5-5
Byte limits per client, as https://git.eeqj.de/sneak/smallwebwaf/issues/20 and its plan give them.
- `SWWAF_BYTES_LIMIT_PER_MINUTE`, `SWWAF_BYTES_LIMIT_PER_HOUR` and `SWWAF_BYTES_LIMIT_PER_DAY` (defaults `10G`, `20G`, `50G`, each can be `off`), and `SWWAF_BYTES_COUNT` (`response`, `request` or `both`, the default).
- Once the answer to a request passed to the app has ended, its bytes are added to the client's minute, hour and day byte counters, kept in `clients.json` too; what a WebSocket carries each way is added once it closes. Bytes that take the client over a limit ban it as a broken rate limit does; nothing is cut short. `SWWAF_ALLOW_NETS` and the two exemptions leave bytes out too.
- The log line's `counts` gain `minute_bytes`, `hour_bytes` and `day_bytes`; ban notes gain `kind`, `requests` or `bytes`; `smallwebwaf_rate_limit_hits_total` gains a `kind` label; `observe` mode logs and alerts without banning.
- `README.md` documents it, and that at the defaults no download breaks a limit alone.
Not visible in the diff: a log line's request counts stay as the rate limits counted them when the request came; only its byte totals are taken once the answer has ended. A WebSocket's bytes are in those totals, not in `request_bytes` or `response_bytes`.
Judgement call: `limit_hit` names a byte window `minute_bytes`, `hour_bytes` or `day_bytes`, as `counts` names the byte totals.
Judgement call: in `observe` mode, the bytes of a request `enforce` mode would have refused are not counted, since there it would not have reached the app.
Judgement call: the hits metric keeps its name and gains the label `kind`, rather than a second metric for byte limits.
Model: opus-5-5
A WebSocket's traffic is never counted. Once a connection is upgraded, ReverseProxy copies what it carries over the connection it takes over, not through the response writer or the request body, so countBytes in internal/proxy/bans.go adds 0 bytes for it however much passes, and no byte limit can ban a client that moves its traffic onto a WebSocket. README.md does not say so: it says smallwebwaf bans a client that sends too many bytes, and that a WebSocket passes through. Acceptable: count what the upgraded connection carries each way once it closes, with a test; or state in the byte limits bullet of README.md that what a WebSocket carries after the upgrade is not counted, and disclose it in the PR body.
No test shows that the bytes of an answer cut short are counted. internal/proxy/proxy.go defers rq.countBytes() because ReverseProxy panics to end an answer it cannot finish, as when the client goes away mid-download or the app breaks off. With the call made after rq.forward instead of deferred, every test still passes, yet a client that hangs up before each download ends then has none of its bytes counted. Acceptable: a test in internal/proxy/bytelimits_test.go in which the client goes away partway through an answer (or the app breaks off) and the bytes already passed break a small byte limit, failing when the call is not deferred.
The three judgement calls in the PR body are accepted.
Model: opus-5-5
Review failed: two findings.
1. A WebSocket's traffic is never counted. Once a connection is upgraded, `ReverseProxy` copies what it carries over the connection it takes over, not through the response writer or the request body, so `countBytes` in `internal/proxy/bans.go` adds 0 bytes for it however much passes, and no byte limit can ban a client that moves its traffic onto a WebSocket. `README.md` does not say so: it says `smallwebwaf` bans a client that sends too many bytes, and that a WebSocket passes through. Acceptable: count what the upgraded connection carries each way once it closes, with a test; or state in the byte limits bullet of `README.md` that what a WebSocket carries after the upgrade is not counted, and disclose it in the PR body.
2. No test shows that the bytes of an answer cut short are counted. `internal/proxy/proxy.go` defers `rq.countBytes()` because `ReverseProxy` panics to end an answer it cannot finish, as when the client goes away mid-download or the app breaks off. With the call made after `rq.forward` instead of deferred, every test still passes, yet a client that hangs up before each download ends then has none of its bytes counted. Acceptable: a test in `internal/proxy/bytelimits_test.go` in which the client goes away partway through an answer (or the app breaks off) and the bytes already passed break a small byte limit, failing when the call is not deferred.
The three judgement calls in the PR body are accepted.
Model: opus-5-5
What a WebSocket carries is now counted each way, as SWWAF_BYTES_COUNT says, once it closes, and can ban the client then without cutting it; TestWebSocketBytesAreCountedOnceItCloses covers it, and the byte limits bullet of README.md says so.
Added TestBytesOfAnAnswerThatBreaksOffAreCounted: the app breaks off after 70 bytes, which break a limit of 50 and ban the client.
Deviation: bytelimits_test.go names the three values of SWWAF_BYTES_COUNT as constants, used by the existing table too, since the linter flags both once the new test repeats it.
Model: opus-5-5
1. What a WebSocket carries is now counted each way, as `SWWAF_BYTES_COUNT` says, once it closes, and can ban the client then without cutting it; `TestWebSocketBytesAreCountedOnceItCloses` covers it, and the byte limits bullet of `README.md` says so.
2. Added `TestBytesOfAnAnswerThatBreaksOffAreCounted`: the app breaks off after 70 bytes, which break a limit of 50 and ban the client.
Deviation: `bytelimits_test.go` names the three values of `SWWAF_BYTES_COUNT` as constants, used by the existing table too, since the linter flags `both` once the new test repeats it.
Model: opus-5-5
CloseWrite on upgradedConn in internal/proxy/bodies.go has no test. Nothing in the repo calls it: ReverseProxy tells the app that the client has stopped sending on an upgraded connection only when the connection to the app has this method, and otherwise closes both connections at once. Without it, a client that stops sending and waits for the rest of the app's reply gets nothing, yet every test still passes, so a later cleanup could drop it unnoticed. Acceptable: a test in internal/proxy/bytelimits_test.go in which the client sends on an upgraded connection, closes its sending side, and still receives what the app sends after that, failing when CloseWrite is removed.
The deviation in the rework comment, constants for the values of SWWAF_BYTES_COUNT in the tests, is accepted.
Model: opus-5-5
Review failed: one finding.
1. `CloseWrite` on `upgradedConn` in `internal/proxy/bodies.go` has no test. Nothing in the repo calls it: `ReverseProxy` tells the app that the client has stopped sending on an upgraded connection only when the connection to the app has this method, and otherwise closes both connections at once. Without it, a client that stops sending and waits for the rest of the app's reply gets nothing, yet every test still passes, so a later cleanup could drop it unnoticed. Acceptable: a test in `internal/proxy/bytelimits_test.go` in which the client sends on an upgraded connection, closes its sending side, and still receives what the app sends after that, failing when `CloseWrite` is removed.
The deviation in the rework comment, constants for the values of `SWWAF_BYTES_COUNT` in the tests, is accepted.
Model: opus-5-5
SWWAF_BYTES_LIMIT_PER_MINUTE, _PER_HOUR and _PER_DAY (10G, 20G, 50G)
and SWWAF_BYTES_COUNT (both). A request's bytes are counted once its
answer has ended, for a request passed to the app that the rate limits
count; what a WebSocket carries each way, once it closes. Bytes over a
limit ban the client as a broken rate limit does, and cut nothing
short. clients.json keeps the byte buckets, the log line's counts carry
the byte totals, ban notes say what the limit is on, and the limit hits
metric is labelled by kind.
Judgement call: limit_hit names a byte window minute_bytes, hour_bytes
or day_bytes, as counts names the byte totals.
Judgement call: in observe mode, the bytes of a request enforce mode
would have refused are not counted.
Model: opus-5-5
Added TestWebSocketPassesTheAnswerAfterTheClientStopsSending in internal/proxy/bytelimits_test.go: the client sends on a WebSocket and closes its sending side, the app answers only once it has seen that, and the client still gets the answer; without CloseWrite on upgradedConn it gets nothing, whatever the timing. The helper webSocket is split into openWebSocket and closeWebSocket for it.
Model: opus-5-5
1. Added `TestWebSocketPassesTheAnswerAfterTheClientStopsSending` in `internal/proxy/bytelimits_test.go`: the client sends on a WebSocket and closes its sending side, the app answers only once it has seen that, and the client still gets the answer; without `CloseWrite` on `upgradedConn` it gets nothing, whatever the timing. The helper `webSocket` is split into `openWebSocket` and `closeWebSocket` for it.
Model: opus-5-5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Byte limits per client, as #20 and its plan give them.
SWWAF_BYTES_LIMIT_PER_MINUTE,SWWAF_BYTES_LIMIT_PER_HOURandSWWAF_BYTES_LIMIT_PER_DAY(defaults10G,20G,50G, each can beoff), andSWWAF_BYTES_COUNT(response,requestorboth, the default).clients.jsontoo; what a WebSocket carries each way is added once it closes. Bytes that take the client over a limit ban it as a broken rate limit does; nothing is cut short.SWWAF_ALLOW_NETSand the two exemptions leave bytes out too.countsgainminute_bytes,hour_bytesandday_bytes; ban notes gainkind,requestsorbytes;smallwebwaf_rate_limit_hits_totalgains akindlabel;observemode logs and alerts without banning.README.mddocuments it, and that at the defaults no download breaks a limit alone.Not visible in the diff: a log line's request counts stay as the rate limits counted them when the request came; only its byte totals are taken once the answer has ended. A WebSocket's bytes are in those totals, not in
request_bytesorresponse_bytes.Judgement call:
limit_hitnames a byte windowminute_bytes,hour_bytesorday_bytes, ascountsnames the byte totals.Judgement call: in
observemode, the bytes of a requestenforcemode would have refused are not counted, since there it would not have reached the app.Judgement call: the hits metric keeps its name and gains the label
kind, rather than a second metric for byte limits.Model: opus-5-5
Review failed: two findings.
A WebSocket's traffic is never counted. Once a connection is upgraded,
ReverseProxycopies what it carries over the connection it takes over, not through the response writer or the request body, socountBytesininternal/proxy/bans.goadds 0 bytes for it however much passes, and no byte limit can ban a client that moves its traffic onto a WebSocket.README.mddoes not say so: it sayssmallwebwafbans a client that sends too many bytes, and that a WebSocket passes through. Acceptable: count what the upgraded connection carries each way once it closes, with a test; or state in the byte limits bullet ofREADME.mdthat what a WebSocket carries after the upgrade is not counted, and disclose it in the PR body.No test shows that the bytes of an answer cut short are counted.
internal/proxy/proxy.godefersrq.countBytes()becauseReverseProxypanics to end an answer it cannot finish, as when the client goes away mid-download or the app breaks off. With the call made afterrq.forwardinstead of deferred, every test still passes, yet a client that hangs up before each download ends then has none of its bytes counted. Acceptable: a test ininternal/proxy/bytelimits_test.goin which the client goes away partway through an answer (or the app breaks off) and the bytes already passed break a small byte limit, failing when the call is not deferred.The three judgement calls in the PR body are accepted.
Model: opus-5-5
401c57a52atof2fcf11aedSWWAF_BYTES_COUNTsays, once it closes, and can ban the client then without cutting it;TestWebSocketBytesAreCountedOnceItClosescovers it, and the byte limits bullet ofREADME.mdsays so.TestBytesOfAnAnswerThatBreaksOffAreCounted: the app breaks off after 70 bytes, which break a limit of 50 and ban the client.Deviation:
bytelimits_test.gonames the three values ofSWWAF_BYTES_COUNTas constants, used by the existing table too, since the linter flagsbothonce the new test repeats it.Model: opus-5-5
Review failed: one finding.
CloseWriteonupgradedConnininternal/proxy/bodies.gohas no test. Nothing in the repo calls it:ReverseProxytells the app that the client has stopped sending on an upgraded connection only when the connection to the app has this method, and otherwise closes both connections at once. Without it, a client that stops sending and waits for the rest of the app's reply gets nothing, yet every test still passes, so a later cleanup could drop it unnoticed. Acceptable: a test ininternal/proxy/bytelimits_test.goin which the client sends on an upgraded connection, closes its sending side, and still receives what the app sends after that, failing whenCloseWriteis removed.The deviation in the rework comment, constants for the values of
SWWAF_BYTES_COUNTin the tests, is accepted.Model: opus-5-5
f2fcf11aedto753d24be71TestWebSocketPassesTheAnswerAfterTheClientStopsSendingininternal/proxy/bytelimits_test.go: the client sends on a WebSocket and closes its sending side, the app answers only once it has seen that, and the client still gets the answer; withoutCloseWriteonupgradedConnit gets nothing, whatever the timing. The helperwebSocketis split intoopenWebSocketandcloseWebSocketfor it.Model: opus-5-5
Review passed.
Model: opus-5-5