From 88cb313fc07f80f5fa7374c79e65a36eb2e0a12f Mon Sep 17 00:00:00 2001 From: Timo <6156589+Shik3i@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:20:45 +0200 Subject: [PATCH] Security & Stability: Fixed SW race, ping-pong loops, and invite parsing --- extension/background.js | 27 +++++++++++++++++---------- extension/content.js | 32 +++++++++++++++++++++----------- extension/popup.js | 16 +++++++++------- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/extension/background.js b/extension/background.js index 93012b4..7177b45 100644 --- a/extension/background.js +++ b/extension/background.js @@ -14,6 +14,7 @@ let history = []; // New: for Action History let storageInitialized = false; let pendingLogs = []; let pendingHistory = []; +let eventQueue = []; // Restore state from session storage chrome.storage.session.get(['logs', 'history'], (data) => { @@ -182,6 +183,11 @@ async function connect() { protocolVersion: PROTOCOL_VERSION }); } + while (eventQueue.length > 0) { + const queuedMsg = eventQueue.shift(); + emit(queuedMsg.event, queuedMsg.data); + } + eventQueue = []; // Explicitly reset to avoid memory leaks } else if (msg.startsWith('42')) { // Event: 42["event", data] try { @@ -272,6 +278,8 @@ function emit(event, data) { if (socket && socket.readyState === WebSocket.OPEN) { const msg = `42${JSON.stringify([event, data])}`; socket.send(msg); + } else { + eventQueue.push({ event, data }); } } @@ -425,15 +433,14 @@ async function routeToContent(action, payload) { } // --- Keep-Alive Mechanism --- -chrome.alarms.create('keepAlive', { periodInMinutes: 1 }); // every 1m -chrome.alarms.onAlarm.addListener((alarm) => { - if (alarm.name === 'keepAlive') { - // console.log('SW KeepAlive Heartbeat'); - if (!socket || socket.readyState !== WebSocket.OPEN) { - connect(); - } +chrome.alarms.clearAll(); +setInterval(() => { + // Calling a chrome API keeps the SW alive in MV3 (Chrome 110+) + chrome.storage.session.get('keepAlive', () => {}); + if (!socket || socket.readyState !== WebSocket.OPEN) { + connect(); } -}); +}, 20000); // every 20s // --- Extension Message Listeners --- chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { @@ -459,9 +466,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { logs = []; sendResponse({ status: 'ok' }); } else if (message.type === 'GET_LOGS') { - sendResponse(logs); + sendResponse(storageInitialized ? logs : pendingLogs); } else if (message.type === 'GET_HISTORY') { - sendResponse(history); + sendResponse(storageInitialized ? history : pendingHistory); } else if (message.type === 'GET_ROOM_LIST') { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(`42${JSON.stringify([EVENTS.GET_ROOMS])}`); diff --git a/extension/content.js b/extension/content.js index 46a6873..d0407ef 100644 --- a/extension/content.js +++ b/extension/content.js @@ -7,7 +7,7 @@ if (window.koalaSyncInjected) return; window.koalaSyncInjected = true; - let processingCommandUntil = 0; + let lastTargetState = null; // --- Helper: find the best video element on the page --- function findVideo() { @@ -20,7 +20,6 @@ const video = findVideo(); if (!video) return; - processingCommandUntil = Date.now() + 1000; try { const host = window.location.hostname.toLowerCase(); const isYouTube = host.includes('youtube.com'); @@ -31,11 +30,12 @@ if (ytButton) { const isCurrentlyPlaying = !video.paused; if ((action === 'play' && !isCurrentlyPlaying) || (action === 'pause' && isCurrentlyPlaying)) { + lastTargetState = action === 'play' ? 'playing' : 'paused'; ytButton.click(); } + if (action === 'seek') video.currentTime = data.targetTime; + return; } - if (action === 'seek') video.currentTime = data.targetTime; - return; } if (isTwitch) { @@ -43,17 +43,20 @@ if (twitchButton) { const isCurrentlyPlaying = !video.paused; if ((action === 'play' && !isCurrentlyPlaying) || (action === 'pause' && isCurrentlyPlaying)) { + lastTargetState = action === 'play' ? 'playing' : 'paused'; twitchButton.click(); } + if (action === 'seek') video.currentTime = data.targetTime; + return; } - if (action === 'seek') video.currentTime = data.targetTime; - return; } // Fallback for native HTML5 if (action === 'play') { + lastTargetState = 'playing'; video.play().catch(() => {}); } else if (action === 'pause') { + lastTargetState = 'paused'; video.pause(); } else if (action === 'seek') { video.currentTime = data.targetTime; @@ -104,17 +107,15 @@ } else if (action === 'seek') { tryMediaAction('seek', payload); } else if (action === 'force_sync_prepare') { - processingCommandUntil = Date.now() + 10000; + if (!payload || payload.targetTime === undefined) return; const video = findVideo(); if (video) { + lastTargetState = 'paused'; video.pause(); video.currentTime = payload.targetTime; pollSeekReady(payload.targetTime).then(() => { chrome.runtime.sendMessage({ type: 'FORCE_SYNC_ACK' }); - processingCommandUntil = Date.now() + 1000; }); - } else { - processingCommandUntil = 0; } } else if (action === 'force_sync_execute') { tryMediaAction('play'); @@ -124,10 +125,19 @@ // Detect native events function reportEvent(action) { - if (Date.now() < processingCommandUntil) return; const video = findVideo(); if (!video) return; + const eventState = action === 'play' ? 'playing' : (action === 'pause' ? 'paused' : null); + + if (eventState && lastTargetState === eventState) { + lastTargetState = null; // Consume the match + return; // Ignore event caused by our programmatic action + } + if (action !== 'seek') { + lastTargetState = null; // Reset on mismatch + } + chrome.runtime.sendMessage({ type: 'CONTENT_EVENT', action, diff --git a/extension/popup.js b/extension/popup.js index 33cb05e..cdebb69 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -260,23 +260,25 @@ function checkInviteLink() { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { const tab = tabs[0]; if (tab && tab.url && tab.url.includes(OFFICIAL_LANDING_PAGE_URL) && tab.url.includes('#join:')) { - const parts = tab.url.split('#join:')[1].split(':'); + const rawHash = tab.url.split('#join:')[1]; + const parts = rawHash.split(':'); if (parts.length >= 2) { - const roomId = parts[0]; - const password = parts[1]; + const roomId = parts.shift(); let useCustomServer = false; let serverUrl = ''; // Smart Link: Parse Server Config if present - if (parts.length >= 4) { - useCustomServer = parts[2] === '1'; - serverUrl = decodeURIComponent(parts[3]); + if (parts.length >= 3 && (parts[parts.length - 2] === '0' || parts[parts.length - 2] === '1')) { + serverUrl = decodeURIComponent(parts.pop()); + useCustomServer = parts.pop() === '1'; } + const password = parts.join(':'); + elements.roomId.value = roomId; elements.password.value = password; - if (parts.length >= 4) { + if (serverUrl || useCustomServer) { elements.serverUrl.value = serverUrl; setServerMode(useCustomServer); chrome.storage.sync.set({ serverUrl, useCustomServer });