Admin: Live-Konsole der Container und Live-Log der Docker-Hosts (nur lesen, Passwörter ausgeblendet)
This commit is contained in:
1 parent
fb96fb5d08
commit
ee9c488a7e
9 files changed
+328
-3
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import express from 'express';
|
||||
import { formatPortList } from './config.js';
|
||||
import { formatDockerEvent } from './servers/dockerAdapter.js';
|
||||
import { HttpError } from './matchService.js';
|
||||
import { createRateLimiter, randomToken, safeEqual } from './security.js';
|
||||
|
||||
@@ -164,6 +165,93 @@ export function createAdminRouter({ service, manager, config, settings, knife, p
|
||||
}));
|
||||
|
||||
// Alle Matches mit allen Details (Codes, Verbindungsdaten, Spieler, Veto, Serie, Ereignisse)
|
||||
// Konsolenausgabe eines Containers live (Match-Server oder Messer-Server). Passwörter werden ausgeblendet.
|
||||
let openStreams = 0;
|
||||
router.get('/logs/:kind/:id', requireAdmin, (req, res, next) => {
|
||||
try {
|
||||
const { kind, id } = req.params;
|
||||
const row = kind === 'match' ? manager.row(id) : kind === 'knife' ? knife.row(id) : null;
|
||||
if (!row) throw new HttpError(404, 'Server nicht gefunden', 'not_found');
|
||||
if (!row.handle) throw new HttpError(409, 'Für diesen Server gibt es noch keinen Container', 'no_container');
|
||||
if (!manager.adapter.streamLogs) throw new HttpError(501, 'Dieser Adapter hat keine Konsolenausgabe', 'unsupported');
|
||||
if (openStreams >= 8) throw new HttpError(429, 'Zu viele offene Konsolen, erst eine schließen', 'rate_limited');
|
||||
const tail = Math.max(0, Math.min(2000, Number.parseInt(req.query.tail, 10) || 200));
|
||||
const secrets = [row.rcon_password, row.server_password, row.tv_password].filter((s) => s && s.length >= 4);
|
||||
const mask = (text) => secrets.reduce((t, s) => t.split(s).join('••••'), text);
|
||||
|
||||
const proc = manager.adapter.streamLogs(JSON.parse(row.handle), { tail });
|
||||
openStreams += 1;
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no' });
|
||||
// Zeilenweise ausgeben, damit ein Passwort nie an einer Paketgrenze zerrissen und so doch sichtbar wird
|
||||
const pipe = (stream) => {
|
||||
let rest = '';
|
||||
stream.setEncoding?.('utf8');
|
||||
stream.on('data', (chunk) => {
|
||||
const lines = (rest + chunk).split('\n');
|
||||
rest = lines.pop();
|
||||
if (lines.length) res.write(mask(lines.join('\n')) + '\n');
|
||||
});
|
||||
stream.on('end', () => { if (rest) res.write(mask(rest) + '\n'); });
|
||||
};
|
||||
pipe(proc.stdout);
|
||||
pipe(proc.stderr);
|
||||
let done = false;
|
||||
const finish = (note) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
openStreams -= 1;
|
||||
if (note && !res.writableEnded) res.end(`\n[${note}]\n`);
|
||||
};
|
||||
proc.on('close', () => finish('Ausgabe beendet'));
|
||||
proc.on?.('error', (err) => finish(`Fehler: ${err.message}`));
|
||||
req.on('close', () => {
|
||||
proc.kill();
|
||||
finish(null);
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Live-Log eines Docker-Hosts: seine Ereignisse (Container gestartet, beendet, entfernt, Images geladen …). Nur lesen.
|
||||
router.get('/host-log/:name', requireAdmin, (req, res, next) => {
|
||||
try {
|
||||
if (!manager.adapter.streamEvents) throw new HttpError(501, 'Nur mit dem Docker-Adapter möglich', 'unsupported');
|
||||
if (openStreams >= 8) throw new HttpError(429, 'Zu viele offene Konsolen, erst eine schließen', 'rate_limited');
|
||||
const since = Math.max(0, Math.min(24 * 60, Number.parseInt(req.query.since, 10) || 120));
|
||||
const proc = manager.adapter.streamEvents(req.params.name, { sinceMinutes: since });
|
||||
if (!proc) throw new HttpError(404, 'Docker-Host nicht gefunden', 'not_found');
|
||||
openStreams += 1;
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no' });
|
||||
res.write(`Ereignisse von ${req.params.name} seit ${since} Minuten, neue erscheinen live.\n`);
|
||||
let rest = '';
|
||||
proc.stdout.setEncoding('utf8');
|
||||
proc.stdout.on('data', (chunk) => {
|
||||
const lines = (rest + chunk).split('\n');
|
||||
rest = lines.pop();
|
||||
const out = lines.map(formatDockerEvent).filter(Boolean);
|
||||
if (out.length) res.write(out.join('\n') + '\n');
|
||||
});
|
||||
proc.stderr.setEncoding('utf8');
|
||||
proc.stderr.on('data', (chunk) => res.write(`[docker] ${chunk}`));
|
||||
let done = false;
|
||||
const finish = (note) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
openStreams -= 1;
|
||||
if (note && !res.writableEnded) res.end(`\n[${note}]\n`);
|
||||
};
|
||||
proc.on('close', () => finish('Ausgabe beendet'));
|
||||
proc.on('error', (err) => finish(`Fehler: ${err.message}`));
|
||||
req.on('close', () => {
|
||||
proc.kill();
|
||||
finish(null);
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Auslastung und Speicher der Docker-Hosts mit den Servern darauf
|
||||
router.get('/hosts', requireAdmin, wrap(async () => ({ hosts: await manager.hostUsage() })));
|
||||
|
||||
|
||||
Reference in new issue
Block a user