'use strict'; /* * mcping.js – "Server List Ping" (SLP), das Protokoll, das auch der * Vanilla-Mehrspieler-Bildschirm nutzt, um MOTD/Spielerzahl/Version * abzufragen, ohne sich wirklich zu verbinden. * * Ablauf (siehe minecraft.wiki/w/Server_List_Ping): * 1) Handshake-Paket senden (Protokollversion, Adresse, Port, next state=1) * 2) Status-Request-Paket senden (leer) * 3) Status-Response-Paket empfangen: enthält einen JSON-String * Alle Pakete sind mit ihrer Länge als VarInt vorangestellt. */ const net = require('net'); function encodeVarInt(value) { const bytes = []; let v = value; do { let temp = v & 0b01111111; v = v >>> 7; if (v !== 0) temp |= 0b10000000; bytes.push(temp); } while (v !== 0); return Buffer.from(bytes); } function encodeString(str) { const strBuf = Buffer.from(str, 'utf8'); return Buffer.concat([encodeVarInt(strBuf.length), strBuf]); } function encodeUShort(n) { const b = Buffer.alloc(2); b.writeUInt16BE(n & 0xffff, 0); return b; } // liest einen VarInt aus buf ab offset -> { value, next } oder null, // wenn noch nicht genug Bytes angekommen sind function decodeVarInt(buf, offset) { let numRead = 0; let result = 0; let read; do { if (offset + numRead >= buf.length) return null; read = buf[offset + numRead]; result |= (read & 0b01111111) << (7 * numRead); numRead++; if (numRead > 5) throw new Error('VarInt zu groß'); } while ((read & 0b10000000) !== 0); return { value: result >>> 0, next: offset + numRead }; } // "description" kann ein reiner String oder eine Chat-Komponente // ({text, extra:[...]}) sein – hier auf reinen Text reduzieren function extractMotdText(desc) { if (!desc) return ''; if (typeof desc === 'string') return desc; let text = String(desc.text || ''); if (Array.isArray(desc.extra)) { for (const part of desc.extra) text += extractMotdText(part); } return text; } // Formatierungscodes (§a, §l, …) sind fürs Spiel gedacht, nicht für unsere Liste function stripFormatting(text) { return String(text || '').replace(/§./g, '').replace(/\s+/g, ' ').trim(); } /* * Fragt einen Server ab. * -> { ok, motd, version, protocol, online, max, sample, favicon, latencyMs } * oder { ok: false, message } */ function pingServer(host, port = 25565, timeoutMs = 4000) { return new Promise((resolve) => { const start = Date.now(); const socket = new net.Socket(); let buffer = Buffer.alloc(0); let finished = false; const done = (result) => { if (finished) return; finished = true; try { socket.destroy(); } catch { /* egal */ } resolve(result); }; socket.setTimeout(timeoutMs); socket.on('timeout', () => done({ ok: false, message: 'Zeitüberschreitung.' })); socket.on('error', (err) => done({ ok: false, message: err.code === 'ENOTFOUND' ? 'Adresse nicht gefunden.' : err.message })); socket.connect(port, host, () => { const handshakeBody = Buffer.concat([ encodeVarInt(0x00), encodeVarInt(-1), // Protokollversion "unbekannt" – für den Status-Abfrage-Zweck egal encodeString(host), encodeUShort(port), encodeVarInt(1), // next state: 1 = status ]); const handshakePacket = Buffer.concat([encodeVarInt(handshakeBody.length), handshakeBody]); const requestBody = encodeVarInt(0x00); const requestPacket = Buffer.concat([encodeVarInt(requestBody.length), requestBody]); socket.write(Buffer.concat([handshakePacket, requestPacket])); }); socket.on('data', (chunk) => { if (finished) return; buffer = Buffer.concat([buffer, chunk]); try { const lenInfo = decodeVarInt(buffer, 0); if (!lenInfo) return; // Länge noch nicht vollständig angekommen const packetEnd = lenInfo.next + lenInfo.value; if (buffer.length < packetEnd) return; // Paket noch nicht vollständig da const idInfo = decodeVarInt(buffer, lenInfo.next); if (!idInfo) return; const jsonLenInfo = decodeVarInt(buffer, idInfo.next); if (!jsonLenInfo) return; const jsonStart = jsonLenInfo.next; const jsonEnd = jsonStart + jsonLenInfo.value; if (buffer.length < jsonEnd) return; const data = JSON.parse(buffer.toString('utf8', jsonStart, jsonEnd)); done({ ok: true, latencyMs: Date.now() - start, motd: stripFormatting(extractMotdText(data.description)), version: (data.version && data.version.name) || '', protocol: data.version ? data.version.protocol : null, online: data.players ? data.players.online : null, max: data.players ? data.players.max : null, sample: (data.players && Array.isArray(data.players.sample)) ? data.players.sample.map((p) => String(p.name || '')).filter(Boolean) : [], favicon: typeof data.favicon === 'string' ? data.favicon : '', }); } catch (err) { done({ ok: false, message: 'Ungültige Antwort: ' + err.message }); } }); }); } module.exports = { pingServer };