Files
OwnCord/Server/ws/voice_broadcast.go
T
jevbandclaude-flow c53d63da47 feat: comprehensive spec docs, test suite, E2E overhaul, and security hardening
Spec Documentation (18 files, 680KB):
- Expanded all 15 existing spec files with deep detail from source code
- Created 3 new specs: DM-SYSTEM, THEME-SYSTEM, RECONNECTION
- Created E2E-BEST-PRACTICES spec
- Audited all specs against source: fixed 50 errors

Unit Tests (143 new):
- Go: dm_queries_test (21), dm_handler_test (17), dm_handlers_test (18), ringbuffer_test (22)
- TS: dm-store (16), disposable (14), themes security (17), ws reconnection (8), dispatcher DM (2)

E2E Tests (22 mocked + 6 native specs):
- New: dm-system, theme-persistence, reconnection (mocked + native)
- Fixed 12 fake assertions, 18 hardcoded timeouts, 5 stale selectors
- Persistent fixture: login once per run instead of per test
- ensureLoggedIn with exponential backoff for rate limiting

Security Fixes:
- DM auth bypass: added IsDMParticipant to handleGetPins, handleSetPinned, handleSearch
- LiveKit InsecureVerifier replaced with PinnedVerifier (TOFU from shared cert store)
- IDOR leak: handleChatEdit/Delete now return opaque error codes
- CSS injection: added deny-list for dangerous CSS functions in themes
- BANNED error now triggers logout instead of infinite reconnect
- CredFree leak fixed: Windows credential memory freed before parsing
- Login lockout off-by-one: limit=9 so 10th failure triggers lockout

Stability Fixes:
- Rate limiter StartCleanup goroutine now started (prevents memory leak)
- Voice mute/deafen rate limiting added (2/sec, matching camera/screenshare)
- DM typing no longer echoes back to sender
- Accept loop spin protection (5 consecutive error limit)
- voice_config protocol drift resolved (3 missing fields added)
- Login rate limit set to 60/min (spec updated, 10-failure lockout is real protection)
- Hardcoded roleNameToId replaced with dynamic lookup from ready payload

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-28 10:39:23 +01:00

51 lines
1.5 KiB
Go

package ws
import (
"log/slog"
"time"
)
// Voice rate limit settings.
const (
voiceMuteRateLimit = 2
voiceMuteWindow = time.Second
voiceDeafenRateLimit = 2
voiceDeafenWindow = time.Second
voiceCameraRateLimit = 2
voiceCameraWindow = time.Second
voiceScreenshareRateLimit = 2
voiceScreenshareWindow = time.Second
)
// voiceQualities maps accepted voice quality presets to their target bitrate
// in bits/s. This is the single source of truth — voice_join.go validates
// against these keys, qualityBitrate looks up the value.
var voiceQualities = map[string]int{
"low": 32000,
"medium": 64000,
"high": 128000,
}
// qualityBitrate returns the target audio bitrate in bits/s based on a quality preset.
func qualityBitrate(quality string) int {
if bitrate, ok := voiceQualities[quality]; ok {
return bitrate
}
return voiceQualities["medium"]
}
// broadcastVoiceStateUpdate fetches the current voice state for the client
// and broadcasts it to all members of the voice channel they are in.
func (h *Hub) broadcastVoiceStateUpdate(c *Client) {
state, err := h.db.GetVoiceState(c.userID)
if err != nil {
slog.Error("ws broadcastVoiceStateUpdate GetVoiceState", "err", err, "user_id", c.userID)
c.sendMsg(buildErrorMsg(ErrCodeInternal, "failed to broadcast voice state update"))
return
}
if state == nil {
return // user not in a voice channel — nothing to broadcast
}
h.BroadcastToAll(buildVoiceState(*state))
}