diff --git a/lib/bedrock.js b/lib/bedrock.js index 17593f8..f9aa1b3 100644 --- a/lib/bedrock.js +++ b/lib/bedrock.js @@ -1,12 +1,12 @@ /** * Implementation of the RakNet ping/pong protocol. - * @see https://wiki.vg/Raknet_Protocol + * @see https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Raknet_Protocol */ -'use strict'; +"use strict"; -import dgram from 'node:dgram'; -import crypto from 'node:crypto' +import dgram from "node:dgram"; +import crypto from "node:crypto"; const MAGIC = "00ffff00fefefefefdfdfdfd12345678"; const START_TIME = new Date().getTime(); @@ -15,47 +15,51 @@ const START_TIME = new Date().getTime(); * Creates an Unconnected Ping packet. * @param {number} pingId * @returns {Buffer} - * @see {@link https://wiki.vg/Raknet_Protocol#Unconnected_Ping} + * @see {@link https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Raknet_Protocol#Unconnected_Ping} */ const createUnconnectedPingFrame = (timestamp) => { - const buffer = Buffer.alloc(33); - buffer.writeUInt8(0x01, 0); // Packet ID - buffer.writeBigInt64LE(BigInt(timestamp), 1); // Timestamp - Buffer.from(MAGIC, "hex").copy(buffer, 9); // OFFLINE_MESSAGE_DATA_ID (Magic) - Buffer.from(crypto.randomBytes(8)).copy(buffer, 25); // Client GUID - return buffer; + const buffer = Buffer.alloc(33); + buffer.writeUInt8(0x01, 0); // Packet ID + buffer.writeBigInt64LE(BigInt(timestamp), 1); // Timestamp + Buffer.from(MAGIC, "hex").copy(buffer, 9); // OFFLINE_MESSAGE_DATA_ID (Magic) + Buffer.from(crypto.randomBytes(8)).copy(buffer, 25); // Client GUID + return buffer; }; /** * Extract Modt from Unconnected Pong Packet and convert to an object * @param {Buffer} unconnectedPongPacket * @returns {Object} - * @see {@link https://wiki.vg/Raknet_Protocol#Unconnected_Pong} + * @see {@link https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Raknet_Protocol#Unconnected_Pong} */ const extractModt = (unconnectedPongPacket) => { - // Skip everything to Modt - const offset = 33; - const length = unconnectedPongPacket.readUInt16BE(offset); - let modt = unconnectedPongPacket.toString("utf-8", offset + 2, offset + 2 + length); + // Skip everything to Modt + const offset = 33; + const length = unconnectedPongPacket.readUInt16BE(offset); + let modt = unconnectedPongPacket.toString( + "utf-8", + offset + 2, + offset + 2 + length + ); - const components = modt.split(';'); - const parsedComponents = { - edition: components[0], - name: components[1], - version: { - protocolVersion: Number(components[2]), - minecraftVersion: components[3], - }, - players: { - online: Number(components[4]), - max: Number(components[5]) - }, - serverId: components[6], - mapName: components[7], - gameMode: components[8] - }; + const components = modt.split(";"); + const parsedComponents = { + edition: components[0], + name: components[1], + version: { + protocolVersion: Number(components[2]), + minecraftVersion: components[3], + }, + players: { + online: Number(components[4]), + max: Number(components[5]), + }, + serverId: components[6], + mapName: components[7], + gameMode: components[8], + }; - return parsedComponents; + return parsedComponents; }; /** @@ -66,63 +70,63 @@ const extractModt = (unconnectedPongPacket) => { * @param {number} [timeout=5000] - The timeout duration in milliseconds. */ const ping = (host, port = 19132, cb, timeout = 5000) => { - const socket = dgram.createSocket('udp4'); + const socket = dgram.createSocket("udp4"); - // Set manual timeout interval. - // This ensures the connection will NEVER hang regardless of internal state - const timeoutTask = setTimeout(() => { - socket.emit('error', new Error('Socket timeout')); - }, timeout); + // Set manual timeout interval. + // This ensures the connection will NEVER hang regardless of internal state + const timeoutTask = setTimeout(() => { + socket.emit("error", new Error("Socket timeout")); + }, timeout); - const closeSocket = () => { - socket.close(); - clearTimeout(timeoutTask); - }; + const closeSocket = () => { + socket.close(); + clearTimeout(timeoutTask); + }; - // Generic error handler - // This protects multiple error callbacks given the complex socket state - // This is mostly dangerous since it can swallow errors - let didFireError = false; + // Generic error handler + // This protects multiple error callbacks given the complex socket state + // This is mostly dangerous since it can swallow errors + let didFireError = false; - /** - * Handle any error that occurs during the ping process. - * @param {Error} err The error that occurred. - */ - const handleError = (err) => { - closeSocket(); + /** + * Handle any error that occurs during the ping process. + * @param {Error} err The error that occurred. + */ + const handleError = (err) => { + closeSocket(); - if (!didFireError) { - didFireError = true; - cb(null, err); - } - }; + if (!didFireError) { + didFireError = true; + cb(null, err); + } + }; - try { - const ping = createUnconnectedPingFrame(new Date().getTime() - START_TIME); - socket.send(ping, 0, ping.length, port, host); - } catch (err) { - handleError(err); - } + try { + const ping = createUnconnectedPingFrame(new Date().getTime() - START_TIME); + socket.send(ping, 0, ping.length, port, host); + } catch (err) { + handleError(err); + } - socket.on('message', (pongPacket) => { - const id = pongPacket[0]; + socket.on("message", (pongPacket) => { + const id = pongPacket[0]; - switch (id) { - case 0x1c: { - const modtObject = extractModt(pongPacket); - closeSocket(); - cb(modtObject, null); - break; - } + switch (id) { + case 0x1c: { + const modtObject = extractModt(pongPacket); + closeSocket(); + cb(modtObject, null); + break; + } - default: { - handleError(new Error('Received unexpected packet')); - break; - } - } - }); + default: { + handleError(new Error("Received unexpected packet")); + break; + } + } + }); - socket.on('error', handleError); + socket.on("error", handleError); }; /** @@ -133,13 +137,18 @@ const ping = (host, port = 19132, cb, timeout = 5000) => { * @returns {Promise} */ export const pingBedrock = (host, options = {}) => { - if (!host) throw new Error('Host argument is not provided'); + if (!host) throw new Error("Host argument is not provided"); - const { port = 19132, timeout = 5000 } = options; + const { port = 19132, timeout = 5000 } = options; - return new Promise((resolve, reject) => { - ping(host, port, (res, err) => { - err ? reject(err) : resolve(res); - }, timeout); - }); + return new Promise((resolve, reject) => { + ping( + host, + port, + (res, err) => { + err ? reject(err) : resolve(res); + }, + timeout + ); + }); }; diff --git a/lib/java.js b/lib/java.js index 3f0b7b3..f6ecdb1 100644 --- a/lib/java.js +++ b/lib/java.js @@ -1,12 +1,12 @@ /** * Implementation of the Java Minecraft ping protocol. - * @see https://wiki.vg/Server_List_Ping + * @see https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Server_List_Ping */ -'use strict'; +"use strict"; -import net from 'node:net'; -import varint from './varint.js'; +import net from "node:net"; +import varint from "./varint.js"; /** * Ping a Minecraft Java server. @@ -17,102 +17,108 @@ import varint from './varint.js'; * @param {number} [protocolVersion=-1] The protocol version of the Java client. */ function ping(host, port = 25565, cb, timeout = 5000, protocolVersion = -1) { - const socket = net.createConnection(({ host, port })); + const socket = net.createConnection({ host, port }); - // Set manual timeout interval. - // This ensures the connection will NEVER hang regardless of internal state - const timeoutTask = setTimeout(() => { - socket.emit('error', new Error('Socket timeout')); - }, timeout); + // Set manual timeout interval. + // This ensures the connection will NEVER hang regardless of internal state + const timeoutTask = setTimeout(() => { + socket.emit("error", new Error("Socket timeout")); + }, timeout); - const closeSocket = () => { - socket.destroy(); - clearTimeout(timeoutTask); - }; + const closeSocket = () => { + socket.destroy(); + clearTimeout(timeoutTask); + }; - // Generic error handler - // This protects multiple error callbacks given the complex socket state - // This is mostly dangerous since it can swallow errors - let didFireError = false; + // Generic error handler + // This protects multiple error callbacks given the complex socket state + // This is mostly dangerous since it can swallow errors + let didFireError = false; - /** - * Handle any error that occurs during the ping process. - * @param {Error} err The error that occurred. - */ - const handleError = (err) => { - closeSocket(); + /** + * Handle any error that occurs during the ping process. + * @param {Error} err The error that occurred. + */ + const handleError = (err) => { + closeSocket(); - if (!didFireError) { - didFireError = true; - cb(null, err); - } - }; + if (!didFireError) { + didFireError = true; + cb(null, err); + } + }; - // #setNoDelay instantly flushes data during read/writes - // This prevents the runtime from delaying the write at all - socket.setNoDelay(true); + // #setNoDelay instantly flushes data during read/writes + // This prevents the runtime from delaying the write at all + socket.setNoDelay(true); - socket.on('connect', () => { - const handshake = varint.concat([ - varint.encodeInt(0), - varint.encodeInt(protocolVersion), - varint.encodeInt(host.length), - varint.encodeString(host), - varint.encodeUShort(port), - varint.encodeInt(1) - ]); + socket.on("connect", () => { + const handshake = varint.concat([ + varint.encodeInt(0), + varint.encodeInt(protocolVersion), + varint.encodeInt(host.length), + varint.encodeString(host), + varint.encodeUShort(port), + varint.encodeInt(1), + ]); - socket.write(handshake); + socket.write(handshake); - const request = varint.concat([ - varint.encodeInt(0) - ]); + const request = varint.concat([varint.encodeInt(0)]); - socket.write(request); - }); + socket.write(request); + }); - let incomingBuffer = Buffer.alloc(0); + let incomingBuffer = Buffer.alloc(0); - socket.on('data', (data) => { - incomingBuffer = Buffer.concat([incomingBuffer, data]); + socket.on("data", (data) => { + incomingBuffer = Buffer.concat([incomingBuffer, data]); - // Wait until incomingBuffer is at least 5 bytes long to ensure it has captured the first VarInt value - // This value is used to determine the full read length of the response - // "VarInts are never longer than 5 bytes" - // https://wiki.vg/Data_types#VarInt_and_VarLong - if (incomingBuffer.length < 5) { - return; - } + // Wait until incomingBuffer is at least 5 bytes long to ensure it has captured the first VarInt value + // This value is used to determine the full read length of the response + // "VarInts are never longer than 5 bytes" + // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Data_types#VarInt_and_VarLong + if (incomingBuffer.length < 5) { + return; + } - let offset = 0; - const packetLength = varint.decodeInt(incomingBuffer, offset); + let offset = 0; + const packetLength = varint.decodeInt(incomingBuffer, offset); - // Ensure incomingBuffer contains the full response - if (incomingBuffer.length - offset < packetLength) { - return; - } + // Ensure incomingBuffer contains the full response + if (incomingBuffer.length - offset < packetLength) { + return; + } - const packetId = varint.decodeInt(incomingBuffer, varint.decodeLength(packetLength)); + const packetId = varint.decodeInt( + incomingBuffer, + varint.decodeLength(packetLength) + ); - if (packetId === 0) { - const data = incomingBuffer.subarray(varint.decodeLength(packetLength) + varint.decodeLength(packetId)); - const responseLength = varint.decodeInt(data, 0); - const response = data.subarray(varint.decodeLength(responseLength), varint.decodeLength(responseLength) + responseLength); + if (packetId === 0) { + const data = incomingBuffer.subarray( + varint.decodeLength(packetLength) + varint.decodeLength(packetId) + ); + const responseLength = varint.decodeInt(data, 0); + const response = data.subarray( + varint.decodeLength(responseLength), + varint.decodeLength(responseLength) + responseLength + ); - try { - const message = JSON.parse(response); + try { + const message = JSON.parse(response); - closeSocket(); - cb(message, null); - } catch (err) { - handleError(err); - } - } else { - handleError(new Error('Received unexpected packet')); - } - }); + closeSocket(); + cb(message, null); + } catch (err) { + handleError(err); + } + } else { + handleError(new Error("Received unexpected packet")); + } + }); - socket.on('error', handleError); + socket.on("error", handleError); } /** @@ -123,13 +129,19 @@ function ping(host, port = 25565, cb, timeout = 5000, protocolVersion = -1) { * @returns {Promise} */ export function pingJava(host, options = {}) { - if (!host) throw new Error('Host argument is not provided'); + if (!host) throw new Error("Host argument is not provided"); - const { port = 25565, timeout = 5000, protocolVersion = -1 } = options; + const { port = 25565, timeout = 5000, protocolVersion = -1 } = options; - return new Promise((resolve, reject) => { - ping(host, port, (res, err) => { - err ? reject(err) : resolve(res); - }, timeout, protocolVersion); - }); + return new Promise((resolve, reject) => { + ping( + host, + port, + (res, err) => { + err ? reject(err) : resolve(res); + }, + timeout, + protocolVersion + ); + }); } diff --git a/lib/varint.js b/lib/varint.js index a72956b..2fba172 100644 --- a/lib/varint.js +++ b/lib/varint.js @@ -1,127 +1,131 @@ -// https://wiki.vg/Data_types +// https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol#Data_types /** * A utility object for encoding and decoding varints. */ const varint = { - /** - * Encodes an integer value into a varint byte buffer. - * @param {number} val - The integer value to encode. - * @returns {Buffer} - */ - encodeInt: (val) => { - // "constInts are never longer than 5 bytes" - // https://wiki.vg/Data_types#constInt_and_constLong - const buf = Buffer.alloc(5); - let written = 0; + /** + * Encodes an integer value into a varint byte buffer. + * @param {number} val - The integer value to encode. + * @returns {Buffer} + */ + encodeInt: (val) => { + // "VarInts are never longer than 5 bytes" + // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Data_types#VarInt_and_VarLong + const buf = Buffer.alloc(5); + let written = 0; - while (true) { - const byte = val & 0x7F; - val >>>= 7; + while (true) { + const byte = val & 0x7f; + val >>>= 7; - if (val === 0) { - buf.writeUInt8(byte, written++); - break; - } + if (val === 0) { + buf.writeUInt8(byte, written++); + break; + } - buf.writeUInt8(byte | 0x80, written++); - } + buf.writeUInt8(byte | 0x80, written++); + } - return buf.slice(0, written); - }, + return buf.slice(0, written); + }, - /** - * Encodes a string value into a UTF-8 byte buffer. - * @param {string} val - The string value to encode. - * @returns {Buffer} - */ - encodeString: (val) => { - return Buffer.from(val, 'utf-8'); - }, + /** + * Encodes a string value into a UTF-8 byte buffer. + * @param {string} val - The string value to encode. + * @returns {Buffer} + */ + encodeString: (val) => { + return Buffer.from(val, "utf-8"); + }, - /** - * Encodes an unsigned short value into a byte buffer. - * @param {number} val - The unsigned short value to encode. - * @returns {Buffer} - */ - encodeUShort: (val) => { - return Buffer.from([val >> 8, val & 0xFF]); - }, + /** + * Encodes an unsigned short value into a byte buffer. + * @param {number} val - The unsigned short value to encode. + * @returns {Buffer} + */ + encodeUShort: (val) => { + return Buffer.from([val >> 8, val & 0xff]); + }, - /** - * Concatenates multiple byte buffers into a single byte buffer. - * @param {Buffer[]} chunks - An array of byte buffers to concatenate. - * @returns {Buffer} - */ - concat: (chunks) => { - let length = 0; + /** + * Concatenates multiple byte buffers into a single byte buffer. + * @param {Buffer[]} chunks - An array of byte buffers to concatenate. + * @returns {Buffer} + */ + concat: (chunks) => { + let length = 0; - for (const chunk of chunks) { - length += chunk.length; - } + for (const chunk of chunks) { + length += chunk.length; + } - const buffer = [ - varint.encodeInt(length), - ...chunks - ]; + const buffer = [varint.encodeInt(length), ...chunks]; - return Buffer.concat(buffer); - }, + return Buffer.concat(buffer); + }, - /** - * Decodes a varint integer value from a byte buffer. - * @param {Buffer} buffer - The byte buffer to decode from. - * @param {number} offset - The offset in the buffer to start decoding from. - * @returns {number} - */ - decodeInt: (buffer, offset) => { - let val = 0; - let count = 0; + /** + * Decodes a varint integer value from a byte buffer. + * @param {Buffer} buffer - The byte buffer to decode from. + * @param {number} offset - The offset in the buffer to start decoding from. + * @returns {number} + */ + decodeInt: (buffer, offset) => { + let val = 0; + let count = 0; - while (true) { - const byte = buffer.readUInt8(offset++); + while (true) { + const byte = buffer.readUInt8(offset++); - val |= (byte & 0x7F) << count++ * 7; + val |= (byte & 0x7f) << (count++ * 7); - if ((byte & 0x80) !== 0x80) { - break; - } - } + if ((byte & 0x80) !== 0x80) { + break; + } + } - return val; - }, + return val; + }, - /** - * Calculates the number of bytes required to decode a varint integer value. - * @param {number} val - The varint integer value. - * @returns {5 | 7 | 8 | 1 | 2 | 3 | 4 | 6 | 9 | 10} - */ - decodeLength: (val) => { - // Constants representing the powers of 2 used for comparison - const N1 = Math.pow(2, 7); - const N2 = Math.pow(2, 14); - const N3 = Math.pow(2, 21); - const N4 = Math.pow(2, 28); - const N5 = Math.pow(2, 35); - const N6 = Math.pow(2, 42); - const N7 = Math.pow(2, 49); - const N8 = Math.pow(2, 56); - const N9 = Math.pow(2, 63); + /** + * Calculates the number of bytes required to decode a varint integer value. + * @param {number} val - The varint integer value. + * @returns {5 | 7 | 8 | 1 | 2 | 3 | 4 | 6 | 9 | 10} + */ + decodeLength: (val) => { + // Constants representing the powers of 2 used for comparison + const N1 = Math.pow(2, 7); + const N2 = Math.pow(2, 14); + const N3 = Math.pow(2, 21); + const N4 = Math.pow(2, 28); + const N5 = Math.pow(2, 35); + const N6 = Math.pow(2, 42); + const N7 = Math.pow(2, 49); + const N8 = Math.pow(2, 56); + const N9 = Math.pow(2, 63); - // Return the number of bytes required based on the value - return ( - val < N1 ? 1 - : val < N2 ? 2 - : val < N3 ? 3 - : val < N4 ? 4 - : val < N5 ? 5 - : val < N6 ? 6 - : val < N7 ? 7 - : val < N8 ? 8 - : val < N9 ? 9 - : 10 - ); - } + // Return the number of bytes required based on the value + return val < N1 + ? 1 + : val < N2 + ? 2 + : val < N3 + ? 3 + : val < N4 + ? 4 + : val < N5 + ? 5 + : val < N6 + ? 6 + : val < N7 + ? 7 + : val < N8 + ? 8 + : val < N9 + ? 9 + : 10; + }, }; export default varint; diff --git a/types/lib/java.d.ts b/types/lib/java.d.ts index 8084f1a..f94d0c6 100644 --- a/types/lib/java.d.ts +++ b/types/lib/java.d.ts @@ -4,49 +4,49 @@ * @param protocolVersion The protocol version. */ export type JavaPingOptions = { - port?: number | undefined, - timeout?: number | undefined, - protocolVersion?: number | undefined; + port?: number | undefined; + timeout?: number | undefined; + protocolVersion?: number | undefined; }; /** * JSON format chat component used for description field. - * @see https://wiki.vg/Chat + * @see https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Chat */ export type ChatComponent = { - text: string; - bold?: boolean; - italic?: boolean; - underlined?: boolean; - strikethrough?: boolean; - obfuscated?: boolean; - color?: string; - extra?: ChatComponent[]; + text: string; + bold?: boolean; + italic?: boolean; + underlined?: boolean; + strikethrough?: boolean; + obfuscated?: boolean; + color?: string; + extra?: ChatComponent[]; }; export type SampleProp = { - name: string, - id: string; + name: string; + id: string; }; /** * `JSON Response` field of Response packet. - * @see https://wiki.vg/Server_List_Ping#Response + * @see https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Server_List_Ping#Status_Response */ export type JavaPingResponse = { - version: { - name: string; - protocol: number; - }; - players: { - max: number; - online: number; - sample?: SampleProp[]; - }; - description: string | ChatComponent; - favicon?: string; - enforcesSecureChat?: boolean; - previewsChat?: boolean; + version: { + name: string; + protocol: number; + }; + players: { + max: number; + online: number; + sample?: SampleProp[]; + }; + description: string | ChatComponent; + favicon?: string; + enforcesSecureChat?: boolean; + previewsChat?: boolean; }; /** @@ -76,5 +76,7 @@ export type JavaPingResponse = { * ``` * @see [source](https://github.com/minescope/mineping/blob/915edbec9c9ad811459458600af3531ec0836911/lib/java.js#L117) */ -export function pingJava(host: string, options?: JavaPingOptions): Promise; - +export function pingJava( + host: string, + options?: JavaPingOptions +): Promise;