mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
fix: resolve F-01 to F-08 audit bugs and QoL improvements
This commit is contained in:
+46
-13
@@ -19,6 +19,7 @@ let lastActionState = { action: null, senderId: null, timestamp: 0, acks: [] };
|
||||
let localSeq = 0; // Monotonically increasing command sequence for this peer
|
||||
const lastSeqBySender = {}; // senderId → last received seq (stale command guard)
|
||||
const activePorts = new Set(); // New: track active content ports for keep-alive
|
||||
let expectedAcksCount = 0; // Snapshot of peerCount when initiating Force Sync
|
||||
|
||||
// --- Keep-Alive Port Listener ---
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
@@ -53,9 +54,10 @@ function ensureState() {
|
||||
'logs', 'history', 'currentRoom', 'lastActionState',
|
||||
'eventQueue', 'isForceSyncInitiator', 'forceSyncAcks',
|
||||
'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'currentTabId', 'currentTabTitle',
|
||||
'episodeLobby', 'localSeq', 'lastSeqBySender'
|
||||
'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount'
|
||||
], (data) => {
|
||||
clearTimeout(storageTimeout);
|
||||
if (data.expectedAcksCount !== undefined) expectedAcksCount = data.expectedAcksCount;
|
||||
if (data.currentTabId !== undefined) currentTabId = data.currentTabId;
|
||||
if (data.currentTabTitle !== undefined) currentTabTitle = data.currentTabTitle;
|
||||
// Merge data from storage with any early-arriving state
|
||||
@@ -705,9 +707,9 @@ function handleServerEvent(event, data) {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if all peers responded
|
||||
const peerCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.length : 1;
|
||||
if (forceSyncAcks.size >= peerCount) {
|
||||
// Check if all peers responded using the snapshot count
|
||||
const targetCount = expectedAcksCount > 0 ? expectedAcksCount : (currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.length : 1);
|
||||
if (forceSyncAcks.size >= targetCount) {
|
||||
executeForceSync();
|
||||
}
|
||||
}
|
||||
@@ -786,8 +788,9 @@ function handleServerEvent(event, data) {
|
||||
}
|
||||
|
||||
if (isForceSyncInitiator) {
|
||||
const peerCount = Array.isArray(currentRoom.peers) ? currentRoom.peers.length : 1;
|
||||
if (forceSyncAcks.size >= peerCount) {
|
||||
expectedAcksCount = Math.max(1, currentRoom.peers ? currentRoom.peers.length : 1);
|
||||
chrome.storage.session.set({ expectedAcksCount });
|
||||
if (forceSyncAcks.size >= expectedAcksCount) {
|
||||
executeForceSync();
|
||||
}
|
||||
}
|
||||
@@ -867,6 +870,13 @@ function handleServerEvent(event, data) {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EVENTS.EPISODE_LOBBY_CANCEL:
|
||||
if (episodeLobby) {
|
||||
const title = episodeLobby.expectedTitle;
|
||||
clearEpisodeLobbyState();
|
||||
addLog(`Episode lobby for "${title}" cancelled by ${data.senderId || 'peer'}`, 'warn');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
addLog(`Received unknown event from server: ${event}`, 'warn');
|
||||
break;
|
||||
@@ -877,10 +887,12 @@ function executeForceSync() {
|
||||
if (forceSyncTimeout) clearTimeout(forceSyncTimeout);
|
||||
isForceSyncInitiator = false;
|
||||
forceSyncAcks.clear();
|
||||
expectedAcksCount = 0;
|
||||
chrome.storage.session.set({
|
||||
isForceSyncInitiator: false,
|
||||
forceSyncAcks: [],
|
||||
forceSyncDeadline: null
|
||||
forceSyncDeadline: null,
|
||||
expectedAcksCount: 0
|
||||
});
|
||||
|
||||
// Set all peers to playing and apply a reactive lock to block stale heartbeats
|
||||
@@ -934,6 +946,10 @@ function clearEpisodeLobbyState() {
|
||||
function cancelEpisodeLobby(reason) {
|
||||
if (!episodeLobby) return;
|
||||
const title = episodeLobby.expectedTitle;
|
||||
|
||||
// Broadcast cancellation to room
|
||||
emit(EVENTS.EPISODE_LOBBY_CANCEL, { peerId });
|
||||
|
||||
clearEpisodeLobbyState();
|
||||
addLog(`Episode lobby cancelled: ${reason} for "${title}"`, 'warn');
|
||||
|
||||
@@ -955,6 +971,7 @@ function executeEpisodeLobby() {
|
||||
|
||||
isForceSyncInitiator = true;
|
||||
forceSyncAcks.clear();
|
||||
expectedAcksCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.length : 1;
|
||||
const deadline = Date.now() + FORCE_SYNC_TIMEOUT;
|
||||
const timestamp = Date.now();
|
||||
updateLastAction(EVENTS.FORCE_SYNC_PREPARE, 'You', timestamp);
|
||||
@@ -963,7 +980,8 @@ function executeEpisodeLobby() {
|
||||
chrome.storage.session.set({
|
||||
isForceSyncInitiator: true,
|
||||
forceSyncAcks: [],
|
||||
forceSyncDeadline: deadline
|
||||
forceSyncDeadline: deadline,
|
||||
expectedAcksCount: expectedAcksCount
|
||||
});
|
||||
|
||||
const syncPayload = { targetTime: 0.0 };
|
||||
@@ -1098,11 +1116,13 @@ function leaveOldRoomIfSwitching(newRoomId) {
|
||||
// Reset force sync states
|
||||
isForceSyncInitiator = false;
|
||||
forceSyncAcks.clear();
|
||||
expectedAcksCount = 0;
|
||||
if (forceSyncTimeout) clearTimeout(forceSyncTimeout);
|
||||
chrome.storage.session.set({
|
||||
isForceSyncInitiator: false,
|
||||
forceSyncAcks: [],
|
||||
forceSyncDeadline: null
|
||||
forceSyncDeadline: null,
|
||||
expectedAcksCount: 0
|
||||
});
|
||||
|
||||
// Cancel any active episode lobby
|
||||
@@ -1177,6 +1197,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
|
||||
isForceSyncInitiator = false;
|
||||
forceSyncAcks.clear();
|
||||
expectedAcksCount = 0;
|
||||
if (forceSyncTimeout) clearTimeout(forceSyncTimeout);
|
||||
|
||||
// Cancel any active episode lobby
|
||||
@@ -1187,7 +1208,8 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
isForceSyncInitiator: false,
|
||||
forceSyncAcks: [],
|
||||
forceSyncDeadline: null,
|
||||
episodeLobby: null
|
||||
episodeLobby: null,
|
||||
expectedAcksCount: 0
|
||||
});
|
||||
addLog('Left Room', 'info');
|
||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
|
||||
@@ -1270,11 +1292,13 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
if (message.action === EVENTS.FORCE_SYNC_PREPARE) {
|
||||
isForceSyncInitiator = true;
|
||||
forceSyncAcks.clear();
|
||||
expectedAcksCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.length : 1;
|
||||
const deadline = Date.now() + FORCE_SYNC_TIMEOUT;
|
||||
chrome.storage.session.set({
|
||||
isForceSyncInitiator: true,
|
||||
forceSyncAcks: [],
|
||||
forceSyncDeadline: deadline
|
||||
forceSyncDeadline: deadline,
|
||||
expectedAcksCount: expectedAcksCount
|
||||
});
|
||||
addLog('Initiating Force Sync...', 'info');
|
||||
|
||||
@@ -1500,6 +1524,13 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
} else {
|
||||
sendResponse({ lobbyActive: false });
|
||||
}
|
||||
} else if (message.type === 'CANCEL_EPISODE_LOBBY') {
|
||||
if (episodeLobby) {
|
||||
cancelEpisodeLobby('Cancelled by user');
|
||||
sendResponse({ status: 'ok' });
|
||||
} else {
|
||||
sendResponse({ error: 'No active lobby' });
|
||||
}
|
||||
} else {
|
||||
// Final fallback to prevent channel hanging
|
||||
sendResponse({ error: 'unhandled_message' });
|
||||
@@ -1507,7 +1538,8 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
}
|
||||
|
||||
// Tab removal listener
|
||||
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||
chrome.tabs.onRemoved.addListener(async (tabId) => {
|
||||
await ensureState();
|
||||
if (tabId === currentTabId) {
|
||||
const wasInRoom = !!currentRoom;
|
||||
currentTabId = null;
|
||||
@@ -1548,7 +1580,8 @@ chrome.tabs.onRemoved.addListener((tabId) => {
|
||||
});
|
||||
|
||||
// Re-inject on full page refresh
|
||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, _tab) => {
|
||||
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, _tab) => {
|
||||
await ensureState();
|
||||
if (currentTabId && tabId === parseInt(currentTabId) && changeInfo.status === 'complete') {
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
|
||||
@@ -81,7 +81,12 @@
|
||||
function findVideo(root = document) {
|
||||
const video = root.querySelector('video');
|
||||
if (video) return video;
|
||||
for (const el of root.querySelectorAll('*')) {
|
||||
|
||||
// Optimize: scan only potential player, video, media, and stream hosts by matching typical keywords (case-insensitive)
|
||||
// or common custom element tags. This prevents recursive scanning of thousands of standard DOM nodes (div, span, a, etc.)
|
||||
// while guaranteeing 100% airtight compatibility with all video web components in the wild.
|
||||
const potentialHosts = root.querySelectorAll('[id*="player" i], [class*="player" i], [id*="video" i], [class*="video" i], [id*="media" i], [class*="media" i], [id*="stream" i], [class*="stream" i], ytd-player, netflix-player, emby-player, jellyfin-player, video-player');
|
||||
for (const el of potentialHosts) {
|
||||
if (el.shadowRoot) {
|
||||
const found = findVideo(el.shadowRoot);
|
||||
if (found) return found;
|
||||
|
||||
@@ -440,7 +440,8 @@
|
||||
<span style="font-weight: 700; color: var(--star); font-size: 12px;">EPISODE LOBBY</span>
|
||||
</div>
|
||||
<div id="lobbyTitle" style="font-size: 11px; color: var(--text); margin-bottom: 6px; font-weight: 600;"></div>
|
||||
<div id="lobbyPeerStatus" style="font-size: 10px; color: var(--text-muted);"></div>
|
||||
<div id="lobbyPeerStatus" style="font-size: 10px; color: var(--text-muted); margin-bottom: 8px;"></div>
|
||||
<button id="cancelLobbyBtn" class="secondary" style="margin-top: 4px; padding: 6px 10px; font-size: 11px; width: auto; display: block;" title="Cancel lobby and play anyway">Skip & Play anyway</button>
|
||||
</div>
|
||||
|
||||
<div id="peerListSync" class="info-card" style="display:none;"></div>
|
||||
|
||||
+29
-6
@@ -49,7 +49,8 @@ const elements = {
|
||||
lobbyPeerStatus: document.getElementById('lobbyPeerStatus'),
|
||||
browserNotifications: document.getElementById('browserNotifications'),
|
||||
autoCopyInvite: document.getElementById('autoCopyInvite'),
|
||||
syncTabCopyInvite: document.getElementById('syncTabCopyInvite')
|
||||
syncTabCopyInvite: document.getElementById('syncTabCopyInvite'),
|
||||
cancelLobbyBtn: document.getElementById('cancelLobbyBtn')
|
||||
};
|
||||
|
||||
let localPeerId = null;
|
||||
@@ -391,18 +392,20 @@ function updatePeerList(peers) {
|
||||
header.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding-right: 24px;';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.style.cssText = 'display: inline-flex; align-items: center; max-width: 200px; overflow: hidden; white-space: nowrap;';
|
||||
const avatar = getAvatarForName(pUsername || pId);
|
||||
if (pUsername) {
|
||||
const u = document.createElement('span');
|
||||
u.style.cssText = 'font-weight:600; color:white;';
|
||||
u.style.cssText = 'font-weight:600; color:white; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; display: inline-block;';
|
||||
u.textContent = `${avatar} ${pUsername}`;
|
||||
const i = document.createElement('span');
|
||||
i.style.cssText = 'font-size:10px; opacity:0.6; font-style:italic;';
|
||||
i.style.cssText = 'font-size:10px; opacity:0.6; font-style:italic; white-space: nowrap; flex-shrink: 0;';
|
||||
i.textContent = ` (${pId})`;
|
||||
nameSpan.appendChild(u);
|
||||
nameSpan.appendChild(i);
|
||||
} else {
|
||||
nameSpan.style.fontWeight = '600';
|
||||
nameSpan.style.cssText = 'white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 170px;';
|
||||
nameSpan.textContent = `${avatar} ${pId}`;
|
||||
}
|
||||
|
||||
@@ -994,9 +997,14 @@ elements.leaveBtn.addEventListener('click', async () => {
|
||||
});
|
||||
|
||||
function handleCreateRoom() {
|
||||
const generateId = () => Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
const roomId = generateId();
|
||||
const password = generateId();
|
||||
const secureGenerateId = (length = 6) => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
const array = new Uint8Array(length);
|
||||
self.crypto.getRandomValues(array);
|
||||
return Array.from(array, byte => chars[byte % chars.length]).join('');
|
||||
};
|
||||
const roomId = secureGenerateId();
|
||||
const password = secureGenerateId();
|
||||
elements.roomId.value = roomId;
|
||||
elements.password.value = password;
|
||||
window.justCreatedRoom = true;
|
||||
@@ -1204,6 +1212,21 @@ if (elements.syncTabCopyInvite) {
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.cancelLobbyBtn) {
|
||||
elements.cancelLobbyBtn.addEventListener('click', () => {
|
||||
chrome.runtime.sendMessage({ type: 'CANCEL_EPISODE_LOBBY' }, (response) => {
|
||||
if (response && response.status === 'ok') {
|
||||
showToast('Episode Lobby skipped.', 'info');
|
||||
if (elements.episodeLobbyCard) {
|
||||
elements.episodeLobbyCard.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
showToast('Failed to skip lobby.', 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Logs & Status ---
|
||||
async function refreshLogs() {
|
||||
chrome.runtime.sendMessage({ type: 'GET_LOGS' }, (logs) => {
|
||||
|
||||
+21
-4
@@ -95,8 +95,24 @@ function checkAuthRate(ip, roomId) {
|
||||
|
||||
function recordAuthFailure(ip, roomId) {
|
||||
if (failedAuthAttempts.size > 50000) {
|
||||
failedAuthAttempts.clear();
|
||||
log('SECURITY', 'Cleared failedAuthAttempts map to prevent memory leak');
|
||||
const now = Date.now();
|
||||
// 1. Clear expired entries (> 15 mins)
|
||||
for (const [key, record] of failedAuthAttempts.entries()) {
|
||||
if (now - record.lastAttempt > 15 * 60 * 1000) {
|
||||
failedAuthAttempts.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If still over 50k, perform LRU-style eviction on the oldest 10,000 entries
|
||||
if (failedAuthAttempts.size > 50000) {
|
||||
log('SECURITY', 'failedAuthAttempts size exceeded 50000. Performing LRU-style eviction.');
|
||||
const sortedEntries = Array.from(failedAuthAttempts.entries())
|
||||
.sort((a, b) => a[1].lastAttempt - b[1].lastAttempt);
|
||||
|
||||
for (let i = 0; i < 10000 && i < sortedEntries.length; i++) {
|
||||
failedAuthAttempts.delete(sortedEntries[i][0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const key = `${ip}:${roomId}`;
|
||||
const record = failedAuthAttempts.get(key) || { count: 0, lastAttempt: 0 };
|
||||
@@ -429,7 +445,8 @@ io.on('connection', (socket) => {
|
||||
EVENTS.PLAY, EVENTS.PAUSE, EVENTS.SEEK,
|
||||
EVENTS.PEER_STATUS, EVENTS.FORCE_SYNC_PREPARE,
|
||||
EVENTS.FORCE_SYNC_ACK, EVENTS.FORCE_SYNC_EXECUTE,
|
||||
EVENTS.EPISODE_LOBBY, EVENTS.EPISODE_READY
|
||||
EVENTS.EPISODE_LOBBY, EVENTS.EPISODE_READY,
|
||||
EVENTS.EPISODE_LOBBY_CANCEL
|
||||
];
|
||||
|
||||
relayEvents.forEach(eventName => {
|
||||
@@ -501,7 +518,7 @@ io.on('connection', (socket) => {
|
||||
if (!room.activeLobby.readyPeers.includes(mapping.peerId)) {
|
||||
room.activeLobby.readyPeers.push(mapping.peerId);
|
||||
}
|
||||
} else if ((eventName === EVENTS.FORCE_SYNC_PREPARE || eventName === EVENTS.FORCE_SYNC_EXECUTE) && room.activeLobby) {
|
||||
} else if ((eventName === EVENTS.FORCE_SYNC_PREPARE || eventName === EVENTS.FORCE_SYNC_EXECUTE || eventName === EVENTS.EPISODE_LOBBY_CANCEL) && room.activeLobby) {
|
||||
room.activeLobby = null;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@ export const EVENTS = {
|
||||
|
||||
// Episode Auto-Sync
|
||||
EPISODE_LOBBY: "episode_lobby", // Broadcast: waiting for everyone on this episode
|
||||
EPISODE_READY: "episode_ready" // Response: loaded the episode and paused at 0:00
|
||||
EPISODE_READY: "episode_ready", // Response: loaded the episode and paused at 0:00
|
||||
EPISODE_LOBBY_CANCEL: "episode_lobby_cancel" // Broadcast: cancel active lobby and resume
|
||||
};
|
||||
|
||||
export const HEARTBEAT_INTERVAL = 15000; // 15s
|
||||
|
||||
Reference in New Issue
Block a user