mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-29 03:57:09 +00:00
fix(extension,server): resolve 12 audit findings (lobby cleanup, dedup race, protocol version, rate limiting, observer scope, and more)
This commit is contained in:
@@ -361,6 +361,9 @@ async function connect() {
|
|||||||
forceSyncDeadline: null
|
forceSyncDeadline: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cancel any active episode lobby
|
||||||
|
clearEpisodeLobbyState();
|
||||||
|
|
||||||
if (currentRoom) {
|
if (currentRoom) {
|
||||||
currentRoom.peers = [];
|
currentRoom.peers = [];
|
||||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
|
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
|
||||||
@@ -412,6 +415,7 @@ function showNotification(senderName, action) {
|
|||||||
const label = action === 'play' ? 'started playback' :
|
const label = action === 'play' ? 'started playback' :
|
||||||
action === 'pause' ? 'paused playback' :
|
action === 'pause' ? 'paused playback' :
|
||||||
action === 'seek' ? 'seeked the video' :
|
action === 'seek' ? 'seeked the video' :
|
||||||
|
action === 'force_sync_prepare' ? 'started force sync' :
|
||||||
action === 'force_sync_execute' ? 'synchronized everyone' : action;
|
action === 'force_sync_execute' ? 'synchronized everyone' : action;
|
||||||
|
|
||||||
// Find username in current room if available
|
// Find username in current room if available
|
||||||
@@ -954,6 +958,9 @@ function leaveOldRoomIfSwitching(newRoomId) {
|
|||||||
forceSyncAcks: [],
|
forceSyncAcks: [],
|
||||||
forceSyncDeadline: null
|
forceSyncDeadline: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cancel any active episode lobby
|
||||||
|
clearEpisodeLobbyState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1051,9 +1058,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
|||||||
} else if (message.type === 'GET_HISTORY') {
|
} else if (message.type === 'GET_HISTORY') {
|
||||||
sendResponse(history);
|
sendResponse(history);
|
||||||
} else if (message.type === 'GET_ROOM_LIST') {
|
} else if (message.type === 'GET_ROOM_LIST') {
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
emit(EVENTS.GET_ROOMS, {});
|
||||||
socket.send(`42${JSON.stringify([EVENTS.GET_ROOMS])}`);
|
|
||||||
}
|
|
||||||
sendResponse({ status: 'ok' });
|
sendResponse({ status: 'ok' });
|
||||||
} else if (message.type === 'WEB_JOIN_REQUEST') {
|
} else if (message.type === 'WEB_JOIN_REQUEST') {
|
||||||
const { roomId, password, useCustomServer, serverUrl } = message;
|
const { roomId, password, useCustomServer, serverUrl } = message;
|
||||||
|
|||||||
@@ -519,7 +519,7 @@
|
|||||||
observerTimeout = setTimeout(checkVideo, 1000 - (now - lastMutate));
|
observerTimeout = setTimeout(checkVideo, 1000 - (now - lastMutate));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
observer.observe(document.body, { childList: true, subtree: true });
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
|
||||||
// --- SHARED_HEARTBEAT_INJECT_START ---
|
// --- SHARED_HEARTBEAT_INJECT_START ---
|
||||||
const HEARTBEAT_INTERVAL_VAL = 15000;
|
const HEARTBEAT_INTERVAL_VAL = 15000;
|
||||||
|
|||||||
+17
-2
@@ -231,6 +231,13 @@ function getVolumeIcon(volume, muted) {
|
|||||||
let activePeers = [];
|
let activePeers = [];
|
||||||
let interpolationInterval = null;
|
let interpolationInterval = null;
|
||||||
|
|
||||||
|
function stopInterpolation() {
|
||||||
|
if (interpolationInterval) {
|
||||||
|
clearInterval(interpolationInterval);
|
||||||
|
interpolationInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function startInterpolation() {
|
function startInterpolation() {
|
||||||
if (interpolationInterval) return;
|
if (interpolationInterval) return;
|
||||||
interpolationInterval = setInterval(() => {
|
interpolationInterval = setInterval(() => {
|
||||||
@@ -249,7 +256,11 @@ function startInterpolation() {
|
|||||||
function updatePeerList(peers) {
|
function updatePeerList(peers) {
|
||||||
if (!peers) return;
|
if (!peers) return;
|
||||||
activePeers = peers;
|
activePeers = peers;
|
||||||
if (!interpolationInterval) startInterpolation();
|
if (peers.length === 0) {
|
||||||
|
stopInterpolation();
|
||||||
|
} else if (!interpolationInterval) {
|
||||||
|
startInterpolation();
|
||||||
|
}
|
||||||
|
|
||||||
// UI Throttle: Only re-render if the peer state actually changed (excluding time interpolation)
|
// UI Throttle: Only re-render if the peer state actually changed (excluding time interpolation)
|
||||||
const stateToHash = peers.map(p => ({
|
const stateToHash = peers.map(p => ({
|
||||||
@@ -793,6 +804,10 @@ elements.forceSyncBtn.addEventListener('click', async () => {
|
|||||||
let targetTime = null;
|
let targetTime = null;
|
||||||
|
|
||||||
if (mode === 'jump-to-others') {
|
if (mode === 'jump-to-others') {
|
||||||
|
if (!localPeerId) {
|
||||||
|
showError('Identity not yet loaded. Wait a moment and try again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const peers = status.peers || [];
|
const peers = status.peers || [];
|
||||||
const otherTimes = peers
|
const otherTimes = peers
|
||||||
.filter(p => typeof p === 'object' && p.peerId !== localPeerId && p.currentTime != null && !isNaN(p.currentTime))
|
.filter(p => typeof p === 'object' && p.peerId !== localPeerId && p.currentTime != null && !isNaN(p.currentTime))
|
||||||
@@ -888,7 +903,7 @@ async function refreshLogs() {
|
|||||||
logs.forEach(log => {
|
logs.forEach(log => {
|
||||||
const entry = document.createElement('div');
|
const entry = document.createElement('div');
|
||||||
entry.className = `log-entry log-${log.type}`;
|
entry.className = `log-entry log-${log.type}`;
|
||||||
const timeStr = log.timestamp.split('T')[1].split('.')[0];
|
const timeStr = log.timestamp?.split('T')?.[1]?.split('.')[0] || '?';
|
||||||
entry.textContent = `[${timeStr}] ${log.message}`;
|
entry.textContent = `[${timeStr}] ${log.message}`;
|
||||||
elements.logList.appendChild(entry);
|
elements.logList.appendChild(entry);
|
||||||
});
|
});
|
||||||
|
|||||||
+21
-15
@@ -3,7 +3,7 @@ import { createServer } from 'http';
|
|||||||
import { Server } from 'socket.io';
|
import { Server } from 'socket.io';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import { EVENTS, OFFICIAL_SERVER_TOKEN } from '../shared/constants.js';
|
import { EVENTS, OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION } from '../shared/constants.js';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@@ -125,12 +125,8 @@ function checkEventRate(socketId) {
|
|||||||
* @param {string} socketId - The socket.id being removed.
|
* @param {string} socketId - The socket.id being removed.
|
||||||
* @param {string} roomId - The room it belongs to.
|
* @param {string} roomId - The room it belongs to.
|
||||||
* @param {string} reason - Log label ('disconnect', 'leave', 'reaper', 'dedupe', 'room-switch').
|
* @param {string} reason - Log label ('disconnect', 'leave', 'reaper', 'dedupe', 'room-switch').
|
||||||
* @param {boolean} [emitLeave=true] - Set false when the socket.io room leave
|
|
||||||
* is handled by the caller (e.g. reaper calls
|
|
||||||
* socket.leave() before us, or dedupe calls
|
|
||||||
* oldSocket.leave() before disconnecting).
|
|
||||||
*/
|
*/
|
||||||
function removePeerFromRoom(socketId, roomId, reason, emitLeave = true) {
|
function removePeerFromRoom(socketId, roomId, reason) {
|
||||||
const room = rooms.get(roomId);
|
const room = rooms.get(roomId);
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
@@ -225,7 +221,7 @@ io.on('connection', (socket) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Protocol check
|
// Protocol check
|
||||||
if (protocolVersion !== '1.0.0') {
|
if (protocolVersion !== PROTOCOL_VERSION) {
|
||||||
log('AUTH', `Protocol mismatch from ${peerId}: ${protocolVersion}`);
|
log('AUTH', `Protocol mismatch from ${peerId}: ${protocolVersion}`);
|
||||||
socket.emit(EVENTS.ERROR, { message: 'Incompatible protocol version' });
|
socket.emit(EVENTS.ERROR, { message: 'Incompatible protocol version' });
|
||||||
return;
|
return;
|
||||||
@@ -279,18 +275,23 @@ io.on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Peer Deduplication: Remove existing socket for the same peerId
|
// Peer Deduplication: Remove existing socket for the same peerId
|
||||||
|
// Snapshot stale SIDs first to avoid mutating the Map during iteration
|
||||||
|
const dedupeSids = [];
|
||||||
for (const [sid, data] of room.peerData.entries()) {
|
for (const [sid, data] of room.peerData.entries()) {
|
||||||
if (data.peerId === peerId && sid !== socket.id) {
|
if (data.peerId === peerId && sid !== socket.id) {
|
||||||
const oldSocket = io.sockets.sockets.get(sid);
|
dedupeSids.push(sid);
|
||||||
if (oldSocket) {
|
|
||||||
oldSocket.emit(EVENTS.ERROR, { message: 'Deduplication: Another session with this ID joined. Disconnecting...' });
|
|
||||||
oldSocket.leave(roomId);
|
|
||||||
oldSocket.disconnect(true);
|
|
||||||
log('DEDUPE', `Kicked old session for peer ${peerId}`);
|
|
||||||
}
|
|
||||||
removePeerFromRoom(sid, roomId, 'dedupe');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const sid of dedupeSids) {
|
||||||
|
const oldSocket = io.sockets.sockets.get(sid);
|
||||||
|
if (oldSocket) {
|
||||||
|
oldSocket.emit(EVENTS.ERROR, { message: 'Deduplication: Another session with this ID joined. Disconnecting...' });
|
||||||
|
oldSocket.leave(roomId);
|
||||||
|
oldSocket.disconnect(true);
|
||||||
|
log('DEDUPE', `Kicked old session for peer ${peerId}`);
|
||||||
|
}
|
||||||
|
removePeerFromRoom(sid, roomId, 'dedupe');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.join(roomId);
|
socket.join(roomId);
|
||||||
@@ -404,6 +405,11 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on(EVENTS.EVENT_ACK, (data) => {
|
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 (!data || typeof data !== 'object') return;
|
||||||
if (typeof data.targetId !== 'string') return;
|
if (typeof data.targetId !== 'string') return;
|
||||||
if (data.actionTimestamp !== undefined && (typeof data.actionTimestamp !== 'number' || !Number.isFinite(data.actionTimestamp))) return;
|
if (data.actionTimestamp !== undefined && (typeof data.actionTimestamp !== 'number' || !Number.isFinite(data.actionTimestamp))) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user