Files
OwnCord/Server/api/diagnostics_handler.go
T
J3vb c3a8aa477c fix: resolve 20 code review bugs across Rust, TypeScript, and Go
Critical/High Rust (Tauri client):
- BUG-140: replace .run() with .build() + RunEvent::Exit handler; native error dialog on startup failure
- BUG-141: eliminate PTT thread TOCTOU race with Mutex critical section; add AtomicBool shutdown and catch_unwind
- BUG-144: fix TOFU cert store corruption — read-before-write rollback restores previous fingerprint on save failure (all 3 write sites)
- BUG-145: add VK code range guard (1..=254) in is_key_down; fix cast to (state as i16) < 0
- BUG-147: replace bare spawns with JoinSet; abort_all + drain on exit; unconditional closed event
- BUG-150: add CRLF guard in handle_connection before header rewriting
- BUG-151: wrap header read loop in tokio::time::timeout(10s)
- BUG-158: extract CERTS_STORE/SETTINGS_STORE to constants.rs (eliminate 3 duplicates)
- HIGH-2: PTT thread self-cleanup uses unwrap_or_else defensive pattern
- HIGH-4: ws_send distinguishes Full vs Closed errors; warn log on backpressure

Critical/High TypeScript (Tauri client):
- BUG-142: join-generation counter prevents stale connectAndSetup completions
- BUG-143: replace 8 mutable LiveKit session fields with discriminated union SessionState
- BUG-146: 60s token refresh deadline; cleared on reply or voice leave
- BUG-148: ResizeObserver hoisted to outer scope; disconnect() in destroy() before ac.abort()
- BUG-152: dismissSignal.aborted guard already present (no change needed)
- BUG-153: measureRendered split into two-pass read-then-write; eliminates per-message reflow
- BUG-154: WS dedup cache batch-evicts to 80% on overflow (amortised O(1))
- BUG-157: pendingUpdates replaced with coalesced function-composition slot (O(1) queue depth)

Go server:
- BUG-149: safe two-value type assertion in getOutboundIP with localhost fallback
- BUG-155: broadcast buffer 256→1024; broadcastDrops atomic counter exposed in /api/v1/metrics
- BUG-156: LiveKitHealthCheck and implementations accept ctx context.Context; all call sites pass r.Context() (12 files)
- BUG-159: MaxMessageBytes constant in config/constants.go; replaces 1<<20 literals in serve.go and updater.go
- HIGH-1: cert store rollback reads old value before write; restores previous cert on save failure

All validation passes: go build, go vet, cargo check, npm typecheck
2026-04-03 23:18:06 +02:00

99 lines
2.4 KiB
Go

package api
import (
"net/http"
"net/url"
"runtime"
"time"
"github.com/owncord/server/config"
"github.com/owncord/server/ws"
)
// diagnosticsResponse is returned by GET /api/v1/diagnostics/connectivity.
type diagnosticsResponse struct {
Server serverDiag `json:"server"`
Voice voiceDiag `json:"voice"`
Client clientDiag `json:"client"`
}
type serverDiag struct {
Version string `json:"version"`
Uptime int64 `json:"uptime_s"`
GoVersion string `json:"go_version"`
OnlineUsers int `json:"online_users"`
}
type voiceDiag struct {
Enabled bool `json:"enabled"`
LiveKitURL string `json:"livekit_url,omitempty"`
LiveKitHealth bool `json:"livekit_health"`
NodeIP string `json:"node_ip,omitempty"`
ProxyPath string `json:"proxy_path"`
}
type clientDiag struct {
RemoteAddr string `json:"remote_addr"`
IsPrivateNet bool `json:"is_private_network"`
}
func handleDiagnosticsConnectivity(
cfg *config.Config,
ver string,
hub *ws.Hub,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
clientAddr := clientIP(r)
lkHealthy := false
if ok, _ := hub.LiveKitHealthCheck(r.Context()); ok {
lkHealthy = true
}
// Strip credentials from LiveKit URL before exposing in diagnostics.
sanitizedLKURL := ""
if cfg.Voice.LiveKitURL != "" {
if parsed, parseErr := url.Parse(cfg.Voice.LiveKitURL); parseErr == nil {
sanitizedLKURL = parsed.Host
}
}
resp := diagnosticsResponse{
Server: serverDiag{
Version: ver,
Uptime: int64(time.Since(serverStartTime).Seconds()),
GoVersion: runtime.Version(),
OnlineUsers: hub.ClientCount(),
},
Voice: voiceDiag{
Enabled: cfg.Voice.LiveKitURL != "",
LiveKitURL: sanitizedLKURL,
LiveKitHealth: lkHealthy,
NodeIP: cfg.Voice.NodeIP,
ProxyPath: "/livekit",
},
Client: clientDiag{
RemoteAddr: clientAddr,
IsPrivateNet: isPrivateIP(clientAddr),
},
}
writeJSON(w, http.StatusOK, resp)
}
}
// isPrivateIP checks if an IP string is in a private/reserved range.
func isPrivateIP(ip string) bool {
for _, prefix := range []string{
"10.", "172.16.", "172.17.", "172.18.", "172.19.",
"172.20.", "172.21.", "172.22.", "172.23.", "172.24.",
"172.25.", "172.26.", "172.27.", "172.28.", "172.29.",
"172.30.", "172.31.", "192.168.", "127.", "::1", "fc", "fd",
} {
if len(ip) >= len(prefix) && ip[:len(prefix)] == prefix {
return true
}
}
return false
}