diff --git a/extension/background.js b/extension/background.js index 7177b45..765f6f0 100644 --- a/extension/background.js +++ b/extension/background.js @@ -280,6 +280,10 @@ function emit(event, data) { socket.send(msg); } else { eventQueue.push({ event, data }); + if (eventQueue.length > 50) { + eventQueue.shift(); + addLog('Event queue cap reached, dropping oldest event', 'warn'); + } } } @@ -433,7 +437,16 @@ async function routeToContent(action, payload) { } // --- Keep-Alive Mechanism --- -chrome.alarms.clearAll(); +chrome.alarms.create('keepAlive', { periodInMinutes: 1 }); +chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === 'keepAlive') { + chrome.storage.session.get('keepAlive', () => {}); + if (!socket || socket.readyState !== WebSocket.OPEN) { + connect(); + } + } +}); + setInterval(() => { // Calling a chrome API keeps the SW alive in MV3 (Chrome 110+) chrome.storage.session.get('keepAlive', () => {}); diff --git a/extension/content.js b/extension/content.js index d0407ef..636ad18 100644 --- a/extension/content.js +++ b/extension/content.js @@ -8,6 +8,17 @@ window.koalaSyncInjected = true; let lastTargetState = null; + let targetStateTimeout = null; + + function setTargetState(state) { + lastTargetState = state; + if (targetStateTimeout) clearTimeout(targetStateTimeout); + if (state !== null) { + targetStateTimeout = setTimeout(() => { + lastTargetState = null; + }, 1500); + } + } // --- Helper: find the best video element on the page --- function findVideo() { @@ -30,7 +41,7 @@ if (ytButton) { const isCurrentlyPlaying = !video.paused; if ((action === 'play' && !isCurrentlyPlaying) || (action === 'pause' && isCurrentlyPlaying)) { - lastTargetState = action === 'play' ? 'playing' : 'paused'; + setTargetState(action === 'play' ? 'playing' : 'paused'); ytButton.click(); } if (action === 'seek') video.currentTime = data.targetTime; @@ -43,7 +54,7 @@ if (twitchButton) { const isCurrentlyPlaying = !video.paused; if ((action === 'play' && !isCurrentlyPlaying) || (action === 'pause' && isCurrentlyPlaying)) { - lastTargetState = action === 'play' ? 'playing' : 'paused'; + setTargetState(action === 'play' ? 'playing' : 'paused'); twitchButton.click(); } if (action === 'seek') video.currentTime = data.targetTime; @@ -53,10 +64,13 @@ // Fallback for native HTML5 if (action === 'play') { - lastTargetState = 'playing'; - video.play().catch(() => {}); + setTargetState('playing'); + video.play().catch((e) => { + console.warn('KoalaSync playback prevented:', e); + setTargetState(null); + }); } else if (action === 'pause') { - lastTargetState = 'paused'; + setTargetState('paused'); video.pause(); } else if (action === 'seek') { video.currentTime = data.targetTime; @@ -110,7 +124,7 @@ if (!payload || payload.targetTime === undefined) return; const video = findVideo(); if (video) { - lastTargetState = 'paused'; + setTargetState('paused'); video.pause(); video.currentTime = payload.targetTime; pollSeekReady(payload.targetTime).then(() => { @@ -131,11 +145,11 @@ const eventState = action === 'play' ? 'playing' : (action === 'pause' ? 'paused' : null); if (eventState && lastTargetState === eventState) { - lastTargetState = null; // Consume the match + setTargetState(null); // Consume the match return; // Ignore event caused by our programmatic action } if (action !== 'seek') { - lastTargetState = null; // Reset on mismatch + setTargetState(null); // Reset on mismatch } chrome.runtime.sendMessage({ diff --git a/extension/popup.js b/extension/popup.js index cdebb69..b5ba4a7 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -268,7 +268,13 @@ function checkInviteLink() { let serverUrl = ''; // Smart Link: Parse Server Config if present - if (parts.length >= 3 && (parts[parts.length - 2] === '0' || parts[parts.length - 2] === '1')) { + const last = parts[parts.length - 1]; + const secondToLast = parts[parts.length - 2]; + const decodedLast = decodeURIComponent(last || ''); + const isCustom = secondToLast === '1' && (decodedLast.startsWith('ws://') || decodedLast.startsWith('wss://')); + const isOfficial = secondToLast === '0' && last === ''; + + if (parts.length >= 3 && (isCustom || isOfficial)) { serverUrl = decodeURIComponent(parts.pop()); useCustomServer = parts.pop() === '1'; } diff --git a/server/index.js b/server/index.js index fbeb5f6..aa9bcc6 100644 --- a/server/index.js +++ b/server/index.js @@ -148,6 +148,11 @@ io.on('connection', (socket) => { log('CONN', `New connection: ${socket.id} from ${clientIp}`); socket.on(EVENTS.JOIN_ROOM, async (payload) => { + if (!checkEventRate(socket.id)) { + log('SECURITY', `Event rate limit exceeded for socket (JOIN): ${socket.id}`); + socket.disconnect(true); + return; + } if (!payload || typeof payload.roomId !== 'string') return; const { roomId, password, peerId, protocolVersion } = payload; try {