mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
fix: reject duplicate WebSocket logins to prevent reconnect ping-pong
Server now checks IsUserConnected before accepting a new WebSocket and returns an auth_error with a clear message instead of silently replacing the old session. Client dispatcher surfaces the error via transient UI state so the ConnectPage can display it.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
import type { WsClient } from "./ws";
|
||||
import { authStore, setAuth, clearAuth } from "@stores/auth.store";
|
||||
import { setTransientError } from "@stores/ui.store";
|
||||
import {
|
||||
setChannels,
|
||||
setActiveChannel,
|
||||
@@ -67,6 +68,7 @@ export function wireDispatcher(ws: WsClient): DispatcherCleanup {
|
||||
unsubs.push(
|
||||
ws.on("auth_error", (payload) => {
|
||||
log.error("Auth failed", { message: payload.message });
|
||||
setTransientError(payload.message);
|
||||
clearAuth();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
qs,
|
||||
} from "@lib/dom";
|
||||
import type { MountableComponent } from "@lib/safe-render";
|
||||
import { openSettings, closeSettings } from "@stores/ui.store";
|
||||
import { openSettings, closeSettings, uiStore, setTransientError } from "@stores/ui.store";
|
||||
import { createSettingsOverlay } from "@components/SettingsOverlay";
|
||||
import type { HealthStatus, ServerProfile } from "@lib/profiles";
|
||||
import { loadCredential } from "@lib/credentials";
|
||||
@@ -788,6 +788,13 @@ export function createConnectPage(
|
||||
});
|
||||
settingsOverlay.mount(rootEl);
|
||||
|
||||
// Show any pending auth error (e.g. "already connected from another client")
|
||||
const pendingError = uiStore.getState().transientError;
|
||||
if (pendingError) {
|
||||
transitionTo("error", pendingError);
|
||||
setTransientError(null);
|
||||
}
|
||||
|
||||
// Focus the first input
|
||||
hostInput.focus();
|
||||
}
|
||||
|
||||
+9
-22
@@ -161,28 +161,6 @@ func (h *Hub) Run() {
|
||||
|
||||
case c := <-h.register:
|
||||
h.mu.Lock()
|
||||
// If an existing client has the same userID, close its send channel
|
||||
// so writePump exits cleanly before the new client takes over.
|
||||
// Also clean up any voice state the old client held.
|
||||
if old, ok := h.clients[c.userID]; ok && old != c {
|
||||
slog.Info("hub: replacing existing client", "user_id", c.userID)
|
||||
oldChID, oldPC := old.clearVoice()
|
||||
if oldPC != nil {
|
||||
_ = oldPC.Close()
|
||||
}
|
||||
if oldChID > 0 {
|
||||
if room := h.GetVoiceRoom(oldChID); room != nil {
|
||||
room.RemoveParticipant(old.userID)
|
||||
if room.IsEmpty() {
|
||||
h.voiceRoomsMu.Lock()
|
||||
delete(h.voiceRooms, oldChID)
|
||||
h.voiceRoomsMu.Unlock()
|
||||
}
|
||||
}
|
||||
_ = h.db.LeaveVoiceChannel(old.userID)
|
||||
}
|
||||
old.closeSend()
|
||||
}
|
||||
h.clients[c.userID] = c
|
||||
slog.Info("hub: client registered", "user_id", c.userID, "total_clients", len(h.clients))
|
||||
h.mu.Unlock()
|
||||
@@ -255,6 +233,15 @@ func (h *Hub) CleanupVoiceForChannel(channelID int64) {
|
||||
}
|
||||
}
|
||||
|
||||
// IsUserConnected returns true if a client with the given userID is already
|
||||
// registered in the hub. Safe to call from any goroutine.
|
||||
func (h *Hub) IsUserConnected(userID int64) bool {
|
||||
h.mu.RLock()
|
||||
_, ok := h.clients[userID]
|
||||
h.mu.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
// Register queues a client for registration with the hub.
|
||||
func (h *Hub) Register(c *Client) {
|
||||
h.register <- c
|
||||
|
||||
@@ -43,6 +43,15 @@ func ServeWS(hub *Hub, database *db.DB, allowedOrigins []string) http.HandlerFun
|
||||
return
|
||||
}
|
||||
|
||||
// Reject duplicate connections — prevent ping-pong reconnect loops.
|
||||
if hub.IsUserConnected(user.ID) {
|
||||
slog.Warn("ws duplicate login rejected", "user_id", user.ID, "remote", r.RemoteAddr)
|
||||
ctx := r.Context()
|
||||
_ = conn.Write(ctx, websocket.MessageText, buildAuthError("already connected from another client"))
|
||||
_ = conn.Close(websocket.StatusPolicyViolation, "already connected")
|
||||
return
|
||||
}
|
||||
|
||||
c := newClient(hub, conn, user, tokenHash)
|
||||
hub.Register(c)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user