package callstest import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "log/slog" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "amadan.net/oficina/calls/internal/calls" "amadan.net/oficina/calls/internal/calls/relay" ) const vectorKey = "uMhwW4P/9O5dQUIs91t2CCHGhwJbeiM0xQMQ2G0CJLU=" func relayContract(t *testing.T) string { t.Helper() b, _ := json.Marshal([]map[string]any{{ "region": "eu-west-1", "urls": []string{"stun:relay.eu-west-1.oncarlos.com:3478", "turn:relay.eu-west-1.oncarlos.com:3478?transport=udp"}, "key": vectorKey, "username_suffix": "abc:meet:0123456789abcdef:k1", }}) return base64.StdEncoding.EncodeToString(b) } type iceAnswer struct { ICEServers []relay.ICEServer `json:"iceServers"` ExpiresAt int64 `json:"expiresAt"` RenewAt int64 `json:"renewAt"` } // guestJoinedAndWaiting signs a fresh outsider in through tok, opens its // stream and posts join without anyone admitting it — the waiting shape // isolation_test.go's openIsolationRoom also builds inline, lifted here // for a single guest. func guestJoinedAndWaiting(t *testing.T, x *harness, sqid, tok string) (*client, string) { enableWaitingRoom(t, x, sqid) t.Helper() g := x.browser(t) g.get("/j/" + tok) g.signIn("waiting-guest@example.com") s := g.stream("/m/" + sqid + "/stream") t.Cleanup(s.close) peer, _ := s.hello() if resp, b := g.signal(sqid, peer, `{"type":"join"}`); resp.StatusCode != http.StatusOK { t.Fatalf("guest join (the knock): %d %s", resp.StatusCode, b) } return g, peer } func TestIceHandsAnAdmittedParticipationCredentialsThatVerify(t *testing.T) { x, sqid, _ := memberWithMeeting(t) if err := x.ctl.SetRelay(relayContract(t)); err != nil { t.Fatal(err) } c := x.browser(t) c.signIn("paul@example.com") s := c.stream("/m/" + sqid + "/stream") defer s.close() peer, _ := s.hello() if resp, b := c.signal(sqid, peer, `{"type":"join"}`); resp.StatusCode != http.StatusOK { t.Fatalf("member join: %d %s", resp.StatusCode, b) } s.nextOf("roster", 2*time.Second) rec := c.getStream("/m/" + sqid + "/ice?peer=" + peer) // any bounded GET helper; the Accept header is harmless if rec.Code != http.StatusOK || rec.Header().Get("Cache-Control") != "no-store" { t.Fatalf("ice: %d %q", rec.Code, rec.Header().Get("Cache-Control")) } var ans iceAnswer if err := json.Unmarshal(rec.Body.Bytes(), &ans); err != nil { t.Fatal(err) } if len(ans.ICEServers) != 2 || ans.ICEServers[0].Username != "" || ans.ICEServers[1].Username == "" { t.Fatalf("servers %+v", ans.ICEServers) } now := x.now if ans.ExpiresAt < now.Add(3*time.Hour-time.Second).Unix() || ans.ExpiresAt > now.Add(3*time.Hour+time.Second).Unix() { t.Fatalf("expiry %d is not three hours ahead of %d", ans.ExpiresAt, now.Unix()) } if ans.RenewAt != now.Add(150*time.Minute).Unix() { t.Fatalf("renewAt %d", ans.RenewAt) } // The credential verifies against the vector key for the username the server chose. key, _ := base64.StdEncoding.DecodeString(vectorKey) turn := ans.ICEServers[1] if relay.Credential(key, turn.Username) != turn.Credential { t.Fatal("credential does not verify") } if want := fmt.Sprintf("%d:abc:meet:0123456789abcdef:k1", ans.ExpiresAt); turn.Username != want { t.Fatalf("username %q, want %q", turn.Username, want) } // The wire shape itself, not just the typed struct's own tags: // relay's own golden test pins Mint's output in isolation, but // nothing before this pinned what actually crosses this endpoint — // a bare entry carries only urls, a turn entry carries urls, // username and credential and nothing else. var raw struct { ICEServers []map[string]json.RawMessage `json:"iceServers"` } if err := json.Unmarshal(rec.Body.Bytes(), &raw); err != nil { t.Fatal(err) } if len(raw.ICEServers) != 2 { t.Fatalf("want two servers over the wire, got %d", len(raw.ICEServers)) } for k := range raw.ICEServers[0] { if k != "urls" { t.Fatalf("bare entry over the wire carries an extra key %q", k) } } for _, want := range []string{"urls", "username", "credential"} { if _, ok := raw.ICEServers[1][want]; !ok { t.Fatalf("turn entry over the wire is missing %q", want) } } for k := range raw.ICEServers[1] { if k != "urls" && k != "username" && k != "credential" { t.Fatalf("turn entry over the wire carries an extra key %q", k) } } } // TestIceRenewAtIsFiveSixthsOfTheLife is a tripwire for the client, not // for this endpoint. room.js schedules its renewal from the *margin* // this answer leaves it — expiresAt minus renewAt, a subtraction of two // instants on one clock, and therefore the one reading immune to a // browser whose clock disagrees with ours — and it multiplies that // margin by five because the contract's shape says the margin is a // sixth of the life (§5.3 step 4). Move iceLife or iceRenewAfter without // the other and that arithmetic quietly starts scheduling renewals after // the credential it is renewing has already died. The client also reads // the clock directly and takes whichever answer is earlier, so this // would not be fatal — but it would silently cost the schedule its only // skew-proof half, and nothing else in the tree would notice. func TestIceRenewAtIsFiveSixthsOfTheLife(t *testing.T) { x, sqid, _ := memberWithMeeting(t) if err := x.ctl.SetRelay(relayContract(t)); err != nil { t.Fatal(err) } c := x.browser(t) c.signIn("paul@example.com") s := c.stream("/m/" + sqid + "/stream") defer s.close() peer, _ := s.hello() if resp, b := c.signal(sqid, peer, `{"type":"join"}`); resp.StatusCode != http.StatusOK { t.Fatalf("member join: %d %s", resp.StatusCode, b) } s.nextOf("roster", 2*time.Second) rec := c.getStream("/m/" + sqid + "/ice?peer=" + peer) var ans iceAnswer if err := json.Unmarshal(rec.Body.Bytes(), &ans); err != nil { t.Fatal(err) } now := x.now.Unix() life, renew, margin := ans.ExpiresAt-now, ans.RenewAt-now, ans.ExpiresAt-ans.RenewAt if renew*6 != life*5 { t.Fatalf("renewAt is not five sixths of the life: life %ds, renew %ds", life, renew) } if margin*6 != life { t.Fatalf("the margin the client multiplies by five is not a sixth of the life: margin %ds, life %ds", margin, life) } } func TestIceIs404ForAnythingButAnAdmittedParticipationOfThisSession(t *testing.T) { x, sqid, tok := memberWithMeeting(t) if err := x.ctl.SetRelay(relayContract(t)); err != nil { t.Fatal(err) } c := x.browser(t) c.signIn("paul@example.com") s := c.stream("/m/" + sqid + "/stream") defer s.close() peer, _ := s.hello() unjoined := c.getStream("/m/" + sqid + "/ice?peer=" + peer) if unjoined.Code != http.StatusNotFound { t.Fatalf("unjoined: %d", unjoined.Code) } other := x.browser(t) other.signIn("paul@rslw.com") foreign := other.getStream("/m/" + sqid + "/ice?peer=" + peer) if foreign.Code != http.StatusNotFound || !bytes.Equal(foreign.Body.Bytes(), unjoined.Body.Bytes()) { t.Fatalf("another session must get the identical 404: %d %s vs %s", foreign.Code, foreign.Body.Bytes(), unjoined.Body.Bytes()) } // A real /signal 404, from the same unbound session, must be the // identical body too: both endpoints share one not-found shape // (spec §2.5), not merely two handlers that happen to agree today. signalResp, signalBody := other.signal(sqid, peer, `{"type":"leave"}`) if signalResp.StatusCode != http.StatusNotFound || !bytes.Equal(signalBody, unjoined.Body.Bytes()) { t.Fatalf("a real /signal 404 must be byte-identical: %d %s vs %s", signalResp.StatusCode, signalBody, unjoined.Body.Bytes()) } bad := c.getStream("/m/" + sqid + "/ice?peer=zz") if bad.Code != http.StatusNotFound || !bytes.Equal(bad.Body.Bytes(), unjoined.Body.Bytes()) { t.Fatal("a malformed peer is the same 404") } // "2222222222" is validSqid's own shape (ten characters, every one // in its alphabet) naming a room that does not exist; "0000000000" // is not that shape at all — '0' is never in sqidAlphabet — so the // two exercise different refusal arms (an unknown room vs a // malformed one) and both must still be the one 404. room := c.getStream("/m/2222222222/ice?peer=" + peer) if room.Code != http.StatusNotFound || !bytes.Equal(room.Body.Bytes(), unjoined.Body.Bytes()) { t.Fatal("an unknown room is the same 404") } malformedRoom := c.getStream("/m/0000000000/ice?peer=" + peer) if malformedRoom.Code != http.StatusNotFound || !bytes.Equal(malformedRoom.Body.Bytes(), unjoined.Body.Bytes()) { t.Fatal("a malformed sqid is the same 404") } // A waiting guest: sign a guest in through the link and join; 404. g, gpeer := guestJoinedAndWaiting(t, x, sqid, tok) waiting := g.getStream("/m/" + sqid + "/ice?peer=" + gpeer) if waiting.Code != http.StatusNotFound || !bytes.Equal(waiting.Body.Bytes(), unjoined.Body.Bytes()) { t.Fatal("a waiting guest is the same 404") } } func TestIceWithNoRelayIsAnEmptyList(t *testing.T) { x, sqid, _ := memberWithMeeting(t) c := x.browser(t) c.signIn("paul@example.com") s := c.stream("/m/" + sqid + "/stream") defer s.close() peer, _ := s.hello() if resp, b := c.signal(sqid, peer, `{"type":"join"}`); resp.StatusCode != http.StatusOK { t.Fatalf("member join: %d %s", resp.StatusCode, b) } s.nextOf("roster", 2*time.Second) rec := c.getStream("/m/" + sqid + "/ice?peer=" + peer) if rec.Code != http.StatusOK || rec.Body.String() != "{\"expiresAt\":0,\"iceServers\":[],\"renewAt\":0}\n" { t.Fatalf("%d %q", rec.Code, rec.Body.String()) } } // TestIceTestTTLHeaderIsHonouredOnlyInDevelopment is stream_test.go's // TestTheTestTTLHeaderIsDevelopmentOnly over HTTP: EventSource cannot // set a header, but a plain GET can, so this one needs no browser gate // to reach the handler — only the development check does, and this // test is what proves it. func TestIceTestTTLHeaderIsHonouredOnlyInDevelopment(t *testing.T) { withHeader := func(c *client, target, ttl string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodGet, target, nil) req.Header.Set("X-Calls-Test-Ice-TTL", ttl) return c.do(req) } t.Run("development", func(t *testing.T) { x, sqid, _ := memberWithMeeting(t) if err := x.ctl.SetRelay(relayContract(t)); err != nil { t.Fatal(err) } c := x.browser(t) c.signIn("paul@example.com") s := c.stream("/m/" + sqid + "/stream") defer s.close() peer, _ := s.hello() c.signal(sqid, peer, `{"type":"join"}`) // Twelve seconds, not two: expiresAt and renewAt are both // truncated to whole seconds by Unix(), and life*5/6's fractional // remainder can tie the two together at a short duration. Twelve // gives a two-second gap (12s vs 10s) that survives the rounding. rec := withHeader(c, "/m/"+sqid+"/ice?peer="+peer, "12s") if rec.Code != http.StatusOK { t.Fatalf("development with header: %d %s", rec.Code, rec.Body.String()) } var ans iceAnswer if err := json.Unmarshal(rec.Body.Bytes(), &ans); err != nil { t.Fatal(err) } now := x.now if ans.ExpiresAt < now.Unix() || ans.ExpiresAt > now.Add(15*time.Second).Unix() { t.Fatalf("expiresAt %d is not ~12s ahead of %d", ans.ExpiresAt, now.Unix()) } if ans.RenewAt >= ans.ExpiresAt { t.Fatalf("renewAt %d is not before expiresAt %d", ans.RenewAt, ans.ExpiresAt) } }) t.Run("production", func(t *testing.T) { x := newHarnessWithLogger(t, "production", slog.New(slog.NewTextHandler(io.Discard, nil)), "paul@example.com") if err := x.ctl.SetRelay(relayContract(t)); err != nil { t.Fatal(err) } c := x.browser(t) c.signIn("paul@example.com") sqid, _ := createMeeting(t, c, "Board meeting", "") s := c.stream("/m/" + sqid + "/stream") defer s.close() peer, _ := s.hello() c.signal(sqid, peer, `{"type":"join"}`) rec := withHeader(c, "/m/"+sqid+"/ice?peer="+peer, "12s") if rec.Code != http.StatusOK { t.Fatalf("production with header: %d %s", rec.Code, rec.Body.String()) } var ans iceAnswer if err := json.Unmarshal(rec.Body.Bytes(), &ans); err != nil { t.Fatal(err) } now := x.now if ans.ExpiresAt < now.Add(3*time.Hour-time.Second).Unix() || ans.ExpiresAt > now.Add(3*time.Hour+time.Second).Unix() { t.Fatalf("production must ignore the header: expiresAt %d, want ~3h ahead of %d", ans.ExpiresAt, now.Unix()) } }) } // TestIceLogsNothingOnSuccessAndNoCredentialOnRefusal is // signal_test.go's TestSignallingLogsCarryNoContent for this endpoint: // a successful call grows the log buffer by not one byte (the answer // itself carries the credential, so even a line mentioning the room // alone would be a leash on it), and once a real credential has been // minted, neither its authority refusal's line, nor any other line // this test produces, may ever contain that exact minted username or // credential — not merely the vector key or a hardcoded substring. func TestIceLogsNothingOnSuccessAndNoCredentialOnRefusal(t *testing.T) { var buf bytes.Buffer x := newHarnessWithLogger(t, calls.EnvDevelopment, slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})), "paul@example.com", "paul@rslw.com") if err := x.ctl.SetRelay(relayContract(t)); err != nil { t.Fatal(err) } c := x.browser(t) c.signIn("paul@example.com") sqid, _ := createMeeting(t, c, "Board meeting", "") s := c.stream("/m/" + sqid + "/stream") defer s.close() peer, _ := s.hello() if resp, b := c.signal(sqid, peer, `{"type":"join"}`); resp.StatusCode != http.StatusOK { t.Fatalf("member join: %d %s", resp.StatusCode, b) } before := buf.Len() rec := c.getStream("/m/" + sqid + "/ice?peer=" + peer) if rec.Code != http.StatusOK { t.Fatalf("ice: %d %s", rec.Code, rec.Body.String()) } if buf.Len() != before { t.Fatalf("success grew the log by %d bytes: %q", buf.Len()-before, buf.String()[before:]) } var ans iceAnswer if err := json.Unmarshal(rec.Body.Bytes(), &ans); err != nil { t.Fatal(err) } if len(ans.ICEServers) != 2 || ans.ICEServers[1].Username == "" || ans.ICEServers[1].Credential == "" { t.Fatalf("no minted credential to check the log against: %+v", ans.ICEServers) } turn := ans.ICEServers[1] // An authority refusal: a foreign session naming this session's own // admitted peer. other := x.browser(t) other.signIn("paul@rslw.com") if rec := other.getStream("/m/" + sqid + "/ice?peer=" + peer); rec.Code != http.StatusNotFound { t.Fatalf("foreign session: %d", rec.Code) } // A shape refusal too, for the same reasons signal_test.go's own // content test covers more than one refusal path. if rec := c.getStream("/m/" + sqid + "/ice?peer=zz"); rec.Code != http.StatusNotFound { t.Fatalf("malformed peer: %d", rec.Code) } logs := buf.String() if !strings.Contains(logs, "calls: ice refused") { t.Fatalf("the refusal was not logged:\n%s", logs) } for _, forbidden := range []string{vectorKey, ":abc:meet:", turn.Username, turn.Credential} { if strings.Contains(logs, forbidden) { t.Fatalf("log leaked a credential (%q):\n%s", forbidden, logs) } } } // TestIceRefusesAnUnboundSessionBeforeTheStoreOrTheLimiter is // signal.go's own LiveForSession gate (review Task 5 item 1), carried // over to this endpoint: a session with no live participation in this // room at all is refused before either the store or the limiter is // ever touched. The meetings table is dropped first, so a store read // that happened anyway would 500, not 404 — and the limiter's own map // staying empty is the second, independent witness that nothing was // spent for a room this session holds nothing in. func TestIceRefusesAnUnboundSessionBeforeTheStoreOrTheLimiter(t *testing.T) { x, sqid, _ := memberWithMeeting(t) c := x.browser(t) c.signIn("paul@example.com") // No stream opened at all: this session has no live participation // anywhere, in this room least of all. if err := x.db.G.Exec("DROP TABLE meetings").Error; err != nil { t.Fatal(err) } rec := c.getStream("/m/" + sqid + "/ice?peer=" + strings.Repeat("a", 32)) if rec.Code != http.StatusNotFound { t.Fatalf("unbound session: %d, want 404 (a store read would have hit the dropped table and 500ed)", rec.Code) } if n := x.ctl.SignalLimiterSize(); n != 0 { t.Fatalf("the limiter minted a bucket for an unbound session: %d keys", n) } } // TestIceSpendsTheSignalEndpointsOwnBucket is the shared-bucket claim // itself, which the burst test below cannot make: a limiter of /ice's // own would pass that one exactly as a shared one does. So this // exhausts the budget entirely through POST /signal and then asks /ice // for a list — one request, on a participation that is admitted and // would otherwise be answered 200 (Codex S3-7). func TestIceSpendsTheSignalEndpointsOwnBucket(t *testing.T) { x, sqid, _ := memberWithMeeting(t) if err := x.ctl.SetRelay(relayContract(t)); err != nil { t.Fatal(err) } c := x.browser(t) c.signIn("paul@example.com") s := c.stream("/m/" + sqid + "/stream") defer s.close() peer, _ := s.hello() if resp, b := c.signal(sqid, peer, `{"type":"join"}`); resp.StatusCode != http.StatusOK { t.Fatalf("member join: %d %s", resp.StatusCode, b) } // One list before the burst, so the 429 below cannot be a refusal // this participation would have got anyway. if rec := c.getStream("/m/" + sqid + "/ice?peer=" + peer); rec.Code != http.StatusOK { t.Fatalf("ice before the burst: %d %s", rec.Code, rec.Body.String()) } // Now spend the budget on the *other* endpoint. A candidate to a // peer that is not there is refused on its merits, and every refusal // still costs a token: the limiter runs in front of the handler. spent := 0 for i := 0; i < 200; i++ { resp, _ := c.signal(sqid, peer, `{"type":"candidate","to":"`+strings.Repeat("b", 32)+`","seq":1,"gen":1,"payload":{}}`) spent++ if resp.StatusCode == http.StatusTooManyRequests { break } } if spent >= 200 { t.Fatal("200 signal posts never hit the rate limit, so nothing was exhausted") } rec := c.getStream("/m/" + sqid + "/ice?peer=" + peer) if rec.Code != http.StatusTooManyRequests { t.Fatalf("ice after %d signal posts exhausted the bucket: %d %s — /ice has a limiter of its own", spent, rec.Code, rec.Body.String()) } if rec.Header().Get("Retry-After") == "" { t.Fatal("429 without Retry-After") } } // TestIceRateLimitsOneSessionsBurst proves /ice is behind the limiter at // all: a hundred requests in a row are refused before the hundredth. // The shared-bucket half is the test above. func TestIceRateLimitsOneSessionsBurst(t *testing.T) { x, sqid, _ := memberWithMeeting(t) if err := x.ctl.SetRelay(relayContract(t)); err != nil { t.Fatal(err) } c := x.browser(t) c.signIn("paul@example.com") s := c.stream("/m/" + sqid + "/stream") defer s.close() peer, _ := s.hello() if resp, b := c.signal(sqid, peer, `{"type":"join"}`); resp.StatusCode != http.StatusOK { t.Fatalf("member join: %d %s", resp.StatusCode, b) } var got429 bool for i := 0; i < 100; i++ { rec := c.getStream("/m/" + sqid + "/ice?peer=" + peer) if rec.Code == http.StatusTooManyRequests { if rec.Header().Get("Retry-After") == "" { t.Fatal("429 without Retry-After") } got429 = true break } if rec.Code != http.StatusOK { t.Fatalf("unexpected status mid-burst: %d %s", rec.Code, rec.Body.String()) } } if !got429 { t.Fatal("100 requests in a burst never hit the rate limit") } } // TestIceHeaderClampsExtremeValues is item 3's clamp: // life = min(max(d, minIceLife), iceLife). A zero or negative header // floors to the two-second minimum; an unparseable one is ignored // outright (the untouched three-hour default); an oversized one is // capped at the three-hour default rather than minting a credential // that outlives it. renewAt is always strictly before expiresAt. // // The harness clock is truncated to a whole second first: expiresAt // and renewAt are both truncated to whole seconds by Unix(), and the // floor case's own two-thirds-of-a-second gap (2s vs on the order of // 1.667s) is not reliably on the same side of a rounding boundary // starting from a fractional second. func TestIceHeaderClampsExtremeValues(t *testing.T) { x, sqid, _ := memberWithMeeting(t) if err := x.ctl.SetRelay(relayContract(t)); err != nil { t.Fatal(err) } x.now = x.now.Truncate(time.Second) x.advance(0) c := x.browser(t) c.signIn("paul@example.com") s := c.stream("/m/" + sqid + "/stream") defer s.close() peer, _ := s.hello() if resp, b := c.signal(sqid, peer, `{"type":"join"}`); resp.StatusCode != http.StatusOK { t.Fatalf("member join: %d %s", resp.StatusCode, b) } ask := func(ttl string) iceAnswer { t.Helper() req := httptest.NewRequest(http.MethodGet, "/m/"+sqid+"/ice?peer="+peer, nil) req.Header.Set("X-Calls-Test-Ice-TTL", ttl) rec := c.do(req) if rec.Code != http.StatusOK { t.Fatalf("ttl=%q: %d %s", ttl, rec.Code, rec.Body.String()) } var ans iceAnswer if err := json.Unmarshal(rec.Body.Bytes(), &ans); err != nil { t.Fatal(err) } return ans } now := x.now cases := []struct { name string ttl string wantExpiresAt, wantRenewAt int64 }{ {"zero clamps to the floor", "0s", now.Add(2 * time.Second).Unix(), now.Add(1 * time.Second).Unix()}, {"negative clamps to the floor", "-5s", now.Add(2 * time.Second).Unix(), now.Add(1 * time.Second).Unix()}, {"unparseable falls back to the default", "not a duration", now.Add(3 * time.Hour).Unix(), now.Add(150 * time.Minute).Unix()}, {"an outsized value clamps to the ceiling", "10h", now.Add(3 * time.Hour).Unix(), now.Add(150 * time.Minute).Unix()}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { ans := ask(tc.ttl) if ans.ExpiresAt != tc.wantExpiresAt { t.Errorf("ttl=%q: expiresAt %d, want %d", tc.ttl, ans.ExpiresAt, tc.wantExpiresAt) } if ans.RenewAt != tc.wantRenewAt { t.Errorf("ttl=%q: renewAt %d, want %d", tc.ttl, ans.RenewAt, tc.wantRenewAt) } if ans.RenewAt >= ans.ExpiresAt { t.Errorf("ttl=%q: renewAt %d is not before expiresAt %d", tc.ttl, ans.RenewAt, ans.ExpiresAt) } }) } } // TestIceRefusesAnEndedMeetingEvenForAStillAdmittedGuest proves ice.go's // own ended_at read is load-bearing on its own: the meeting is ended // directly in the store here, never through the real end-meeting // handler (which also calls hub.End and erases the hub's own record of // the room) — so the hub still believes this guest is live and // admitted right up to the request this test makes. Only ice.go's own // store read stands between that stale hub state and a 200. func TestIceRefusesAnEndedMeetingEvenForAStillAdmittedGuest(t *testing.T) { x, sqid, tok := memberWithMeeting(t) enableWaitingRoom(t, x, sqid) if err := x.ctl.SetRelay(relayContract(t)); err != nil { t.Fatal(err) } m := x.browser(t) m.signIn("paul@example.com") sm := m.stream("/m/" + sqid + "/stream") defer sm.close() pm, _ := sm.hello() if resp, b := m.signal(sqid, pm, `{"type":"join"}`); resp.StatusCode != http.StatusOK { t.Fatalf("member join: %d %s", resp.StatusCode, b) } g := x.browser(t) g.get("/j/" + tok) g.signIn("guest@example.com") sg := g.stream("/m/" + sqid + "/stream") defer sg.close() pg, _ := sg.hello() if resp, b := g.signal(sqid, pg, `{"type":"join"}`); resp.StatusCode != http.StatusOK { t.Fatalf("guest join: %d %s", resp.StatusCode, b) } if resp, b := m.signal(sqid, pm, `{"type":"admit","payload":{"peer":"`+pg+`"}}`); resp.StatusCode != http.StatusOK { t.Fatalf("admit: %d %s", resp.StatusCode, b) } if ev, _, ok := sg.nextOf("roster", 2*time.Second); !ok { t.Fatalf("the guest was never admitted (got %q)", ev) } // A baseline 404 from an unrelated, unbound session, established // before the meeting ends: every ice.go refusal shares this one // body, so it stands in for a.notFoundBody() without reaching into // the app package. baseline := x.browser(t) baseline.signIn("paul@rslw.com") base := baseline.getStream("/m/" + sqid + "/ice?peer=" + pg) if base.Code != http.StatusNotFound { t.Fatalf("baseline 404: %d", base.Code) } if err := x.db.G.Exec("UPDATE meetings SET ended_at = ? WHERE sqid = ?", x.now, sqid).Error; err != nil { t.Fatal(err) } rec := g.getStream("/m/" + sqid + "/ice?peer=" + pg) if rec.Code != http.StatusNotFound || !bytes.Equal(rec.Body.Bytes(), base.Body.Bytes()) { t.Fatalf("ended meeting: %d %s, want the same 404 as %s", rec.Code, rec.Body.Bytes(), base.Body.Bytes()) } } // TestIceNeverLogsAMalformedPeerVerbatim is validPeerID's load-bearing // proof for this endpoint, signal.go's own precedent (review Important // 2): a peer that fails validation is never logged, whatever it // contains — a newline that could forge a second log line, or a length // that could bloat one. func TestIceNeverLogsAMalformedPeerVerbatim(t *testing.T) { var buf bytes.Buffer x := newHarnessWithLogger(t, calls.EnvDevelopment, slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})), "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") sqid, _ := createMeeting(t, c, "Board meeting", "") s := c.stream("/m/" + sqid + "/stream") defer s.close() s.hello() needle := "forged-log-line\n" + strings.Repeat("z", 4096) rec := c.getStream("/m/" + sqid + "/ice?peer=" + url.QueryEscape(needle)) if rec.Code != http.StatusNotFound { t.Fatalf("malformed peer: %d", rec.Code) } if strings.Contains(buf.String(), needle) || strings.Contains(buf.String(), "forged-log-line") { t.Fatalf("the malformed peer reached the log:\n%s", buf.String()) } } // TestIceNeverLogsAMalformedRoomOrMintsABucketForOne is the room half of // the same guard, and it is written to fail if validSqid is taken out of // the handler. The old assertion could not: it checked only the status, // and an unchecked sqid is refused a step later anyway when // LiveForSession finds no participation there (Codex S3-7). Two things // only the shape guard can give: a log line that names the room as // `invalid` rather than repeating four kilobytes of someone else's // text, and a limiter that mints no bucket for a room id that cannot // exist — the key is session+room, so an unchecked one is an unbounded // space of keys anybody can fill. func TestIceNeverLogsAMalformedRoomOrMintsABucketForOne(t *testing.T) { var buf bytes.Buffer x := newHarnessWithLogger(t, calls.EnvDevelopment, slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})), "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") needle := "forged-room-line\n" + strings.Repeat("q", 4096) rec := c.getStream("/m/" + url.PathEscape(needle) + "/ice?peer=" + strings.Repeat("a", 32)) if rec.Code != http.StatusNotFound { t.Fatalf("malformed room: %d %s", rec.Code, rec.Body.String()) } log := buf.String() if strings.Contains(log, "forged-room-line") || strings.Contains(log, strings.Repeat("q", 64)) { t.Fatalf("the malformed room reached the log:\n%s", log) } if strings.Contains(log, "ice refused") && !strings.Contains(log, "room=invalid") { t.Fatalf("a refusal was logged without naming the room `invalid`:\n%s", log) } if n := x.ctl.SignalLimiterSize(); n != 0 { t.Fatalf("the limiter minted a bucket for a room id that cannot exist: %d keys", n) } }