fix(wsapi): loosen the join limiter and answer a non-resumable token

joinsPerSecond/joinBurst at 1/5 was tight enough to refuse a whole
NAT egress sharing one address, not just a room-code brute force.
Raised to 5/20, which still takes centuries to walk the 31^6 room
code space.

handleHello stayed silent on a resume token that did not resolve to a
live session, leaving a client's resume latch waiting forever. It now
answers session_not_resumable and carries on as a fresh session.
This commit is contained in:
tiennm99 committed 2026-09-21 16:13:30 +07:00
1 parent e3efadfd63
commit eae8d42f25
1 file changed
+19 -3
+19 -3
View File
@@ -47,8 +47,15 @@ const (
chatsPerSecond = 2.0
chatBurst = 5
joinsPerSecond = 1
joinBurst = 5
// joinsPerSecond and joinBurst bound how many rooms one address may join
// or attempt to join. The limiter exists to slow a brute-force walk of the
// room-code space (31 characters over 6 places, ~8.9e8 codes) to
// centuries even at this rate — it is not meant to ration ordinary play.
// A single NAT/CGNAT egress (a café, a school, a mobile carrier) can be
// many real players sharing one address, so the budget has to be generous
// enough for a whole one of those, not just one person.
joinsPerSecond = 5
joinBurst = 20
// maxWordReportsPerSession bounds how many distinct words one session may
// file with ReportWord. A duplicate report of a word already filed does
@@ -640,8 +647,17 @@ func (s *session) handleHello(h *noituv1.Hello) error {
s.hub.register(s)
s.send(welcomeMsg(s.id, s.resumeToken, s.nickname()))
if prior, ok := s.hub.resumable(h.GetResumeToken()); ok && prior != s {
token := h.GetResumeToken()
switch prior, ok := s.hub.resumable(token); {
case ok && prior != s:
s.resumeFrom(prior)
case token != "" && !ok:
// A token the server restarted since, or that outlived its grace
// window, resolves to nothing. Silence here left the client's resume
// latch waiting forever for a reply that was never coming — this
// connection is answered and carries on as a fresh session instead of
// being closed, since a fresh session is exactly what it is.
s.send(errorMsg("session_not_resumable"))
}
return nil
}