7.4 KiB
wsapi fixes
Scope: server/internal/wsapi/ only, no commit. All listed findings are fixed; nothing skipped.
What changed per finding
wsapi review:
- Stale token resuming into a reused seat.
seat.heldForrecords the connection whose drop opened the window.handleResumeaccepts only the live prior or the connection the window is held for. A kicked player is answeredsession_not_resumable, and tokens are not revoked at kick. - Inputs queued behind a room's last one.
runnow registerscancellast (so it runs first) andrefusePendingfirst (so it runs last). It answers joins withroom_not_found, resumes withsession_not_resumable, create/bot start withroom_start_failed, submit/resign/claim withnot_in_a_game, and lobby/chat/report withnot_in_a_room. - Control notices in the lossy inbox. Resign is charged on
submitLimiter.leaveRoomuses the new blockingroom.sendReliably.attachcalls it from its own goroutine so two rooms cannot wait on each other.resumeFromanswersbusyfor a live but full room andgame_already_overonly for a finished one. - Quick-match
autoStartoutliving its pairing. The firsthandleJoinconsumes the flag, including on early returns. - Idle window. It restarts only when the room changed (
lobbyChangedread before the broadcast). It also starts or stops when the room crosses between lobby and game, becausebeginGamedoes not setlobbyChanged. A ready toggle counts as activity, as decided. - IPv6 and unmapped-IPv4 keying.
limiterKeyunmaps IPv4-mapped addresses, drops the zone, and folds IPv6 to its /64.clientIPapplies it, so the join limiter, the connection cap and the room budget share one key. - Quick match while draining.
hub.quickMatchreturnserrDrainingfirst. - No
defaultarm. It now answersunknown_message. The code is inerrcodes.go, andvi.jsalready has it (added by the web implementer). TestChatDoesNotKeepARoomAlivenow fails if the idle close comes later thanIdleFor+ 200ms.TestIdleRoomReleasesItsSeatsnow requiresnot_in_a_roomafter the idle close.TestPointKindMappingIsExhaustivenow checks each kind maps to the wire name derived fromString().- A resume calls
hub.cancelQuickMatch. TestOneConnectionCannotStrandRoomsusesawaitNoRooms.
Security review:
- H1a.
session.runarms a 10shelloTimeouttimer that sendshandshake_requiredand closes the socket.handleHellostops it. Tests override it throughhub.helloTimeout, which is unexported. - H1b.
hub.roomLimiteris a keyed limiter on the client address. It sits alongside the per-connection budget, is charged inallowRoom, and is swept insweepLimiters. Budget is 0.5/s with burst 30 (addressRoomsPerSecond,addressRoomBurst). The comment gives the NAT and no-trusted-proxy reasoning, and notes that 0.5/s over the 10 minute idle window is 300 rooms, about 30% of the default ceiling. The per-IP connection cap default is unchanged. - L1. Same as wsapi #6.
- L2. New
corpuslog.go: one process-wide bucket (20/s, burst 100) in front of all threeword_rejectedandword_reportedsites. Suppressed lines feed a new expvarnoitu_corpus_log_suppressed. The next line to get through carriessuppressed_before=N. The per-session limit is kept. - L3.
Server.ServeHTTPsetsX-Content-Type-Options: nosniff,Content-Security-Policy: frame-ancestors 'self'andReferrer-Policy: strict-origin-when-cross-originon every response. The static handler lives inserver.go, somain.gois untouched. HSTS is left to the proxy. - N1.
blankLettersdrops U+115F, U+1160, U+3164, U+FFA0, U+2800, and the Khmer inherent vowels U+17B4 and U+17B5. A name made only of these falls back to the default nickname.
Server-core #3: sanitizeText maps every unicode.IsSpace rune to ' ' first. This also turns VT, FF, NEL and U+2028/2029 into spaces where they used to be dropped. "ngữ pháp" typed with NBSP now reaches the engine as two syllables.
Web #1: a resume into a lobby replays the seat's own GameOver right after RoomState, in the seat's own rendering. broadcastGameOver now renders for every human seat. A detached seat keeps its version in seat.missedResult. It is cleared when replayed, when the game overtakes it, or when the next game begins. The replay is queued in room.resumeReplays and flushed by the run loop right after broadcastRoomState.
Resume frame order for the web client
Resume into a lobby after a game ended during the absence: welcome, chat_history, room_state, game_over.
- The other seats also get their own
room_statebroadcast. - A resume into a running game is unchanged:
welcome,chat_history,game_started,turn_update(if a move exists), thenroom_state. - A resume into a lobby whose game the player saw live gets no extra frame.
- The replayed GameOver has the same shape as a live one. A client that already handles
game_overwhile sitting on a lobby screen needs no change beyond accepting it afterroom_state. - Resume errors:
busyis now a possible answer to a resume (a full inbox).unknown_messageis new. Both are already keys invi.js.
Verification
go vet ./...,gofmt -l .andgolangci-lint run ./...are all clean (0 issues).go test ./internal/wsapi -race -count=3passes (96s).go test ./... -race -count=1passes, all packages.- Old-behaviour check: I copied the module to a scratch directory and reverted each fix by hand, one at a time. The corresponding test failed for every one of these:
- heldFor, refusePending, resign limiter, reliable send, busy answer, autoStart, idle rule
- limiterKey (both the mapped-address and /64 halves), quick-match drain, default arm, hello timer (two variants), address room budget, corpus limiter, headers
- NBSP mapping, blank letters, missed GameOver, resume dequeue
- chat resetting idle (the tightened chat test),
detachAllremoval, swapped PointKind arm
- Existing tests changed:
TestFrameFloodClosesTheConnectionreads until the close instead of five frames, because in-burst empty frames now getunknown_messagereplies.newTestServertakes optionalfunc(*Server)options.
- No bare sleeps as synchronisation:
- The idle tests use pacing sleeps with wall-clock assertions in the direction that a stall cannot fool.
- The hello test orders itself through the silent socket's close.
- The handler-level tests wait on the outbox.
Notes for others
docs/deployment.md(docs owner): document the newnoitu_corpus_log_suppressedcounter, the 10s hello deadline, and the per-address room budget. The per-IP connection cap default is unchanged, so H1's deployment half (setNOITU_TRUSTED_PROXIESandNOITU_MAX_CONNECTIONS_PER_IP) is still an operator task.- Limitation in the exit fix:
room.sendchecksctx.Doneand then enqueues, so a send that passes the check exactly as the room exits can still land after the final drain. This is a much narrower window than before (nanoseconds, not the whole queue), and closing it fully would need a lock on the hot path. - No proto change.
Status: DONE
Summary: All 13 wsapi findings, security H1, L1, L2, L3 and N1, server-core #3 and the web resume GameOver replay are implemented in server/internal/wsapi/ with tests. Vet, gofmt, golangci-lint, wsapi -race -count=3 and the full suite are clean.
Concerns/Blockers: none blocking. The tiny residual window in room exit is noted above. The deployment half of H1 (trusted proxies and per-IP cap) still needs the operator.