diff --git a/extension/background.js b/extension/background.js index 25e555a..e25a77c 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1,4 +1,4 @@ -import { EVENTS, PROTOCOL_VERSION, OFFICIAL_SERVER_URL, OFFICIAL_SERVER_TOKEN, APP_VERSION, EPISODE_LOBBY_TIMEOUT } from './shared/constants.js'; +import { EVENTS, PROTOCOL_VERSION, OFFICIAL_SERVER_URL, OFFICIAL_SERVER_TOKEN, APP_VERSION, EPISODE_LOBBY_TIMEOUT, FORCE_SYNC_TIMEOUT } from './shared/constants.js'; // --- State Management --- let socket = null; @@ -849,7 +849,7 @@ function executeEpisodeLobby() { isForceSyncInitiator = true; forceSyncAcks.clear(); - const deadline = Date.now() + 8500; + const deadline = Date.now() + FORCE_SYNC_TIMEOUT; const timestamp = Date.now(); updateLastAction(EVENTS.FORCE_SYNC_PREPARE, 'You', timestamp); lastActionState.targetTime = 0.0; @@ -869,7 +869,7 @@ function executeEpisodeLobby() { addLog('Force Sync (Episode): Timeout waiting for ACKs, executing anyway...', 'warn'); executeForceSync(); } - }, 8500); + }, FORCE_SYNC_TIMEOUT); } function checkEpisodeLobbyCompletion() { @@ -1149,7 +1149,7 @@ async function handleAsyncMessage(message, sender, sendResponse) { if (message.action === EVENTS.FORCE_SYNC_PREPARE) { isForceSyncInitiator = true; forceSyncAcks.clear(); - const deadline = Date.now() + 8500; + const deadline = Date.now() + FORCE_SYNC_TIMEOUT; chrome.storage.session.set({ isForceSyncInitiator: true, forceSyncAcks: [], @@ -1164,7 +1164,7 @@ async function handleAsyncMessage(message, sender, sendResponse) { addLog('Force Sync: Timeout waiting for ACKs, executing anyway...', 'warn'); executeForceSync(); } - }, 8500); + }, FORCE_SYNC_TIMEOUT); } addToHistory(message.action, 'You'); emit(message.action, { ...message.payload, peerId }); diff --git a/extension/popup.js b/extension/popup.js index fdbad03..b6e1561 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -57,6 +57,7 @@ let joinBtnTimeout = null; let forceSyncResetTimer = null; let popupIntervals = []; let populateTabsToken = null; +let errorToken = 0; let forceSyncDone = false; // --- Initialization --- @@ -838,6 +839,7 @@ function showToast(message, type = 'info', duration = 3000) { function showError(msg) { if (!elements.roomError) return; + const currentToken = ++errorToken; elements.roomError.textContent = msg; elements.roomError.style.display = 'block'; elements.roomId.style.borderColor = 'var(--error)'; @@ -846,6 +848,7 @@ function showError(msg) { showToast(msg, 'error', 5000); setTimeout(() => { + if (currentToken !== errorToken) return; if (elements.roomError) elements.roomError.style.display = 'none'; elements.roomId.style.borderColor = ''; elements.password.style.borderColor = ''; diff --git a/server/index.js b/server/index.js index f7e8ab5..a31e1f7 100644 --- a/server/index.js +++ b/server/index.js @@ -480,30 +480,30 @@ io.on('connection', (socket) => { } }); - socket.on(EVENTS.EVENT_ACK, (data) => { - if (!checkEventRate(socket.id)) { - log('SECURITY', `Event rate limit exceeded for socket (ACK): ${socket.id}`); - socket.disconnect(true); - return; - } - if (!data || typeof data !== 'object') return; - if (typeof data.targetId !== 'string') return; - if (data.actionTimestamp !== undefined && (typeof data.actionTimestamp !== 'number' || !Number.isFinite(data.actionTimestamp))) return; - - const senderMapping = socketToRoom.get(socket.id); - const targetSocketId = peerToSocket.get(data.targetId); - const targetMapping = targetSocketId ? socketToRoom.get(targetSocketId) : null; + socket.on(EVENTS.EVENT_ACK, (data) => { + if (!checkEventRate(socket.id)) { + log('SECURITY', `Event rate limit exceeded for socket (ACK): ${socket.id}`); + socket.disconnect(true); + return; + } + if (!data || typeof data !== 'object') return; + if (typeof data.targetId !== 'string') return; + if (data.actionTimestamp !== undefined && (typeof data.actionTimestamp !== 'number' || !Number.isFinite(data.actionTimestamp))) return; + + const senderMapping = socketToRoom.get(socket.id); + const targetSocketId = peerToSocket.get(data.targetId); + const targetMapping = targetSocketId ? socketToRoom.get(targetSocketId) : null; - // Security: Only relay ACK if both peers are in the same room - if (senderMapping && targetMapping && senderMapping.roomId === targetMapping.roomId) { - io.to(targetSocketId).emit(EVENTS.EVENT_ACK, { - senderId: senderMapping.peerId, - actionTimestamp: data.actionTimestamp - }); - } else { - log('SECURITY', `Blocked cross-room ACK attempt from ${socket.id} to ${data.targetId}`); - } - }); + // Security: Only relay ACK if both peers are in the same room + if (senderMapping && targetMapping && senderMapping.roomId === targetMapping.roomId) { + io.to(targetSocketId).emit(EVENTS.EVENT_ACK, { + senderId: senderMapping.peerId, + actionTimestamp: data.actionTimestamp + }); + } else { + log('SECURITY', `Blocked cross-room ACK attempt from ${socket.id} to ${data.targetId}`); + } + }); socket.on('disconnect', () => { eventCounts.delete(socket.id); diff --git a/shared/constants.js b/shared/constants.js index 3714cca..78cc09e 100644 --- a/shared/constants.js +++ b/shared/constants.js @@ -40,5 +40,5 @@ export const EVENTS = { }; export const HEARTBEAT_INTERVAL = 15000; // 15s -export const FORCE_SYNC_TIMEOUT = 5000; // 5s timeout for ACKs +export const FORCE_SYNC_TIMEOUT = 8500; // 8.5s timeout for force sync ACKs (must be > content.js poll timeout of 8s) export const EPISODE_LOBBY_TIMEOUT = 60000; // 60s timeout for episode lobby