diff --git a/extension/background.js b/extension/background.js index dfbef9e..8bc2a05 100644 --- a/extension/background.js +++ b/extension/background.js @@ -101,6 +101,26 @@ let forceSyncTimeout = null; // --- Storage Utils --- +/** + * Canonical peer data factory. All peer object construction must go through + * here to guarantee a consistent shape with predictable null defaults. + * @param {object} raw - Raw data from server event or heartbeat payload. + * @returns {object} Normalized peer data object. + */ +function createPeerData(raw) { + return { + peerId: raw.peerId || null, + username: raw.username || null, + tabTitle: raw.tabTitle || null, + mediaTitle: raw.mediaTitle || null, + playbackState: raw.playbackState || null, + currentTime: raw.currentTime != null ? raw.currentTime : null, + volume: raw.volume != null ? raw.volume : null, + muted: raw.muted != null ? raw.muted : null, + lastHeartbeat: Date.now() + }; +} + async function getPeerId() { const data = await chrome.storage.local.get(['peerId']); if (data.peerId) return data.peerId; @@ -413,9 +433,7 @@ function handleServerEvent(event, data) { if (storageInitialized) chrome.storage.session.set({ currentRoom }); addLog(`Joined Room: ${data.roomId}`, 'success'); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: data.peers }).catch(() => {}); - - - + // Inform Website Bridge & Popup const joinStatusMsg = { type: 'JOIN_STATUS', success: true, message: 'Joined' }; chrome.runtime.sendMessage(joinStatusMsg).catch(() => {}); @@ -506,17 +524,7 @@ function handleServerEvent(event, data) { if (!Array.isArray(currentRoom.peers)) currentRoom.peers = []; if (data.status === 'joined') { if (!currentRoom.peers.find(p => (p.peerId || p) === data.peerId)) { - currentRoom.peers.push({ - peerId: data.peerId, - username: data.username, - tabTitle: data.tabTitle, - mediaTitle: data.mediaTitle || null, - playbackState: data.playbackState || null, - currentTime: data.currentTime != null ? data.currentTime : null, - volume: data.volume != null ? data.volume : null, - muted: data.muted != null ? data.muted : null, - lastHeartbeat: Date.now() - }); + currentRoom.peers.push(createPeerData(data)); if (storageInitialized) chrome.storage.session.set({ currentRoom }); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {}); } @@ -538,19 +546,9 @@ function handleServerEvent(event, data) { peer.muted = data.muted !== undefined ? data.muted : peer.muted; peer.lastHeartbeat = Date.now(); } else { - // Migration: replace string with object + // Migration: replace string peer with normalized object const idx = currentRoom.peers.indexOf(peer); - currentRoom.peers[idx] = { - peerId: data.peerId, - username: data.username, - tabTitle: data.tabTitle, - mediaTitle: data.mediaTitle || null, - playbackState: data.playbackState || null, - currentTime: data.currentTime != null ? data.currentTime : null, - volume: data.volume != null ? data.volume : null, - muted: data.muted != null ? data.muted : null, - lastHeartbeat: Date.now() - }; + currentRoom.peers[idx] = createPeerData(data); } if (storageInitialized) chrome.storage.session.set({ currentRoom }); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {}); diff --git a/server/index.js b/server/index.js index 79bf68c..ed087eb 100644 --- a/server/index.js +++ b/server/index.js @@ -200,7 +200,16 @@ io.on('connection', (socket) => { return; } if (!payload || typeof payload.roomId !== 'string') return; - const { roomId, password, peerId, username, tabTitle, mediaTitle, protocolVersion } = payload; + const { password, peerId, protocolVersion } = payload; + + // --- M-2: Sanitize and clamp all string fields --- + const roomId = String(payload.roomId || '').substring(0, 64); + const username = typeof payload.username === 'string' ? payload.username.substring(0, 30) : null; + const tabTitle = typeof payload.tabTitle === 'string' ? payload.tabTitle.substring(0, 100) : null; + const mediaTitle = typeof payload.mediaTitle === 'string' ? payload.mediaTitle.substring(0, 100) : null; + + if (!roomId) return; // Guard: empty after sanitization + try { // Protocol check if (protocolVersion !== '1.0.0') { @@ -422,3 +431,23 @@ setInterval(() => { httpServer.listen(PORT, () => { log('SERVER', `KoalaSync Relay running on port ${PORT}`); }); + +// --- M-4: Graceful Shutdown --- +function gracefulShutdown(signal) { + log('SERVER', `${signal} received — starting graceful shutdown...`); + // 1. Notify all connected clients so they can display a meaningful message + io.emit(EVENTS.ERROR, { message: 'Server is restarting. Please reconnect in a moment.' }); + // 2. Stop accepting new HTTP connections + httpServer.close(() => { + log('SERVER', 'HTTP server closed. Exiting.'); + process.exit(0); + }); + // 3. Safety net: force-exit after 5s if connections don't drain + setTimeout(() => { + log('SERVER', 'Force-exit after timeout.'); + process.exit(1); + }, 5000); +} + +process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); +process.on('SIGINT', () => gracefulShutdown('SIGINT'));