refactor: improve Bedrock error handling and validation

This commit is contained in:
Timofey Gelazoniya 2025-02-07 08:48:04 +03:00
parent c71236f223
commit 0959403b1b
Signed by: zeldon
GPG Key ID: 047886915281DD2A
3 changed files with 75 additions and 44 deletions

View File

@ -10,6 +10,7 @@ import crypto from "node:crypto";
const MAGIC = "00ffff00fefefefefdfdfdfd12345678"; const MAGIC = "00ffff00fefefefefdfdfdfd12345678";
const START_TIME = new Date().getTime(); const START_TIME = new Date().getTime();
const UNCONNECTED_PONG = 0x1c;
/** /**
* Creates an Unconnected Ping packet. * Creates an Unconnected Ping packet.
@ -30,12 +31,25 @@ const createUnconnectedPingFrame = (timestamp) => {
* Extract Modt from Unconnected Pong Packet and convert to an object * Extract Modt from Unconnected Pong Packet and convert to an object
* @param {Buffer} unconnectedPongPacket * @param {Buffer} unconnectedPongPacket
* @returns {Object} * @returns {Object}
* @throws {Error} If packet is malformed or invalid
* @see {@link https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Raknet_Protocol#Unconnected_Pong} * @see {@link https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Raknet_Protocol#Unconnected_Pong}
*/ */
const extractModt = (unconnectedPongPacket) => { const extractModt = (unconnectedPongPacket) => {
// Skip everything to Modt if (
!Buffer.isBuffer(unconnectedPongPacket) ||
unconnectedPongPacket.length < 35
) {
throw new Error("Invalid pong packet");
}
const offset = 33; const offset = 33;
const length = unconnectedPongPacket.readUInt16BE(offset); const length = unconnectedPongPacket.readUInt16BE(offset);
// Check for buffer bounds
if (offset + 2 + length > unconnectedPongPacket.length) {
throw new Error("Malformed pong packet");
}
let modt = unconnectedPongPacket.toString( let modt = unconnectedPongPacket.toString(
"utf-8", "utf-8",
offset + 2, offset + 2,
@ -43,6 +57,12 @@ const extractModt = (unconnectedPongPacket) => {
); );
const components = modt.split(";"); const components = modt.split(";");
// Validate required components
if (components.length < 9) {
throw new Error("Invalid MODT format");
}
const parsedComponents = { const parsedComponents = {
edition: components[0], edition: components[0],
name: components[1], name: components[1],
@ -109,20 +129,23 @@ const ping = (host, port = 19132, cb, timeout = 5000) => {
} }
socket.on("message", (pongPacket) => { socket.on("message", (pongPacket) => {
if (!Buffer.isBuffer(pongPacket) || pongPacket.length === 0) {
handleError(new Error("Invalid packet received"));
return;
}
const id = pongPacket[0]; const id = pongPacket[0];
if (id !== UNCONNECTED_PONG) {
handleError(new Error(`Unexpected packet ID: 0x${id.toString(16)}`));
return;
}
switch (id) { try {
case 0x1c: { const modtObject = extractModt(pongPacket);
const modtObject = extractModt(pongPacket); closeSocket();
closeSocket(); cb(modtObject, null);
cb(modtObject, null); } catch (err) {
break; handleError(err);
}
default: {
handleError(new Error("Received unexpected packet"));
break;
}
} }
}); });

View File

@ -27,7 +27,7 @@ const varint = {
buf.writeUInt8(byte | 0x80, written++); buf.writeUInt8(byte | 0x80, written++);
} }
return buf.slice(0, written); return buf.subarray(0, written);
}, },
/** /**

View File

@ -1,33 +1,31 @@
/** /**
* @param port The server port. * @param port The server port (1-65535).
* @param timeout The read/write socket timeout. * @param timeout The read/write socket timeout in milliseconds.
*/ */
export type BedrockPingOptions = { export type BedrockPingOptions = {
port?: number; port?: number & { _brand: "Port" }; // 1-65535
timeout?: number; timeout?: number & { _brand: "Timeout" }; // > 0
}; };
export type BedrockPingResponse = { export type BedrockPingResponse = {
edition: string; edition: string;
name: string; name: string;
version: { version: {
protocolVersion: number; protocolVersion: number;
minecraftVersion: string; minecraftVersion: string;
}; };
players: { players: {
online: number; online: number;
max: number; max: number;
}; };
serverId: string; serverId: string;
mapName: string; mapName: string;
gameMode: string; gameMode: string;
}; };
/** /**
* Asynchronously ping Minecraft Bedrock server. * Asynchronously ping Minecraft Bedrock server.
* *
* The optional `options` argument can be an object with a `ping` (default is `19132`) or/and `timeout` (default is `5000`) property.
*
* @param host The Bedrock server address. * @param host The Bedrock server address.
* @param options The configuration for pinging Minecraft Bedrock server. * @param options The configuration for pinging Minecraft Bedrock server.
* *
@ -41,13 +39,23 @@ export type BedrockPingResponse = {
* The resulting output will resemble: * The resulting output will resemble:
* ```console * ```console
* { * {
* version: { name: 'Mineplex', protocol: '475' }, * edition: "MCPE",
* players: { max: '5207', online: '5206' }, * name: "Mineplex",
* description: ' New Costumes', * version: {
* gamemode: 'Survival' * protocolVersion: 475,
* minecraftVersion: "1.18.0"
* },
* players: {
* online: 5206,
* max: 5207
* },
* serverId: "12345678",
* mapName: "Lobby",
* gameMode: "Survival"
* } * }
* ``` * ```
* @see [source](https://github.com/minescope/mineping/blob/915edbec9c9ad811459458600af3531ec0836911/lib/bedrock.js#L204)
*/ */
export function pingBedrock(host: string, options?: BedrockPingOptions): Promise<BedrockPingResponse>; export function pingBedrock(
host: string,
options?: BedrockPingOptions
): Promise<BedrockPingResponse>;