From 910184bf5f364611f731239b10bee9defac92458 Mon Sep 17 00:00:00 2001 From: xzeldon Date: Sun, 22 Oct 2023 21:32:00 +0300 Subject: [PATCH] refactoring and comments fix(cli.js): refactor help and error handling logic for better readability and maintainability feat(cli.js): add support for custom port and timeout options fix(parallel.js): update list of hosts to ping fix(bedrock.js): add comments and improve error handling in ping function fix(java.js): add comments and improve error handling in ping function fix(varint.js): add comments to functions and improve readability fix(index.d.ts): export all functions from java.js and bedrock.js fix(lib/bedrock.d.ts): update source link fix(lib/java.d.ts): update source link --- example/cli.js | 81 ++++++++++++++++++++++++++++-------------- example/parallel.js | 23 ++++++++---- lib/bedrock.js | 15 +++++--- lib/java.js | 25 ++++++++----- lib/varint.js | 55 +++++++++++++++++++++++----- types/index.d.ts | 4 +-- types/lib/bedrock.d.ts | 2 +- types/lib/java.d.ts | 2 +- 8 files changed, 148 insertions(+), 59 deletions(-) diff --git a/example/cli.js b/example/cli.js index 7b9afaf..98725f5 100644 --- a/example/cli.js +++ b/example/cli.js @@ -1,19 +1,15 @@ +/** + * Usage examples: + * - Java (with custom timeout): node cli.js -j --host="mc.hypixel.net" --timeout 1000 + * - Bedrock: node cli.js -b --host="play.timecrack.net" + */ + import { pingBedrock, pingJava } from '../index.js'; const args = getArgs(); -if (args.help || args.h || Object.keys(args).length === 0) { - console.log(`node cli.js [..] - A simple to use, efficient, and full-featured Minecraft server info parser! - - USAGE: - node cli.js [OPTIONS] --host --port --timeout - - OPTIONS: - -j Use for Minecraft Java Edition - -b Use for Minecraft Bedrock Edition - P.S. Don't use them at the same time!`); - +if (shouldShowHelp(args)) { + printHelp(); process.exit(1); } @@ -24,6 +20,42 @@ if (!args.host) { // easter egg <3 if (args.j && args.b) { + printInterestingFacts(); + process.exit(0); +} + +const port = args.port || getDefaultPort(args); +const timeout = args.timeout || 500; + +if (args.j) { + await pingJavaServer(args.host, port, timeout) + .catch(err => console.error(`ERROR: ${err.message}`)); +} else if (args.b) { + await pingBedrockServer(args.host, port, timeout) + .catch(err => console.error(`ERROR: ${err.message}`)); +} else { + console.error('ERROR: Unsupported flag passed. Use -h or --help.'); +} + +function shouldShowHelp(args) { + return args.help || args.h || Object.keys(args).length === 0; +} + +function printHelp() { + console.log(`node cli.js [..] + A simple to use, efficient, and full-featured Minecraft server info parser! + + USAGE: + node cli.js [OPTIONS] --host --port --timeout + + OPTIONS: + -j Use for Minecraft Java Edition + -b Use for Minecraft Bedrock Edition + + P.S. Don't use them at the same time!`); +} + +function printInterestingFacts() { console.log(`Some interesting facts about MOTDs on bedrock: - so far they seem to exclusively use legacy color codes - the random style has a special impl for periods, they turn into animated @@ -31,25 +63,20 @@ if (args.j && args.b) { - motd_2 is ignored? client displays "motd_1 - v{version}", where the appended version text is considered part of motd_1 for color code processing - motd_2 seems to mainly be used to return the server software in use (e.g. PocketMine-MP)`); - process.exit(0); } -if (args.j) { - const data = await pingJava(args.host, { - port: args.port || 25565, - timeout: args.timeout || 500 - }); +function getDefaultPort(args) { + return args.j ? 25565 : 19132; +} - console.log(`host: ${args.host}\nprotocol: ${data.version.protocol}\nonline: ${data.players.online}`); -} else if (args.b) { - const data = await pingBedrock(args.host, { - port: args.port || 19132, - timeout: args.timeout || 500 - }); +async function pingJavaServer(host, port, timeout) { + const data = await pingJava(host, { port, timeout }); + console.log(`host: ${host}\nprotocol: ${data.version?.protocol}\nonline: ${data.players?.online}`); +} - console.log(`host: ${args.host}\nprotocol: ${data.version.protocol}\nonline: ${data.players.online}`); -} else { - console.error('ERROR: Unsupported flag passed. Use -h or --help.'); +async function pingBedrockServer(host, port, timeout) { + const data = await pingBedrock(host, { port, timeout }); + console.log(`host: ${host}\nprotocol: ${data.version.protocol}\nonline: ${data.players.online}`); } // parsing command line arguments diff --git a/example/parallel.js b/example/parallel.js index 3e2e684..5bedfda 100644 --- a/example/parallel.js +++ b/example/parallel.js @@ -1,10 +1,19 @@ 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') -]); +const hosts = [ + 'play.timecrack.net', + 'geo.hivebedrock.network', + 'oasys-pe.com', + 'play.galaxite.net', +]; -console.dir({ thehive, oasys, frizmine, breadix }, { depth: 3 }); \ No newline at end of file +const pingPromises = hosts.map(host => pingBedrock(host)); +const results = await Promise.allSettled(pingPromises); + +for (let result of results) { + if (result.status === 'rejected') { + console.error(result.reason); + } + + console.log(result.value); +} \ No newline at end of file diff --git a/lib/bedrock.js b/lib/bedrock.js index bd5517d..204a6ed 100644 --- a/lib/bedrock.js +++ b/lib/bedrock.js @@ -131,12 +131,15 @@ const UNCONNECTED_PONG = (buffer) => { 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 @@ -144,6 +147,10 @@ const ping = (host, port = 19132, cb, timeout = 5000) => { // 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(); @@ -191,15 +198,15 @@ const ping = (host, port = 19132, cb, timeout = 5000) => { } }); - socket.on('error', (err) => handleError(err)); + socket.on('error', handleError); }; /** * 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/lib/bedrock.js').PingOptions} options The configuration for pinging Minecraft Bedrock server. - * @returns {Promise} + * @param {import('../types/index.js').PingOptions} options The configuration for pinging Minecraft Bedrock server. + * @returns {Promise} */ export const pingBedrock = (host, options = {}) => { if (!host) throw new Error('Host argument is not provided'); diff --git a/lib/java.js b/lib/java.js index fe925e0..fa339b9 100644 --- a/lib/java.js +++ b/lib/java.js @@ -10,6 +10,13 @@ import varint from './varint.js'; const PROTOCOL_VERSION = 0; +/** + * Ping a Minecraft Java server. + * @param {string} host The host of the Java server. + * @param {number} [port=25565] The port of the Java server. + * @param {function} cb The callback function to handle the ping response. + * @param {number} [timeout=5000] The timeout duration in milliseconds. + */ function ping(host, port = 25565, cb, timeout = 5000) { const socket = net.createConnection(({ host, port })); @@ -29,6 +36,10 @@ function ping(host, port = 25565, cb, timeout = 5000) { // 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(); @@ -65,6 +76,7 @@ function ping(host, port = 25565, cb, timeout = 5000) { 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" @@ -84,16 +96,15 @@ function ping(host, port = 25565, cb, timeout = 5000) { const packetId = varint.decodeInt(incomingBuffer, varint.decodeLength(packetLength)); if (packetId === 0) { - const data = incomingBuffer.slice(varint.decodeLength(packetLength) + varint.decodeLength(packetId)); + const data = incomingBuffer.subarray(varint.decodeLength(packetLength) + varint.decodeLength(packetId)); const responseLength = varint.decodeInt(data, 0); - const response = data.slice(varint.decodeLength(responseLength), varint.decodeLength(responseLength) + responseLength); + const response = data.subarray(varint.decodeLength(responseLength), varint.decodeLength(responseLength) + responseLength); try { const message = JSON.parse(response); - cb(null, message); - // Close the socket and clear the timeout task closeSocket(); + cb(message, null); } catch (err) { handleError(err); } @@ -107,12 +118,10 @@ function ping(host, port = 25565, cb, timeout = 5000) { /** * Asynchronously ping Minecraft Java server. - * * The optional `options` argument can be an object with a `ping` (default is `25565`) or/and `timeout` (default is `5000`) property. - * * @param {string} host The Java server address. * @param {import('../types/index.js').PingOptions} options The configuration for pinging Minecraft Java server. - * @returns {Promise} + * @returns {Promise} */ export function pingJava(host, options = {}) { if (!host) throw new Error('Host argument is not provided'); @@ -120,7 +129,7 @@ export function pingJava(host, options = {}) { const { port = 25565, timeout = 5000 } = options; return new Promise((resolve, reject) => { - ping(host, port, (err, res) => { + ping(host, port, (res, err) => { err ? reject(err) : resolve(res); }, timeout); }); diff --git a/lib/varint.js b/lib/varint.js index d0b3377..a72956b 100644 --- a/lib/varint.js +++ b/lib/varint.js @@ -1,6 +1,14 @@ // https://wiki.vg/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 @@ -8,26 +16,43 @@ const varint = { let written = 0; while (true) { - if ((val & 0xFFFFFF80) === 0) { - buf.writeUInt8(val, written++); + const byte = val & 0x7F; + val >>>= 7; + + if (val === 0) { + buf.writeUInt8(byte, written++); break; - } else { - buf.writeUInt8(val & 0x7F | 0x80, written++); - val >>>= 7; } + + buf.writeUInt8(byte | 0x80, 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 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; @@ -43,16 +68,22 @@ const varint = { 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; while (true) { - const b = buffer.readUInt8(offset++); + const byte = buffer.readUInt8(offset++); - val |= (b & 0x7F) << count++ * 7; + val |= (byte & 0x7F) << count++ * 7; - if ((b & 0x80) != 128) { + if ((byte & 0x80) !== 0x80) { break; } } @@ -60,8 +91,13 @@ const varint = { return val; }, - // The number of bytes that the last .decodeInt() call had to use to decode. + /** + * 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); @@ -72,6 +108,7 @@ const varint = { 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 diff --git a/types/index.d.ts b/types/index.d.ts index 3ddd087..f0f7bc4 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,2 +1,2 @@ -export { pingJava } from "./lib/java.js"; -export { pingBedrock } from "./lib/bedrock.js"; \ No newline at end of file +export * from "./lib/java.js"; +export * from "./lib/bedrock.js"; \ No newline at end of file diff --git a/types/lib/bedrock.d.ts b/types/lib/bedrock.d.ts index 914cc7d..923cf14 100644 --- a/types/lib/bedrock.d.ts +++ b/types/lib/bedrock.d.ts @@ -43,7 +43,7 @@ export type BedrockPingResponse = { * gamemode: 'Survival' * } * ``` - * @see [source](https://github.com/minescope/mineping/blob/24a48802300f988d3ae520edbeb4f3e12820dcc9/lib/java.js#L117) + * @see [source](https://github.com/minescope/mineping/blob/915edbec9c9ad811459458600af3531ec0836911/lib/bedrock.js#L204) */ export function pingBedrock(host: string, options?: PingOptions): Promise; diff --git a/types/lib/java.d.ts b/types/lib/java.d.ts index f53ce54..4b66d6d 100644 --- a/types/lib/java.d.ts +++ b/types/lib/java.d.ts @@ -65,7 +65,7 @@ export type JavaPingResponse = { * favicon: 'data:image/png;base64,iVBORw0KGg... } * ``` - * @see [source](https://github.com/minescope/mineping/blob/8c84925ef7f5c420a7ef52740cba027491e82934/lib/bedrock.js#L158) + * @see [source](https://github.com/minescope/mineping/blob/915edbec9c9ad811459458600af3531ec0836911/lib/java.js#L117) */ export function pingJava(host: string, options?: PingOptions): Promise;