edge: a shut door records nothing — check it before the body is read
The front route's door switch is now the first thing serveBeacon asks after "is there an app to attribute to", ahead of the origin rule, the size cap and the parse. A shut door returns through the deferred 202 having read no body and having called no drop, so a stranger posting at a hostname whose owner switched the door off cannot move that owner's dropped counters — and cannot make them pay for a 16 KB read either. The drop closure is defined after the check, so the shut path has nothing to record with. The second switch check, after attribution, stays exactly as it was: a path-routed hostname's beacon belongs to the app owning its path, and that app's own door (and, for an event, its own count switch) decides for it. The two checks answer different questions and the handler's ordering comment now says so. The test lands in TestDoorSwitches where the app is already count-on, door-off — the combination that bites, because an origin or size refusal at an OPEN door would be recorded under those switches. It posts a foreign Origin with a 17 KB body and asserts 202, no record, no track, and an unchanged drop count.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 files changed,
+33
−7
internal/edge/browserdoor.go+20 −7internal/edge/browserdoor_test.go+13 −0
diff --git a/internal/edge/browserdoor.go b/internal/edge/browserdoor.go| index b17152714..14ff4a9fe 100644 |
| --- a/internal/edge/browserdoor.go |
| +++ b/internal/edge/browserdoor.go |
| @@ -192,13 +192,19 @@ func (e *Edge) doorLimiter() *doorLimiter { |
| // app whose count switch is on (ruling 2), so an app that is not being |
| // counted cannot be made to accumulate anything at all. |
| // |
| -// Order matters and is the order below: who may post (origin), how much |
| -// (size), what shape (parse), whose it is (attribution), whether they asked |
| -// for it (switches), how fast (rate), and only then the sink or the meter. |
| -// Attribution comes before the switch check because a path-routed hostname's |
| -// beacon belongs to the app owning its path, and it is THAT app's switches |
| -// and THAT app's buckets which decide — the front route is only the door the |
| -// post arrived at. |
| +// Order matters and is the order below: is the door open at all, who may |
| +// post (origin), how much (size), what shape (parse), whose it is |
| +// (attribution), whether THAT app asked for it (switches again), how fast |
| +// (rate), and only then the sink or the meter. |
| +// |
| +// The front route's door switch is checked FIRST, before the body is read |
| +// and before any drop: a shut door records nothing. The hostname's owner |
| +// turned this off, so a stranger posting at it must not be able to move the |
| +// owner's counters — and must not cost a 16 KB read either. The second |
| +// switch check, after attribution, is a different question: a path-routed |
| +// hostname's beacon belongs to the app owning its path, and it is THAT app's |
| +// switches and THAT app's buckets which decide whether it is kept. The front |
| +// route is only the door the post arrived at. |
| func (e *Edge) serveBeacon(w http.ResponseWriter, r *http.Request, host string, up upstream, door byte) { |
| defer w.WriteHeader(http.StatusAccepted) |
| // A status page has no app to attribute to, and neither does a route |
| @@ -207,6 +213,13 @@ func (e *Edge) serveBeacon(w http.ResponseWriter, r *http.Request, host string, |
| if up.route.Kind == "status" || up.route.App == "" { |
| return |
| } |
| + // A shut door records nothing. Before the origin rule, before the body |
| + // is read, before any drop: the owner of the hostname this arrived at |
| + // switched the door off, and a stranger must not be able to move their |
| + // dropped counters or make them pay for a body read by posting at it. |
| + if !e.switchesFor(up.route).Door { |
| + return |
| + } |
| now := time.Now() |
| // drop records one refusal against rt's app, if that app is counted. |
| // Refusals before attribution are charged to the route the post arrived |
diff --git a/internal/edge/browserdoor_test.go b/internal/edge/browserdoor_test.go| index 70973b56b..c91746b1b 100644 |
| --- a/internal/edge/browserdoor_test.go |
| +++ b/internal/edge/browserdoor_test.go |
| @@ -452,6 +452,19 @@ func TestDoorSwitches(t *testing.T) { |
| if len(meter.snapTracks()) != 0 || len(sink.snap()) != 1 || len(meter.snapDrops()) != 0 { |
| t.Fatal("door off: both dropped silently") |
| } |
| + // A shut door records nothing, whatever arrives at it. The count switch |
| + // is ON here, so an origin or size refusal WOULD be counted if the door |
| + // were open — and it must not be, because the owner shut the door and a |
| + // stranger's post cannot be allowed to move their counters (or to cost |
| + // the body read that judging its size would need). |
| + dropsBefore := len(meter.snapDrops()) |
| + if st := postBeacon(t, srv, "/__carlos/e", `{"name":"`+strings.Repeat("x", 17<<10)+`"}`, |
| + map[string]string{"Origin": "https://evil.example.net"}); st != 202 { |
| + t.Fatalf("shut door: status %d", st) |
| + } |
| + if len(sink.snap()) != 1 || len(meter.snapTracks()) != 0 || len(meter.snapDrops()) != dropsBefore { |
| + t.Fatal("a shut door records nothing — no record, no track, no drop") |
| + } |
| // Count off: an origin drop is not recorded either (ruling 2). |
| e.SetSwitches(map[string]telemetry.Switches{key: {Count: false, Door: true}}) |
| postBeacon(t, srv, "/__carlos/e", `{"name":"n"}`, map[string]string{"Origin": "https://evil.example.net"}) |