Absicherung: Socket-Eingaben, Geheimnisse in Fehlern, Logparser, Start/Stopp, RCON, Stream-Zugang
- Socket-Nachrichten werden geprüft, Handlerfehler abgefangen, Abfragen pro Verbindung begrenzt (vorher beendete stream:subscribe mit null ohne Anmeldung das Backend). - Fehlermeldungen von Prozessen ohne Befehlszeile und mit geschwärzten Passwörtern/Tokens. - Logparser wertet nur ganze Systemzeilen nach dem Zeitstempel aus; Chat kann keine Map beenden. - Verspätet ankommende Starts nach Stopp, Neuanforderung oder Löschen bauen ihren Container ab. - RCON: kaputte Pakete trennen nur die Verbindung, Befehle ohne Antwort gelten als Fehler. - Organisatoren bekommen einen Stream-Schlüssel nur für ihr Match statt des globalen STREAM_KEY. - Rate-Limiter mit regelmäßiger Bereinigung und Obergrenze, alte Admin-Sitzungen werden entfernt.
This commit is contained in:
1 parent
35bdfc1f67
commit
e18b8b4430
13 files changed
+409
-49
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { BEST_OF, computeVetoState, resolveAction, VetoError } from './veto.js';
|
||||
import crypto from 'node:crypto';
|
||||
import { formatCode, hashToken, normalizeCode, randomCode, randomId, randomToken, safeEqual } from './security.js';
|
||||
import { applyLogLines, initialLiveState, seriesScore, splitLogBody } from './liveScore.js';
|
||||
import { trimLiveState } from './rounds.js';
|
||||
@@ -696,6 +697,10 @@ export class MatchService extends EventEmitter {
|
||||
const recent = (this.chatRate.get(who) ?? []).filter((t) => now - t < 10_000);
|
||||
if (recent.length >= 5) throw new HttpError(429, 'Zu viele Nachrichten, kurz warten', 'rate_limited');
|
||||
this.chatRate.set(who, [...recent, now]);
|
||||
// Gelegentlich alte Einträge anderer Spieler entfernen
|
||||
if (this.chatRate.size > 500) {
|
||||
for (const [k, times] of this.chatRate) if (!times.some((t) => now - t < 10_000)) this.chatRate.delete(k);
|
||||
}
|
||||
|
||||
const player = viewer.playerId ? this.db.prepare('SELECT nickname, team FROM players WHERE id = ?').get(viewer.playerId) : null;
|
||||
this.db.prepare(`
|
||||
@@ -823,8 +828,30 @@ export class MatchService extends EventEmitter {
|
||||
* Lobbys und kürzlich beendete Matches knapp. Enthält nur, was Zuschauer sehen dürfen –
|
||||
* keine Codes, keine Verbindungsdaten.
|
||||
*/
|
||||
streamOverview({ resultsWithinMs = 3 * 3_600_000 } = {}) {
|
||||
const rows = this.db.prepare('SELECT * FROM matches ORDER BY created_at DESC LIMIT 200').all();
|
||||
/** Schlüssel für die Stream-Übersicht nur eines Matches, abgeleitet aus STREAM_KEY (ohne ihn preiszugeben). */
|
||||
matchStreamKey(matchId) {
|
||||
const mac = crypto.createHmac('sha256', this.config.streamKey).update(`stream:${matchId}`).digest('base64url').slice(0, 22);
|
||||
return `${matchId}.${mac}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft einen Stream-Schlüssel: { matchId: null } = alle Matches, { matchId } = nur dieses Match, null = kein Zugang.
|
||||
* Ohne STREAM_KEY ist die Übersicht offen.
|
||||
*/
|
||||
streamAccess(key) {
|
||||
if (!this.config.streamKey) return { matchId: null };
|
||||
const value = String(key ?? '');
|
||||
if (safeEqual(value, this.config.streamKey)) return { matchId: null };
|
||||
const dot = value.lastIndexOf('.');
|
||||
const matchId = dot > 0 ? value.slice(0, dot) : '';
|
||||
if (matchId && safeEqual(value, this.matchStreamKey(matchId))) return { matchId };
|
||||
return null;
|
||||
}
|
||||
|
||||
streamOverview({ resultsWithinMs = 3 * 3_600_000, matchId = null } = {}) {
|
||||
const rows = matchId
|
||||
? this.db.prepare('SELECT * FROM matches WHERE id = ?').all(matchId)
|
||||
: this.db.prepare('SELECT * FROM matches ORDER BY created_at DESC LIMIT 200').all();
|
||||
const now = this.now();
|
||||
const active = [];
|
||||
const upcoming = [];
|
||||
@@ -1047,7 +1074,7 @@ export class MatchService extends EventEmitter {
|
||||
// Vollbild-Übersicht für den Stream (OBS-Browserquelle), nutzt den Zuschauercode
|
||||
stream: `${base}/stream/${formatCode(match.code_spectator)}`,
|
||||
// Stream-Übersicht nur für dieses Match, ohne Zuschauercode (Schlüssel nur wenn gesetzt)
|
||||
overview: `${base}/stream/match/${match.id}${this.config.streamKey ? `?key=${encodeURIComponent(this.config.streamKey)}` : ''}`,
|
||||
overview: `${base}/stream/match/${match.id}${this.config.streamKey ? `?key=${encodeURIComponent(this.matchStreamKey(match.id))}` : ''}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user