From 0fe675385faff9b5760948d12d885278295a674a Mon Sep 17 00:00:00 2001 From: xzeldon Date: Sun, 22 Oct 2023 02:29:20 +0300 Subject: [PATCH] bedrock.js refactoring, remove bytebuffer dependency add parallel.js file to demonstrate parallel pinging of multiple Bedrock servers --- example/parallel.js | 10 +++ lib/bedrock.js | 178 ++++++++++++++++++++++++++++---------------- package.json | 5 +- yarn.lock | 15 ---- 4 files changed, 123 insertions(+), 85 deletions(-) create mode 100644 example/parallel.js delete mode 100644 yarn.lock diff --git a/example/parallel.js b/example/parallel.js new file mode 100644 index 0000000..3e2e684 --- /dev/null +++ b/example/parallel.js @@ -0,0 +1,10 @@ +import { pingBedrock } from '../index.js'; + +const [thehive, oasys, frizmine, breadix] = await Promise.allSettled([ + pingBedrock('geo.hivebedrock.network'), + pingBedrock('oasys-pe.com'), + pingBedrock('frizmine.ru'), + pingBedrock('play.breadixpe.ru') +]); + +console.dir({ thehive, oasys, frizmine, breadix }, { depth: 3 }); \ No newline at end of file diff --git a/lib/bedrock.js b/lib/bedrock.js index 5bf5744..bd5517d 100644 --- a/lib/bedrock.js +++ b/lib/bedrock.js @@ -9,82 +9,134 @@ 'use strict'; import dgram from 'dgram'; -import ByteBuffer from 'bytebuffer'; const START_TIME = new Date().getTime(); /** - * Decode Unconnected Ping - * @param {number} pingId - * @returns {import('bytebuffer')} - * @see https://wiki.vg/Raknet_Protocol#Unconnected_Ping + * Creates a buffer with the specified length. + * @param {number} length - The length of the buffer. + * @returns {Buffer} - The created buffer. */ -const UNCONNECTED_PING = (pingId) => { - // 0x01 - const bb = new ByteBuffer(); - bb.buffer[0] = 0x01; - bb.offset = 1; - return bb.writeLong(pingId).append('00ffff00fefefefefdfdfdfd12345678', 'hex').writeLong(0).flip().compact(); +const createBuffer = (length) => { + const buffer = Buffer.alloc(length); + buffer[0] = 0x01; + return buffer; }; /** - * Decode Unconnected Pong - * @param {import('bytebuffer')} buffer - * @see https://wiki.vg/Raknet_Protocol#Unconnected_Pong + * Writes a BigInt value to the buffer at the specified offset using big-endian byte order. + * @param {Buffer} buffer - The buffer to write to. + * @param {number} value - The BigInt value to write. + * @param {number} offset - The offset in the buffer to write the value. */ -const UNCONNECTED_PONG = (buffer) => { - // 0x1c - buffer.offset = 1; - const pingId = buffer.readLong(); - const serverId = buffer.readLong(); - const offset = buffer.offset += 16; - const nameLength = buffer.readShort(); - let advertiseStr; +const writeBigInt64BE = (buffer, value, offset) => { + buffer.writeBigInt64BE(BigInt(value), offset); +}; - try { - advertiseStr = buffer.readUTF8String(nameLength); - } catch (err) { - advertiseStr = buffer.readUTF8String(parseInt(err.message.substr(err.message.indexOf(',') + 2, 3))); - } +/** + * Copies the specified hex value to the buffer at the specified offset. + * @param {Buffer} buffer - The buffer to copy to. + * @param {string} hex - The hex value to copy. + * @param {number} offset - The offset in the buffer to copy the value. + */ +const copyHexToBuffer = (buffer, hex, offset) => { + Buffer.from(hex, 'hex').copy(buffer, offset); +}; - advertiseStr = advertiseStr.split(/;/g); - const gameId = advertiseStr[0]; - const description = advertiseStr[1]; - const protocolVersion = advertiseStr[2]; - const gameVersion = advertiseStr[3]; - const currentPlayers = advertiseStr[4]; - const maxPlayers = advertiseStr[5]; - const name = advertiseStr[7]; - const mode = advertiseStr[8]; +/** + * Reads a BigInt value from the buffer at the specified offset using big-endian byte order. + * @param {Buffer} buffer - The buffer to read from. + * @param {number} offset - The offset in the buffer to read the value. + * @returns {BigInt} - The read BigInt value. + */ +const readBigInt64BE = (buffer, offset) => { + return buffer.readBigInt64BE(offset); +}; +/** + * Reads a string from the buffer at the specified offset. + * @param {Buffer} buffer - The buffer to read from. + * @param {number} offset - The offset in the buffer to read the string. + * @returns {string} - The read string. + */ +const readStringFromBuffer = (buffer, offset) => { + const length = buffer.readUInt16BE(offset); + return buffer.toString('utf8', offset + 2, offset + 2 + length); +}; + +/** + * Parses the advertise string into an object with properties. + * @param {string} advertiseStr - The advertise string to parse. + * @returns {Object} - The parsed object with properties. + */ +const parseAdvertiseString = (advertiseStr) => { + const parts = advertiseStr.split(';'); return { - pingId, - advertiseStr, - serverId, - offset, - gameId, - description, - protocolVersion, - gameVersion, - currentPlayers, - maxPlayers, - name, - mode + gameId: parts[0], + description: parts[1], + protocolVersion: parts[2], + gameVersion: parts[3], + currentPlayers: parts[4], + maxPlayers: parts[5], + name: parts[7], + mode: parts[8] }; }; -function ping(host, port = 19132, cb, timeout = 5000) { +/** + * Creates an Unconnected Ping buffer. + * @param {number} pingId - The ping ID. + * @returns {Buffer} - The Unconnected Ping buffer. + * @see {@link https://wiki.vg/Raknet_Protocol#Unconnected_Ping} + */ +const UNCONNECTED_PING = (pingId) => { + const buffer = createBuffer(35); + writeBigInt64BE(buffer, pingId, 1); + copyHexToBuffer(buffer, '00ffff00fefefefefdfdfdfd12345678', 9); + writeBigInt64BE(buffer, 0, 25); + return buffer; +}; + +/** + * Decodes an Unconnected Pong buffer and returns the parsed data. + * @param {Buffer} buffer - The Unconnected Pong buffer. + * @returns {Object} - The parsed Unconnected Pong data. + * @see {@link https://wiki.vg/Raknet_Protocol#Unconnected_Pong} + */ +const UNCONNECTED_PONG = (buffer) => { + const pingId = readBigInt64BE(buffer, 1); + const serverId = readBigInt64BE(buffer, 9); + let offset = 25; + let advertiseStr; + + try { + advertiseStr = readStringFromBuffer(buffer, offset); + } catch (err) { + const length = parseInt(err.message.substr(err.message.indexOf(',') + 2, 3)); + advertiseStr = buffer.toString('utf8', offset, offset + length); + } + + const parsedAdvertiseStr = parseAdvertiseString(advertiseStr); + + return { pingId, advertiseStr, serverId, offset, ...parsedAdvertiseStr }; +}; + +/** + * Sends a ping request to the specified host and port. + * @param {string} host - The IP address or hostname of the server. + * @param {number} [port=19132] - The port number. + * @param {function} cb - The callback function to handle the response. + * @param {number} [timeout=5000] - The timeout duration in milliseconds. + */ +const ping = (host, port = 19132, cb, timeout = 5000) => { 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); const closeSocket = () => { - socket.close(); - clearTimeout(timeoutTask); + socket.close(); clearTimeout(timeoutTask); }; // Generic error handler @@ -103,19 +155,17 @@ function ping(host, port = 19132, cb, timeout = 5000) { try { const ping = UNCONNECTED_PING(new Date().getTime() - START_TIME); - socket.send(ping.buffer, 0, ping.buffer.length, port, host); + socket.send(ping, 0, ping.length, port, host); } catch (err) { handleError(err); } socket.on('message', (msg) => { - const buffer = new ByteBuffer().append(msg, 'hex').flip(); - const id = buffer.buffer[0]; + const id = msg[0]; switch (id) { - // https://wiki.vg/Raknet_Protocol#Unconnected_Ping case 0x1c: { - const pong = UNCONNECTED_PONG(buffer); + const pong = UNCONNECTED_PONG(msg); const clientData = { version: { name: pong.name, @@ -129,8 +179,6 @@ function ping(host, port = 19132, cb, timeout = 5000) { gamemode: pong.mode }; - // Close the socket and clear the timeout task - // This is a general cleanup for success conditions closeSocket(); cb(clientData, null); break; @@ -144,18 +192,16 @@ function ping(host, port = 19132, cb, timeout = 5000) { }); socket.on('error', (err) => handleError(err)); -} +}; /** * 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 {string} host The Bedrock server address. - * @param {import('../types/index.js').PingOptions} options The configuration for pinging Minecraft Bedrock server. + * @param {import('../types/lib/bedrock.js').PingOptions} options The configuration for pinging Minecraft Bedrock server. * @returns {Promise} */ -export function pingBedrock(host, options = {}) { +export const pingBedrock = (host, options = {}) => { if (!host) throw new Error('Host argument is not provided'); const { port = 19132, timeout = 5000 } = options; @@ -165,4 +211,4 @@ export function pingBedrock(host, options = {}) { err ? reject(err) : resolve(res); }, timeout); }); -} +}; diff --git a/package.json b/package.json index 4b1cfcd..69fac7f 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,5 @@ "engines": { "node": ">=14" }, - "license": "MIT", - "dependencies": { - "bytebuffer": "^5.0.1" - } + "license": "MIT" } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index a345df6..0000000 --- a/yarn.lock +++ /dev/null @@ -1,15 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -bytebuffer@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" - integrity sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0= - dependencies: - long "~3" - -long@~3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" - integrity sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=