diff --git a/README.md b/README.md index 6425db1..4e8c173 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,148 @@ -# gameserver-status-api +# GameDig Server Status API +Pterodactyl Egg das Gameserver per [GameDig](https://github.com/gamedig/node-gamedig) abfragt +und die Ergebnisse als JSON-REST-API bereitstellt. + +## Unterstützte Spiele (Auswahl) + +| Spiel | `type` in config.json | +|---|---| +| DayZ | `dayz` | +| Counter-Strike 2 | `cs2` | +| Rust | `rust` | +| ARK: Survival Evolved | `arkse` | +| + 320 weitere | [GameDig Games-Liste](https://github.com/gamedig/node-gamedig/blob/master/GAMES_LIST.md) | + +--- + +## Installation + +1. `egg.json` im Pterodactyl-Panel importieren *(Admin → Nests → Import Egg)* +2. Neuen Server anlegen und einen **Port** zuweisen +3. Server starten → Installationsscript lädt automatisch alle Dateien von diesem Repo +4. `config.json` im **Dateimanager** mit deinen Serveradressen anpassen +5. Server neu starten + +--- + +## Konfiguration + +### config.json (im Pterodactyl-Dateimanager bearbeiten) + +```json +{ + "servers": [ + { + "label": "DayZ Main Server", + "type": "dayz", + "host": "1.2.3.4", + "port": 2302, + "image": "https://example.com/images/dayz.jpg" + }, + { + "label": "CS2 Server", + "type": "cs2", + "host": "1.2.3.4", + "port": 27015, + "image": "https://example.com/images/cs2.jpg" + } + ] +} +``` + +### Egg-Variablen (im Pterodactyl-Panel konfigurierbar) + +| Variable | Standard | Beschreibung | +|---|---|---| +| `QUERY_INTERVAL` | `60` | Abfrageintervall in Sekunden (min. 10) | +| `API_KEY` | *(leer)* | Optionaler API-Schlüssel – leer = offen | + +--- + +## API-Endpunkte + +### `GET /api/servers` + +**Ohne API-Key:** +``` +GET http://dein-server:PORT/api/servers +``` + +**Mit API-Key via Header:** +``` +GET http://dein-server:PORT/api/servers +X-API-Key: dein-key +``` + +**Mit API-Key via URL-Parameter:** +``` +GET http://dein-server:PORT/api/servers?key=dein-key +``` + +**Beispiel-Antwort:** +```json +{ + "updated": "2026-02-26T14:32:00.000Z", + "interval_seconds": 60, + "servers": [ + { + "label": "DayZ Main Server", + "type": "dayz", + "address": "1.2.3.4:2302", + "image": "https://example.com/images/dayz.jpg", + "status": "online", + "players": 12, + "maxPlayers": 60, + "map": "ChernarusPlus", + "name": "My DayZ Server", + "ping": 42, + "connect": "1.2.3.4:2302" + }, + { + "label": "CS2 Server", + "type": "cs2", + "address": "1.2.3.4:27015", + "status": "offline", + "error": "Connection timed out" + } + ] +} +``` + +### `GET /health` + +Kein API-Key erforderlich. Geeignet für Uptime-Monitoring. + +```json +{ "status": "ok", "updated": "2026-02-26T14:32:00.000Z" } +``` + +--- + +## DayZ Besonderheit + +DayZ nutzt einen separaten Query-Port (`Game-Port + 24714`). +Falls die Abfrage fehlschlägt, den Query-Port direkt angeben: + +```json +{ "type": "dayz", "host": "1.2.3.4", "port": 27016 } +``` + +--- + +## Dateien im Container + +``` +/home/container/ +├── server.js ← Hauptscript (API + Query-Loop) +├── entrypoint.sh ← Startscript +├── config.json ← Serverkonfiguration (selbst bearbeiten) +├── package.json ← npm-Konfiguration +└── node_modules/ ← Abhängigkeiten (automatisch installiert) +``` + +## Update + +Um `server.js` oder `entrypoint.sh` zu aktualisieren, einfach die Dateien in +diesem Repo anpassen und den Server im Pterodactyl-Panel **neu installieren** +*(Server → Settings → Reinstall Server)*. diff --git a/config.json b/config.json new file mode 100644 index 0000000..bd8e274 --- /dev/null +++ b/config.json @@ -0,0 +1,32 @@ +{ + "servers": [ + { + "label": "DayZ Main Server", + "type": "dayz", + "host": "1.2.3.4", + "port": 2302, + "image": "https://example.com/images/dayz.jpg" + }, + { + "label": "Counter Strike 2", + "type": "cs2", + "host": "1.2.3.4", + "port": 27015, + "image": "https://example.com/images/cs2.jpg" + }, + { + "label": "Rust Server", + "type": "rust", + "host": "1.2.3.4", + "port": 28015, + "image": "https://example.com/images/rust.jpg" + }, + { + "label": "ARK Server", + "type": "arkse", + "host": "1.2.3.4", + "port": 7777, + "image": "https://example.com/images/ark.jpg" + } + ] +} diff --git a/egg.json b/egg.json new file mode 100644 index 0000000..d305b3c --- /dev/null +++ b/egg.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://schema.pterodactyl.io/egg.v1.json", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2026-02-26T12:00:00+00:00", + "name": "GameDig Server Status API", + "author": "cap@cap-net.ch", + "description": "Fragt Gameserver (DayZ, CS2, Rust, ARK u.v.m.) per GameDig ab und stellt die Ergebnisse als JSON-REST-API zur Verfügung. Optionaler API-Key-Schutz. Konfiguration über config.json im Dateimanager.", + "features": null, + "docker_images": { + "Node.js 20": "ghcr.io/pterodactyl/yolks:nodejs_20" + }, + "file_denylist": [], + "startup": "bash /home/container/entrypoint.sh", + "config": { + "files": "{}", + "startup": { + "done": "GameDig Server Status API" + }, + "logs": "{}", + "stop": "^C" + }, + "scripts": { + "installation": { + "script": "#!/bin/bash\napt-get update -y && apt-get install -y curl\n\ncd /mnt/server\n\nGITEA_RAW=\"https://gitea.cap-net.ch/Cap/gameserver-status-api/raw/branch/main\"\n\necho \"[INFO] Lade Dateien von Gitea herunter...\"\ncurl -sSL \"${GITEA_RAW}/server.js\" -o server.js || { echo '[FEHLER] server.js konnte nicht geladen werden.'; exit 1; }\ncurl -sSL \"${GITEA_RAW}/entrypoint.sh\" -o entrypoint.sh || { echo '[FEHLER] entrypoint.sh konnte nicht geladen werden.'; exit 1; }\nchmod +x entrypoint.sh\n\necho \"[INFO] Erstelle package.json...\"\ncat > package.json << 'EOF'\n{\n \"name\": \"gameserver-status-api\",\n \"version\": \"1.0.0\",\n \"description\": \"GameDig Server Status API\",\n \"main\": \"server.js\",\n \"dependencies\": {\n \"gamedig\": \"^4.1.0\"\n }\n}\nEOF\n\necho \"[INFO] Installiere npm-Pakete...\"\nnpm install --omit=dev\n\nif [ ! -f 'config.json' ]; then\n echo \"[INFO] Lege Beispiel-config.json an...\"\n curl -sSL \"${GITEA_RAW}/config.json\" -o config.json\n echo \"[WICHTIG] Bitte config.json im Dateimanager mit deinen Serveradressen anpassen!\"\nfi\n\necho \"\"\necho \"[DONE] Installation abgeschlossen.\"\necho \"[INFO] Nächste Schritte:\"\necho \" 1. config.json im Dateimanager mit deinen Serveradressen anpassen\"\necho \" 2. Server starten\"", + "container": "ghcr.io/pterodactyl/installers:debian", + "entrypoint": "bash" + } + }, + "variables": [ + { + "name": "Query-Intervall (Sekunden)", + "description": "Wie oft sollen die Gameserver abgefragt werden? Empfohlen: 60. Minimum: 10.", + "env_variable": "QUERY_INTERVAL", + "default_value": "60", + "user_viewable": true, + "user_editable": true, + "rules": "required|numeric|min:10", + "field_type": "text" + }, + { + "name": "API Key (optional)", + "description": "Schützt die API mit einem Key. Abfrage via Header 'X-API-Key' oder URL-Parameter '?key='. Leer lassen = kein Schutz.", + "env_variable": "API_KEY", + "default_value": "", + "user_viewable": true, + "user_editable": true, + "rules": "nullable|string|max:128", + "field_type": "text" + } + ] +} diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..f9d8743 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/bash +cd /home/container + +echo "=== GameDig Server Status API ===" + +# config.json prüfen +if [ ! -f "config.json" ]; then + echo "[FEHLER] config.json nicht gefunden!" + echo "[INFO] Bitte config.json über den Pterodactyl-Dateimanager anlegen." + echo "[INFO] Vorlage: https://gitea.cap-net.ch/Cap/gameserver-status-api/raw/branch/main/config.json" + exit 1 +fi + +# node_modules prüfen +if [ ! -d "node_modules/gamedig" ]; then + echo "[INFO] Installiere Node-Abhängigkeiten..." + npm install --omit=dev + echo "[INFO] Installation abgeschlossen." +fi + +echo "[INFO] Starte server.js..." +echo "" + +node /home/container/server.js diff --git a/server.js b/server.js new file mode 100644 index 0000000..fa309b3 --- /dev/null +++ b/server.js @@ -0,0 +1,167 @@ +'use strict'; + +const { GameDig } = require('gamedig'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +// ─── Konfiguration ──────────────────────────────────────────────────────────── +const CONFIG_PATH = path.join('/home/container', 'config.json'); +const PORT = parseInt(process.env.SERVER_PORT || '3000', 10); +const INTERVAL_SEC = parseInt(process.env.QUERY_INTERVAL || '60', 10); +const API_KEY = process.env.API_KEY || ''; + +// ─── Config laden ───────────────────────────────────────────────────────────── +let config; +try { + config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); + console.log(`[INIT] config.json geladen – ${config.servers.length} Server konfiguriert.`); +} catch (e) { + console.error(`[FEHLER] config.json konnte nicht gelesen werden: ${e.message}`); + console.error(`[FEHLER] Bitte config.json im Pterodactyl-Dateimanager anlegen.`); + process.exit(1); +} + +// ─── Cache ──────────────────────────────────────────────────────────────────── +let cache = { + updated: null, + interval_seconds: INTERVAL_SEC, + servers: [] +}; + +// ─── Server-Abfrage ─────────────────────────────────────────────────────────── +async function queryAll() { + const start = Date.now(); + console.log(`[QUERY] Starte Abfragen (${config.servers.length} Server)...`); + + const results = await Promise.all( + config.servers.map(async (s) => { + try { + const state = await GameDig.query({ + type: s.type, + host: s.host, + port: s.port, + requestRules: s.type === 'dayz', + socketTimeout: 5000, + }); + + console.log(` ✓ [${s.label}] online – ${state.numplayers}/${state.maxplayers} Spieler`); + + return { + label: s.label, + type: s.type, + address: `${s.host}:${s.port}`, + image: s.image || null, + status: 'online', + players: state.numplayers, + maxPlayers: state.maxplayers, + map: state.map || null, + name: state.name || null, + ping: state.ping || null, + connect: state.connect || null, + }; + + } catch (err) { + console.log(` ✗ [${s.label}] offline – ${err.message}`); + return { + label: s.label, + type: s.type, + address: `${s.host}:${s.port}`, + image: s.image || null, + status: 'offline', + error: err.message, + }; + } + }) + ); + + cache = { + updated: new Date().toISOString(), + interval_seconds: INTERVAL_SEC, + servers: results, + }; + + console.log(`[QUERY] Abgeschlossen in ${Date.now() - start}ms.`); +} + +// ─── Auth-Prüfung ───────────────────────────────────────────────────────────── +function checkAuth(req) { + if (!API_KEY) return true; + + const headerKey = req.headers['x-api-key']; + if (headerKey && headerKey === API_KEY) return true; + + const url = new URL(req.url, `http://localhost`); + const queryKey = url.searchParams.get('key'); + if (queryKey && queryKey === API_KEY) return true; + + return false; +} + +// ─── HTTP Server ────────────────────────────────────────────────────────────── +const server = http.createServer((req, res) => { + const url = new URL(req.url, `http://localhost`); + + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Content-Type', 'application/json'); + + // Health-Endpoint (kein Auth erforderlich) + if (url.pathname === '/health') { + res.writeHead(200); + res.end(JSON.stringify({ status: 'ok', updated: cache.updated })); + return; + } + + // API-Endpoint + if (url.pathname === '/api/servers') { + if (!checkAuth(req)) { + res.writeHead(401); + res.end(JSON.stringify({ + error: 'Unauthorized', + hint: 'X-API-Key Header oder ?key= Parameter erforderlich.' + })); + return; + } + + if (!cache.updated) { + res.writeHead(503); + res.end(JSON.stringify({ error: 'Noch keine Daten verfügbar. Bitte kurz warten.' })); + return; + } + + res.writeHead(200); + res.end(JSON.stringify(cache, null, 2)); + return; + } + + // 404 + res.writeHead(404); + res.end(JSON.stringify({ + error: 'Nicht gefunden.', + endpoints: ['/api/servers', '/health'] + })); +}); + +// ─── Start ──────────────────────────────────────────────────────────────────── +server.listen(PORT, () => { + console.log(''); + console.log('╔══════════════════════════════════════════╗'); + console.log('║ GameDig Server Status API ║'); + console.log('╠══════════════════════════════════════════╣'); + console.log(`║ Port: ${String(PORT).padEnd(29)}║`); + console.log(`║ Interval: ${String(INTERVAL_SEC + 's').padEnd(29)}║`); + console.log(`║ API-Key: ${(API_KEY ? 'aktiv' : 'deaktiviert').padEnd(29)}║`); + console.log(`║ GET /api/servers${' '.repeat(24)}║`); + console.log(`║ GET /health${' '.repeat(29)}║`); + console.log('╚══════════════════════════════════════════╝'); + console.log(''); + + queryAll(); + setInterval(queryAll, INTERVAL_SEC * 1000); +}); + +// ─── Graceful Shutdown ──────────────────────────────────────────────────────── +process.on('SIGTERM', () => { + console.log('[INFO] SIGTERM empfangen, Server wird beendet...'); + server.close(() => process.exit(0)); +});