From f7096edd30d9edff3d6f7edda4ec92881147de72 Mon Sep 17 00:00:00 2001 From: Timo <6156589+Shik3i@users.noreply.github.com> Date: Sat, 30 May 2026 02:00:17 +0200 Subject: [PATCH] Refactor: Behebung Force-Sync ACK-Zaehler-Inflation, SHA-256 Hashing, Room-ID-Desinfizierung, Reduzierung MIN_SEEK_DELTA auf 2s und seekDebounce auf 300ms --- AI_INIT.md | 1 + docs/ROADMAP.md | 29 +++++++++-------------------- extension/background.js | 5 ++++- extension/content.js | 6 +++--- extension/popup.js | 4 ++++ server/index.js | 12 +++++++++--- 6 files changed, 30 insertions(+), 27 deletions(-) diff --git a/AI_INIT.md b/AI_INIT.md index aa4fed7..4b335d9 100644 --- a/AI_INIT.md +++ b/AI_INIT.md @@ -85,6 +85,7 @@ The following features are critical and must not be removed or fundamentally alt - **Server Transport**: Restricted to `websocket` only. Polling is disabled. - **Docker Context**: The Docker build must run from the **Repo Root**. - **Manifest Settings**: `run_at` must remain `document_idle`, and `all_frames` must remain `false`. +- **Strict Backward & Forward Compatibility (Store Delay Rule)**: Browser extensions are distributed through stores (e.g., Chrome Web Store, Firefox Add-ons) which can take up to 2 weeks to approve updates. Therefore, the server MUST NOT reject older extension clients unless a critical protocol version bump is explicitly authorized, and new extension versions MUST remain fully operational when connected to older servers (e.g., by silently falling back if a new feature is not supported). This is a core architectural requirement. ## 9. Security & Deployment - **Tokens**: Security tokens are intentionally managed via `shared/constants.js` and server `.env`. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 27e0330..f5f9f05 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,26 +1,15 @@ # KoalaSync Roadmap -This document tracks planned features, improvements, and their implementation details. +Dieses Dokument erfasst zukünftige technische Pläne und Optimierungen für das KoalaSync-System. --- -## Offene technische Fragen +## Geplante Optimierungen & Technische Roadmap -### 1. Service Worker Fallback bei Room-State Verlust -Manifest V3 suspendiert den Service Worker nach ~30s Inaktivität. `chrome.alarms` weckt ihn auf, aber: -- **Problem:** Wenn der SW neu startet, sind alle Variablen (`currentRoom`, `socket`, `isNamespaceJoined`) weg -- **Aktueller Stand:** `chrome.storage.session` persistiert `currentRoom`, `peerId`, `eventQueue` — der SW stellt diese beim Start wieder her (`ensureState()`) -- **Gelöst:** WebSocket wird automatisch via `connect()` neu aufgebaut. Events werden während Reconnect gequeued und nach Namespace-Join geflushed. "Reconnecting..." Status wird im Popup + Badge angezeigt. KeepAlive-Alarm auf 30s reduziert. Reconnect-Backoff: 500ms Basis, max 5s (statt vorher 1s→30s). - -### 7. Tests für Extensions -Stimmt, sind aufwändig. Praktische Ansätze: -- **Unit Tests:** `jest` + `jest-chrome` (mockt `chrome.*` APIs) — testet `popup.js` Logik, Server-Logik -- **Integration Tests:** `puppeteer` mit `--load-extension` Flag — testet Extension im echten Browser -- **Server Tests:** `supertest` + `socket.io-client` — testet WebSocket-Flows -- **Aufwand:** ~400-600 LOC für sinnvolle Testabdeckung der Kernlogik - ---- - -## Zukünftige Features - -Neue Features werden nur nach expliziter Freigabe hinzugefügt. +### 1. Behebung des synchronen Sortierungs-Flaschenhalses bei Auth-Failure LRU-Eviction +* **Kategorie**: Performance / DoS-Prävention +* **Hintergrund**: Der Server schützt Räume vor Brute-Force-Angriffen durch die Nachverfolgung fehlerhafter Anmeldeversuche (`failedAuthAttempts` Map). Bei Erreichen des Limits von 50.000 Einträgen wird ein Bereinigungsverfahren gestartet, das die gesamte Map in ein Array konvertiert und dieses per `Array.from().sort()` sortiert. +* **Problem**: Dies blockiert den Node.js-Main-Thread für mehrere Millisekunden und stellt einen potenziellen Denial-of-Service-Vektor (DoS) dar, wenn ein Angreifer gezielt Fehlversuche spamt. +* **Geplante Lösung**: + - Umstellung auf eine echte, $O(1)$-basierte LRU-Cache-Datenstruktur (z. B. doppelt verkettete Liste in Kombination mit einer Map). + - Alternativ: Ein vereinfachtes zeitbasiertes Ablauf-Verfahren oder ein schrittweises Löschen von Segmenten (Chunk-Eviction), um Blockaden des Main-Threads vollständig auszuschließen. diff --git a/extension/background.js b/extension/background.js index 31d1a7b..c8e952b 100644 --- a/extension/background.js +++ b/extension/background.js @@ -788,6 +788,8 @@ function handleServerEvent(event, data) { } if (isForceSyncInitiator) { + forceSyncAcks.delete(data.peerId); + chrome.storage.session.set({ forceSyncAcks: Array.from(forceSyncAcks) }); expectedAcksCount = Math.max(1, currentRoom.peers ? currentRoom.peers.length : 1); chrome.storage.session.set({ expectedAcksCount }); if (forceSyncAcks.size >= expectedAcksCount) { @@ -1225,7 +1227,8 @@ async function handleAsyncMessage(message, sender, sendResponse) { emit(EVENTS.GET_ROOMS, {}); sendResponse({ status: 'ok' }); } else if (message.type === 'WEB_JOIN_REQUEST') { - const { roomId, password, useCustomServer, serverUrl } = message; + const { roomId: rawRoomId, password, useCustomServer, serverUrl } = message; + const roomId = typeof rawRoomId === 'string' ? rawRoomId.replace(/[^a-zA-Z0-9\-]/g, '') : ''; chrome.storage.sync.set({ roomId, password, diff --git a/extension/content.js b/extension/content.js index e02f820..79efce8 100644 --- a/extension/content.js +++ b/extension/content.js @@ -52,7 +52,7 @@ // --- Seek Relay Filtering --- // Minimum seek delta (seconds) to report. Prevents HLS/DASH buffering micro-seeks // from being relayed to peers as user-initiated seeks. - const MIN_SEEK_DELTA = 3.0; + const MIN_SEEK_DELTA = 2.0; let lastReportedSeekTime = null; // last currentTime we relayed as a SEEK let seekDebounceTimer = null; // debounce timer for rapid seek events @@ -582,7 +582,7 @@ } // Step 4: Debounce rapid consecutive seeks (e.g. scrubbing) - // — wait 800ms for the user to settle before relaying + // — wait 300ms for the user to settle before relaying if (seekDebounceTimer) clearTimeout(seekDebounceTimer); seekDebounceTimer = setTimeout(() => { seekDebounceTimer = null; @@ -594,7 +594,7 @@ lastReportedSeekTime = settled; reportLog(`[Seek] Relayed @ ${settled.toFixed(2)}s (${finalDeltaStr})`, 'info'); reportEvent(EVENTS.SEEK); - }, 800); + }, 300); }; diff --git a/extension/popup.js b/extension/popup.js index adbb9cd..022101b 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -942,6 +942,10 @@ function showError(msg) { } // --- Action Handlers --- +elements.roomId.addEventListener('input', () => { + elements.roomId.value = elements.roomId.value.replace(/[^a-zA-Z0-9\-]/g, ''); +}); + elements.joinBtn.addEventListener('click', async () => { if (elements.joinBtn.disabled) return; const roomIdInput = elements.roomId.value.trim(); diff --git a/server/index.js b/server/index.js index 01e7a4d..9c449a8 100644 --- a/server/index.js +++ b/server/index.js @@ -1,12 +1,18 @@ import express from 'express'; import { createServer } from 'http'; import { Server } from 'socket.io'; -import bcrypt from 'bcryptjs'; +import crypto from 'crypto'; import dotenv from 'dotenv'; import { EVENTS, OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION } from '../shared/constants.js'; dotenv.config(); +function hashPassword(password) { + if (!password) return null; + const salt = process.env.SERVER_SALT || 'koalasync_salt_3i'; + return crypto.createHmac('sha256', salt).update(password).digest('hex'); +} + const PORT = process.env.PORT || 3000; const MAX_ROOMS = parseInt(process.env.MAX_ROOMS) || 1000; const MAX_PEERS_PER_ROOM = parseInt(process.env.MAX_PEERS_PER_ROOM) || 50; @@ -351,7 +357,7 @@ io.on('connection', (socket) => { return; } - const passwordHash = password ? await bcrypt.hash(password, 10) : null; + const passwordHash = hashPassword(password); room = { passwordHash, peers: new Set(), @@ -376,7 +382,7 @@ io.on('connection', (socket) => { if (!createdByMe) { if (room.passwordHash) { - if (!password || !(await bcrypt.compare(password, room.passwordHash))) { + if (!password || hashPassword(password) !== room.passwordHash) { recordAuthFailure(ip, roomId); log('AUTH', `Invalid password from ${ip} for room ${roomId.substring(0, 3)}***`); socket.emit(EVENTS.ERROR, { message: "Invalid password" });