Files
Cs2-Mapveto/server/test/rcon.test.js
T
mregosar 52c07c88b8 CS2: sv_deltaticks_enforce 0 gegen NETWORK_DISCONNECT_BADDELTATICK
Mit dem Standard 2 trennt CS2 Spieler und CSTV, wenn deren Bestätigungen außer der Reihe kommen. Neue Container starten mit +sv_deltaticks_enforce 0, beim Einrichten jeder Map und auf Messer-Servern setzt die App den Wert zusätzlich per RCON.
2026-11-01 21:44:55 +01:00

87 lines
4.3 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(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')));
});