- Das Image schrieb TV_DELAY (Standard 0) bei jedem Containerstart in alle gamemode_*.cfg, die bei jedem Laden einer Map laufen: tv_delay stand danach immer auf 0. Die App gibt TV_DELAY jetzt mit und setzt tv_delay zusätzlich nach dem Laden der Map per RCON. - Am Serienende (automatisch oder von Hand abgeschlossen) bleibt der Server die CSTV-Verzögerung plus 15 s an, damit der Stream das Ende zeigt. Abgebrochene Matches stoppen sofort. - Ingame-Chat: Hinweis, dass der Server für CSTV noch online bleibt und sich dann abschaltet; beim Mapwechsel die echte Wartezeit nach der CSTV-Übertragung (tv_delaymapchange).
88 lines
4.5 KiB
JavaScript
88 lines
4.5 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import net from 'node:net';
|
|
import { decodePackets, encodePacket, parseStatus, runRcon } from '../src/servers/rcon.js';
|
|
import { buildMapCommands, sanitizeConsoleText } from '../src/servers/cs2Config.js';
|
|
|
|
/** Minimaler Source-RCON-Server für Tests. */
|
|
function fakeRconServer(password, onCommand) {
|
|
const server = net.createServer((socket) => {
|
|
let buffer = Buffer.alloc(0);
|
|
socket.on('data', (chunk) => {
|
|
buffer = Buffer.concat([buffer, chunk]);
|
|
const { packets, rest } = decodePackets(buffer);
|
|
buffer = rest;
|
|
for (const p of packets) {
|
|
if (p.type === 3) {
|
|
socket.write(encodePacket(p.body === password ? p.id : -1, 2, ''));
|
|
} else {
|
|
socket.write(encodePacket(p.id, 0, onCommand(p.body)));
|
|
}
|
|
}
|
|
});
|
|
});
|
|
return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server)));
|
|
}
|
|
|
|
test('Paket-Kodierung ist umkehrbar', () => {
|
|
const buf = Buffer.concat([encodePacket(7, 2, 'status'), encodePacket(8, 2, 'mp_warmup_end')]);
|
|
const { packets, rest } = decodePackets(buf);
|
|
assert.deepEqual(packets.map((p) => [p.id, p.body]), [[7, 'status'], [8, 'mp_warmup_end']]);
|
|
assert.equal(rest.length, 0);
|
|
const partial = decodePackets(buf.subarray(0, 10));
|
|
assert.equal(partial.packets.length, 0);
|
|
});
|
|
|
|
test('RCON-Client authentifiziert und führt Befehle aus', async () => {
|
|
const seen = [];
|
|
const server = await fakeRconServer('geheim', (cmd) => {
|
|
seen.push(cmd);
|
|
return cmd === 'status' ? 'map : de_mirage\nplayers : 3 humans, 0 bots (10 max)' : '';
|
|
});
|
|
const { port } = server.address();
|
|
try {
|
|
const [out] = await runRcon({ host: '127.0.0.1', port, password: 'geheim' }, ['status', 'mp_warmup_end']);
|
|
assert.deepEqual(parseStatus(out), { map: 'de_mirage', playerCount: 3 });
|
|
assert.deepEqual(seen, ['status', 'mp_warmup_end']);
|
|
await assert.rejects(runRcon({ host: '127.0.0.1', port, password: 'falsch' }, ['status']), /abgelehnt/);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test('Konsolentexte können keine Befehle einschleusen', () => {
|
|
assert.equal(sanitizeConsoleText('Team"; rcon_password x'), 'Team rcon_password x');
|
|
const { beforeMapChange, afterMapChange } = buildMapCommands({
|
|
match: { team_a_name: 'Alpha";quit', team_b_name: 'Bravo' },
|
|
pool: { mode: 'wingman', gameType: 0, gameMode: 2 },
|
|
seriesMap: { map: 'de_nuke', ctTeam: 'B' },
|
|
secrets: { serverPassword: 'abc', tvPassword: 'def' },
|
|
settings: { tvEnabled: true, tvDelaySeconds: 90, maxRoundsWingman: 16, maxRoundsCompetitive: 24, overtime: true, extraCommands: [] },
|
|
});
|
|
assert.ok(beforeMapChange.includes('game_mode 2'));
|
|
assert.equal(beforeMapChange.at(-1), 'changelevel de_nuke');
|
|
assert.ok(afterMapChange.includes('mp_teamname_1 "Bravo"'));
|
|
assert.ok(afterMapChange.includes('mp_teamname_2 "Alphaquit"'));
|
|
assert.ok(afterMapChange.includes('mp_maxrounds 16'));
|
|
assert.ok(afterMapChange.includes('sv_pausable 1'), 'Pausen-Knopf braucht sv_pausable');
|
|
assert.ok(afterMapChange.includes('mp_freezetime 20'));
|
|
});
|
|
|
|
test('Log-Weiterleitung wird vor dem Mapwechsel gesetzt, unsichere URLs nicht', () => {
|
|
const base = {
|
|
match: { team_a_name: 'A', team_b_name: 'B' },
|
|
pool: { mode: 'competitive', gameType: 0, gameMode: 1 },
|
|
seriesMap: { map: 'de_nuke', ctTeam: 'A' },
|
|
secrets: { serverPassword: 'abc', tvPassword: 'def' },
|
|
settings: { tvEnabled: true, tvDelaySeconds: 90, maxRoundsWingman: 16, maxRoundsCompetitive: 24, overtime: true, extraCommands: [] },
|
|
};
|
|
const ok = buildMapCommands({ ...base, logUrl: 'http://192.168.178.31:3000/api/logs/abc_D-1/KEY123' }).beforeMapChange;
|
|
assert.ok(ok.includes('logaddress_add_http "http://192.168.178.31:3000/api/logs/abc_D-1/KEY123"'));
|
|
assert.ok(buildMapCommands({ ...base, logUrl: 'http://x/api/logs/a/b' }).afterMapChange.includes('mp_endmatch_votenextmap 0'), 'keine Map-Abstimmung nach dem Match');
|
|
assert.ok(buildMapCommands({ ...base, logUrl: 'http://x/api/logs/a/b' }).afterMapChange.includes('sv_deltaticks_enforce 0'), 'keine Trennung wegen Delta-Ticks');
|
|
assert.ok(buildMapCommands({ ...base, logUrl: 'http://x/api/logs/a/b' }).afterMapChange.some((c) => /^tv_delay \d+$/.test(c)), 'tv_delay nach dem Laden der Map');
|
|
assert.ok(ok.indexOf('log on') < ok.indexOf('changelevel de_nuke'));
|
|
const bad = buildMapCommands({ ...base, logUrl: 'http://x"; quit' }).beforeMapChange;
|
|
assert.ok(!bad.some((c) => c.startsWith('logaddress')));
|
|
});
|