UNPKG

@rlqd/minecraft-server-util

Version:

A Node.js library for Minecraft servers that can retrieve status, perform queries, and RCON into servers.

198 lines (159 loc) 6.16 kB
import assert from 'assert'; import { clean, format, parse, toHTML } from '@rlqd/minecraft-motd-util'; import UDPClient from './structure/UDPClient'; import { QueryOptions } from './types/QueryOptions'; import { resolveSRV } from './util/srvRecord'; const validKeys: Buffer[] = [ 'gametype', 'game_id', 'version', 'plugins', 'map', 'numplayers', 'maxplayers', 'hostport', 'hostip' ].map(s => Buffer.from(s, 'ascii')); export interface FullQueryResponse { motd: { raw: string, clean: string, html: string }, version: string, software: string, plugins: string[], map: string, players: { online: number, max: number, list: string[] }, hostIP: string, hostPort: number } export function queryFull(host: string, port = 25565, options?: QueryOptions): Promise<FullQueryResponse> { host = host.trim(); assert(typeof host === 'string', `Expected 'host' to be a 'string', got '${typeof host}'`); assert(host.length > 0, `Expected 'host' to have a length greater than 0, got ${host.length}`); assert(typeof port === 'number', `Expected 'port' to be a 'number', got '${typeof port}'`); assert(Number.isInteger(port), `Expected 'port' to be an integer, got '${port}'`); assert(port >= 0, `Expected 'port' to be greater than or equal to 0, got '${port}'`); assert(port <= 65535, `Expected 'port' to be less than or equal to 65535, got '${port}'`); assert(typeof options === 'object' || typeof options === 'undefined', `Expected 'options' to be an 'object' or 'undefined', got '${typeof options}'`); if (typeof options === 'object') { assert(typeof options.enableSRV === 'boolean' || typeof options.enableSRV === 'undefined', `Expected 'options.enableSRV' to be a 'boolean' or 'undefined', got '${typeof options.enableSRV}'`); assert(typeof options.sessionID === 'number' || typeof options.sessionID === 'undefined', `Expected 'options.sessionID' to be a 'number' or 'undefined', got '${typeof options.sessionID}'`); assert(typeof options.timeout === 'number' || typeof options.timeout === 'undefined', `Expected 'options.timeout' to be a 'number' or 'undefined', got '${typeof options.timeout}'`); if (typeof options.timeout === 'number') { assert(Number.isInteger(options.timeout), `Expected 'options.timeout' to be an integer, got '${options.timeout}'`); assert(options.timeout >= 0, `Expected 'options.timeout' to be greater than or equal to 0, got '${options.timeout}'`); } } const sessionID = (options?.sessionID ?? 1) & 0x0F0F0F0F; return new Promise(async (resolve, reject) => { const socket = new UDPClient(host, port); const timeout = setTimeout(() => { socket?.close(); reject(new Error('Server is offline or unreachable')); }, options?.timeout ?? 1000 * 5); try { let srvRecord = null; if (typeof options === 'undefined' || typeof options.enableSRV === 'undefined' || options.enableSRV) { srvRecord = await resolveSRV(host, 'udp'); if (srvRecord) { host = srvRecord.host; port = srvRecord.port; } } // Request packet // https://minecraft.wiki/w/Java_Edition_protocol/Query#Request { socket.writeUInt16BE(0xFEFD); socket.writeByte(0x09); socket.writeInt32BE(sessionID); await socket.flush(false); } let challengeToken; // Response packet // https://minecraft.wiki/w/Java_Edition_protocol/Query#Response { const packetType = await socket.readByte(); if (packetType !== 0x09) throw new Error('Expected server to send packet type 0x09, received ' + packetType); const serverSessionID = await socket.readInt32BE(); if (sessionID !== serverSessionID) throw new Error('Server session ID mismatch, expected ' + sessionID + ', received ' + serverSessionID); challengeToken = parseInt(await socket.readStringNT()); if (isNaN(challengeToken)) throw new Error('Server sent an invalid challenge token'); } // Full stat request packet // https://minecraft.wiki/w/Java_Edition_protocol/Query#Request_3 { socket.writeUInt16BE(0xFEFD); socket.writeByte(0x00); socket.writeInt32BE(sessionID); socket.writeInt32BE(challengeToken); socket.writeBytes(Uint8Array.from([0x00, 0x00, 0x00, 0x00])); await socket.flush(false); } // Full stat response packet // https://minecraft.wiki/w/Java_Edition_protocol/Query#Response_3 { const packetType = await socket.readByte(); if (packetType !== 0x00) throw new Error('Expected server to send packet type 0x00, received ' + packetType); const serverSessionID = await socket.readInt32BE(); if (sessionID !== serverSessionID) throw new Error('Server session ID mismatch, expected ' + sessionID + ', received ' + serverSessionID); await socket.readBytes(11); const data: Record<string, string> = {}; const players: string[] = []; // eslint-disable-next-line no-constant-condition while (true) { const key = await socket.readStringNT(); if (key.length < 1) break; let value; if (key === 'hostname') { value = await socket.readStringNTFollowedBy(validKeys); } else { value = await socket.readStringNT(); } data[key] = value; } await socket.readBytes(10); // eslint-disable-next-line no-constant-condition while (true) { const username = await socket.readStringNT(); if (username.length < 1) break; players.push(username); } const motd = parse(data.hostname); const plugins = data.plugins.split(/(?::|;) */g); socket.close(); if (socket.hasRemainingData()) { throw new Error('Server sent more data than expected'); } clearTimeout(timeout); resolve({ motd: { raw: format(motd), clean: clean(motd), html: toHTML(motd) }, version: data.version, software: plugins[0], plugins: plugins.slice(1), map: data.map, players: { online: parseInt(data.numplayers), max: parseInt(data.maxplayers), list: players }, hostIP: data.hostip, hostPort: parseInt(data.hostport) }); } } catch (e) { clearTimeout(timeout); socket?.close(); reject(e); } }); }