mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-09-03 06:27:56 +00:00
feat: add graceful shutdown, input validation, and peer data factory (Phase 3)
This commit is contained in:
+23
-25
@@ -101,6 +101,26 @@ let forceSyncTimeout = null;
|
|||||||
|
|
||||||
// --- Storage Utils ---
|
// --- 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() {
|
async function getPeerId() {
|
||||||
const data = await chrome.storage.local.get(['peerId']);
|
const data = await chrome.storage.local.get(['peerId']);
|
||||||
if (data.peerId) return data.peerId;
|
if (data.peerId) return data.peerId;
|
||||||
@@ -414,8 +434,6 @@ function handleServerEvent(event, data) {
|
|||||||
addLog(`Joined Room: ${data.roomId}`, 'success');
|
addLog(`Joined Room: ${data.roomId}`, 'success');
|
||||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: data.peers }).catch(() => {});
|
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: data.peers }).catch(() => {});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Inform Website Bridge & Popup
|
// Inform Website Bridge & Popup
|
||||||
const joinStatusMsg = { type: 'JOIN_STATUS', success: true, message: 'Joined' };
|
const joinStatusMsg = { type: 'JOIN_STATUS', success: true, message: 'Joined' };
|
||||||
chrome.runtime.sendMessage(joinStatusMsg).catch(() => {});
|
chrome.runtime.sendMessage(joinStatusMsg).catch(() => {});
|
||||||
@@ -506,17 +524,7 @@ function handleServerEvent(event, data) {
|
|||||||
if (!Array.isArray(currentRoom.peers)) currentRoom.peers = [];
|
if (!Array.isArray(currentRoom.peers)) currentRoom.peers = [];
|
||||||
if (data.status === 'joined') {
|
if (data.status === 'joined') {
|
||||||
if (!currentRoom.peers.find(p => (p.peerId || p) === data.peerId)) {
|
if (!currentRoom.peers.find(p => (p.peerId || p) === data.peerId)) {
|
||||||
currentRoom.peers.push({
|
currentRoom.peers.push(createPeerData(data));
|
||||||
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()
|
|
||||||
});
|
|
||||||
if (storageInitialized) chrome.storage.session.set({ currentRoom });
|
if (storageInitialized) chrome.storage.session.set({ currentRoom });
|
||||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
|
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.muted = data.muted !== undefined ? data.muted : peer.muted;
|
||||||
peer.lastHeartbeat = Date.now();
|
peer.lastHeartbeat = Date.now();
|
||||||
} else {
|
} else {
|
||||||
// Migration: replace string with object
|
// Migration: replace string peer with normalized object
|
||||||
const idx = currentRoom.peers.indexOf(peer);
|
const idx = currentRoom.peers.indexOf(peer);
|
||||||
currentRoom.peers[idx] = {
|
currentRoom.peers[idx] = createPeerData(data);
|
||||||
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()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (storageInitialized) chrome.storage.session.set({ currentRoom });
|
if (storageInitialized) chrome.storage.session.set({ currentRoom });
|
||||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
|
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
|
||||||
|
|||||||
+30
-1
@@ -200,7 +200,16 @@ io.on('connection', (socket) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!payload || typeof payload.roomId !== 'string') 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 {
|
try {
|
||||||
// Protocol check
|
// Protocol check
|
||||||
if (protocolVersion !== '1.0.0') {
|
if (protocolVersion !== '1.0.0') {
|
||||||
@@ -422,3 +431,23 @@ setInterval(() => {
|
|||||||
httpServer.listen(PORT, () => {
|
httpServer.listen(PORT, () => {
|
||||||
log('SERVER', `KoalaSync Relay running on port ${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'));
|
||||||
|
|||||||
Reference in New Issue
Block a user