mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-20 16:12:16 +00:00
Refactor: Behebung Force-Sync ACK-Zaehler-Inflation, SHA-256 Hashing, Room-ID-Desinfizierung, Reduzierung MIN_SEEK_DELTA auf 2s und seekDebounce auf 300ms
This commit is contained in:
@@ -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.
|
- **Server Transport**: Restricted to `websocket` only. Polling is disabled.
|
||||||
- **Docker Context**: The Docker build must run from the **Repo Root**.
|
- **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`.
|
- **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
|
## 9. Security & Deployment
|
||||||
- **Tokens**: Security tokens are intentionally managed via `shared/constants.js` and server `.env`.
|
- **Tokens**: Security tokens are intentionally managed via `shared/constants.js` and server `.env`.
|
||||||
|
|||||||
+9
-20
@@ -1,26 +1,15 @@
|
|||||||
# KoalaSync Roadmap
|
# 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
|
### 1. Behebung des synchronen Sortierungs-Flaschenhalses bei Auth-Failure LRU-Eviction
|
||||||
Manifest V3 suspendiert den Service Worker nach ~30s Inaktivität. `chrome.alarms` weckt ihn auf, aber:
|
* **Kategorie**: Performance / DoS-Prävention
|
||||||
- **Problem:** Wenn der SW neu startet, sind alle Variablen (`currentRoom`, `socket`, `isNamespaceJoined`) weg
|
* **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.
|
||||||
- **Aktueller Stand:** `chrome.storage.session` persistiert `currentRoom`, `peerId`, `eventQueue` — der SW stellt diese beim Start wieder her (`ensureState()`)
|
* **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.
|
||||||
- **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).
|
* **Geplante Lösung**:
|
||||||
|
- Umstellung auf eine echte, $O(1)$-basierte LRU-Cache-Datenstruktur (z. B. doppelt verkettete Liste in Kombination mit einer Map).
|
||||||
### 7. Tests für Extensions
|
- Alternativ: Ein vereinfachtes zeitbasiertes Ablauf-Verfahren oder ein schrittweises Löschen von Segmenten (Chunk-Eviction), um Blockaden des Main-Threads vollständig auszuschließen.
|
||||||
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.
|
|
||||||
|
|||||||
@@ -788,6 +788,8 @@ function handleServerEvent(event, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isForceSyncInitiator) {
|
if (isForceSyncInitiator) {
|
||||||
|
forceSyncAcks.delete(data.peerId);
|
||||||
|
chrome.storage.session.set({ forceSyncAcks: Array.from(forceSyncAcks) });
|
||||||
expectedAcksCount = Math.max(1, currentRoom.peers ? currentRoom.peers.length : 1);
|
expectedAcksCount = Math.max(1, currentRoom.peers ? currentRoom.peers.length : 1);
|
||||||
chrome.storage.session.set({ expectedAcksCount });
|
chrome.storage.session.set({ expectedAcksCount });
|
||||||
if (forceSyncAcks.size >= expectedAcksCount) {
|
if (forceSyncAcks.size >= expectedAcksCount) {
|
||||||
@@ -1225,7 +1227,8 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
|||||||
emit(EVENTS.GET_ROOMS, {});
|
emit(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: rawRoomId, password, useCustomServer, serverUrl } = message;
|
||||||
|
const roomId = typeof rawRoomId === 'string' ? rawRoomId.replace(/[^a-zA-Z0-9\-]/g, '') : '';
|
||||||
chrome.storage.sync.set({
|
chrome.storage.sync.set({
|
||||||
roomId,
|
roomId,
|
||||||
password,
|
password,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
// --- Seek Relay Filtering ---
|
// --- Seek Relay Filtering ---
|
||||||
// Minimum seek delta (seconds) to report. Prevents HLS/DASH buffering micro-seeks
|
// Minimum seek delta (seconds) to report. Prevents HLS/DASH buffering micro-seeks
|
||||||
// from being relayed to peers as user-initiated 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 lastReportedSeekTime = null; // last currentTime we relayed as a SEEK
|
||||||
let seekDebounceTimer = null; // debounce timer for rapid seek events
|
let seekDebounceTimer = null; // debounce timer for rapid seek events
|
||||||
|
|
||||||
@@ -582,7 +582,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Debounce rapid consecutive seeks (e.g. scrubbing)
|
// 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);
|
if (seekDebounceTimer) clearTimeout(seekDebounceTimer);
|
||||||
seekDebounceTimer = setTimeout(() => {
|
seekDebounceTimer = setTimeout(() => {
|
||||||
seekDebounceTimer = null;
|
seekDebounceTimer = null;
|
||||||
@@ -594,7 +594,7 @@
|
|||||||
lastReportedSeekTime = settled;
|
lastReportedSeekTime = settled;
|
||||||
reportLog(`[Seek] Relayed @ ${settled.toFixed(2)}s (${finalDeltaStr})`, 'info');
|
reportLog(`[Seek] Relayed @ ${settled.toFixed(2)}s (${finalDeltaStr})`, 'info');
|
||||||
reportEvent(EVENTS.SEEK);
|
reportEvent(EVENTS.SEEK);
|
||||||
}, 800);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -942,6 +942,10 @@ function showError(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Action Handlers ---
|
// --- Action Handlers ---
|
||||||
|
elements.roomId.addEventListener('input', () => {
|
||||||
|
elements.roomId.value = elements.roomId.value.replace(/[^a-zA-Z0-9\-]/g, '');
|
||||||
|
});
|
||||||
|
|
||||||
elements.joinBtn.addEventListener('click', async () => {
|
elements.joinBtn.addEventListener('click', async () => {
|
||||||
if (elements.joinBtn.disabled) return;
|
if (elements.joinBtn.disabled) return;
|
||||||
const roomIdInput = elements.roomId.value.trim();
|
const roomIdInput = elements.roomId.value.trim();
|
||||||
|
|||||||
+9
-3
@@ -1,12 +1,18 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { createServer } from 'http';
|
import { createServer } from 'http';
|
||||||
import { Server } from 'socket.io';
|
import { Server } from 'socket.io';
|
||||||
import bcrypt from 'bcryptjs';
|
import crypto from 'crypto';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import { EVENTS, OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION } from '../shared/constants.js';
|
import { EVENTS, OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION } from '../shared/constants.js';
|
||||||
|
|
||||||
dotenv.config();
|
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 PORT = process.env.PORT || 3000;
|
||||||
const MAX_ROOMS = parseInt(process.env.MAX_ROOMS) || 1000;
|
const MAX_ROOMS = parseInt(process.env.MAX_ROOMS) || 1000;
|
||||||
const MAX_PEERS_PER_ROOM = parseInt(process.env.MAX_PEERS_PER_ROOM) || 50;
|
const MAX_PEERS_PER_ROOM = parseInt(process.env.MAX_PEERS_PER_ROOM) || 50;
|
||||||
@@ -351,7 +357,7 @@ io.on('connection', (socket) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordHash = password ? await bcrypt.hash(password, 10) : null;
|
const passwordHash = hashPassword(password);
|
||||||
room = {
|
room = {
|
||||||
passwordHash,
|
passwordHash,
|
||||||
peers: new Set(),
|
peers: new Set(),
|
||||||
@@ -376,7 +382,7 @@ io.on('connection', (socket) => {
|
|||||||
|
|
||||||
if (!createdByMe) {
|
if (!createdByMe) {
|
||||||
if (room.passwordHash) {
|
if (room.passwordHash) {
|
||||||
if (!password || !(await bcrypt.compare(password, room.passwordHash))) {
|
if (!password || hashPassword(password) !== room.passwordHash) {
|
||||||
recordAuthFailure(ip, roomId);
|
recordAuthFailure(ip, roomId);
|
||||||
log('AUTH', `Invalid password from ${ip} for room ${roomId.substring(0, 3)}***`);
|
log('AUTH', `Invalid password from ${ip} for room ${roomId.substring(0, 3)}***`);
|
||||||
socket.emit(EVENTS.ERROR, { message: "Invalid password" });
|
socket.emit(EVENTS.ERROR, { message: "Invalid password" });
|
||||||
|
|||||||
Reference in New Issue
Block a user