28 Commits

Author SHA1 Message Date
d8d4a9a467 chore: bump version to 1.6.1 2025-02-07 08:57:18 +03:00
d90a916fa5 docs: update cli app example 2025-02-07 08:56:39 +03:00
0959403b1b refactor: improve Bedrock error handling and validation 2025-02-07 08:48:04 +03:00
c71236f223 perf: optimize varint decoding
docs: update type definitions
2025-02-07 08:35:21 +03:00
0b5c5e2938 chore: bump version to 1.6.0 2025-02-07 08:02:26 +03:00
73a2fffe8b feat: add virtualHost option (#6) 2025-02-07 08:01:12 +03:00
9469564736 docs: update references to wiki.vg
wiki.vg has been shut down and is in the process of being merged into minecraft.wiki
2024-12-28 22:47:11 +03:00
838ffc497a chore: bump version to 1.5.0 2024-10-09 22:00:11 +03:00
502029869a style: use "node:" prefix for imports 2024-10-09 21:59:16 +03:00
e3e7e293ed fix: use vanilla ping format 2024-10-09 21:57:37 +03:00
88ad92e59d chore: add example for single server ping 2024-10-09 21:55:01 +03:00
009f542c55 chore: bump version to 1.4.1 2024-03-31 02:02:08 +03:00
0b0bed4e71 docs: some clarifications in createUnconnectedPingFrame 2024-03-31 02:01:46 +03:00
fa4c34d896 chore: bump version to 1.4.0 2024-03-31 01:56:43 +03:00
296294ca96 refactor!: changes in bedrock protocol code
BREAKING CHANGE: new bedrock ping response format
2024-03-31 01:56:03 +03:00
9d25aaf4ea chore: bump version to 1.3.0 2024-03-30 16:42:44 +03:00
c735604c38 fix: #5 add gameVersion field for Bedrock
Add gameVersion field in BedrockPingResponse

Closes: #5
2024-03-30 16:41:42 +03:00
afdaa9eb3e chore(package.json): bump version to 1.2.2 2023-12-12 00:44:41 +03:00
435899309f Merge pull request #4 from sya-ri/fix-types/optional
fix(types): Change options to optional
2023-12-12 00:44:20 +03:00
13e6b8c6ff fix(types): Change options to optional 2023-12-09 22:38:37 +09:00
d7256eabe7 chore(package.json): bump version to 1.2.1 2023-12-09 12:58:53 +03:00
afa2c3025f fix(bedrock.js): resolve UNCONNECTED_PING formation issue
- Simplify UNCONNECTED_PING function
- Address an issue where certain servers, particularly those based on Pocketmine, were unresponsive to Unconnected Ping requests
2023-12-09 12:57:43 +03:00
6c297d0b8c chore(package.json): bump version to 1.2.0 2023-12-08 15:22:53 +03:00
283e9b32c6 Merge pull request #3 from inotflying/patch-1
feat(pingJava): The ability to set a specific protocol version in the parameters. According to https://wiki.vg/Server_List_Ping#:~:text=0x00-,Protocol%20Version,-VarInt
2023-12-08 15:22:07 +03:00
354fa212a6 fix(javaPing): default protocolVersion value 2023-12-08 16:16:30 +04:00
d9bf4cfb3f feat(pingJava): add protocolVersion
feat(types): `PingOptions` for Java

fix(types): type names

fix(types): type names
2023-12-08 16:04:35 +04:00
9dace3748b docs(README.md): improve clarity and grammar in the project description 2023-10-22 22:49:53 +03:00
0aa73655b1 fix(parallel.js): add break statement to stop iterating over results if a promise is rejected 2023-10-22 21:38:34 +03:00
11 changed files with 572 additions and 500 deletions

View File

@ -1,6 +1,6 @@
# mineping # mineping
`mineping` is a Javasript library thar provides Minecraft server ping protocol implementation. It can be used to collect information about the server, such as MODT, current online, server icon (java edition only) and etc. This JavaScript library provides an implementation of the Minecraft server ping protocol. **It allows you to gather information about a Minecraft server**, such as the MOTD, current online players, server icon (Java Edition only), and more.
Mirror on my [<img src="https://git.zeldon.ru/assets/img/logo.svg" align="center" width="20" height="20"/> Git](https://git.zeldon.ru/zeldon/mineping) Mirror on my [<img src="https://git.zeldon.ru/assets/img/logo.svg" align="center" width="20" height="20"/> Git](https://git.zeldon.ru/zeldon/mineping)
@ -10,6 +10,8 @@ Mirror on my [<img src="https://git.zeldon.ru/assets/img/logo.svg" align="center
## Install ## Install
To install `mineping`, simply run the following command:
``` ```
npm i @minescope/mineping npm i @minescope/mineping
``` ```
@ -17,6 +19,7 @@ npm i @minescope/mineping
## Loading and configuration the module ## Loading and configuration the module
### ES Modules (ESM) ### ES Modules (ESM)
If you are using ES Modules, you can import the library like this:
```js ```js
import { pingJava, pingBedrock } from '@minescope/mineping'; import { pingJava, pingBedrock } from '@minescope/mineping';
@ -59,5 +62,7 @@ console.log(data);
## Acknowledgements ## Acknowledgements
Special thanks to the following projects:
- [mcping](https://github.com/Scetch/mcping) crate for Rust - [mcping](https://github.com/Scetch/mcping) crate for Rust
- [mcping-js](https://github.com/Cryptkeeper/mcping-js) library for quering Minecraft Java Edition servers - [mcping-js](https://github.com/Cryptkeeper/mcping-js) library for quering Minecraft Java Edition servers

View File

@ -1,40 +1,83 @@
#!/usr/bin/env node
/** /**
* Usage examples: * Usage examples:
* - Java (with custom timeout): node cli.js -j --host="mc.hypixel.net" --timeout 1000 * - Java (with custom timeout): node cli.js -j --host="mc.hypixel.net" --timeout 1000
* - Bedrock: node cli.js -b --host="play.timecrack.net" * - Bedrock: node cli.js -b --host="play.timecrack.net"
*/ */
import { pingBedrock, pingJava } from '../index.js'; import { pingBedrock, pingJava } from "../index.js";
const args = getArgs(); const DEFAULT_TIMEOUT = 5000;
const JAVA_DEFAULT_PORT = 25565;
const BEDROCK_DEFAULT_PORT = 19132;
if (shouldShowHelp(args)) { try {
const args = parseArgs(process.argv.slice(2));
if (shouldShowHelp(args)) {
printHelp(); printHelp();
process.exit(0);
}
validateArgs(args);
const port = Number(args.port) || getDefaultPort(args);
const timeout = Number(args.timeout) || DEFAULT_TIMEOUT;
if (args.j) {
await pingJavaServer(args.host, port, timeout);
} else if (args.b) {
await pingBedrockServer(args.host, port, timeout);
}
} catch (err) {
console.error(`ERROR: ${err.message}`);
process.exit(1); process.exit(1);
} }
if (!args.host) { function parseArgs(rawArgs) {
console.error('ERROR: The host argument not found! Use -h or --help.'); const args = {};
process.exit(1);
for (let i = 0; i < rawArgs.length; i++) {
const arg = rawArgs[i];
if (arg.startsWith("--")) {
// Handle --key=value and --key value formats
const [key, value] = arg.slice(2).split("=");
args[key] = value ?? rawArgs[++i] ?? true;
} else if (arg.startsWith("-")) {
// Handle short flags (-j, -b, -h)
const flags = arg.slice(1).split("");
flags.forEach((flag) => {
args[flag] = true;
});
}
}
return args;
} }
// easter egg <3 function validateArgs(args) {
if (args.j && args.b) { if (args.j && args.b) {
printInterestingFacts(); printInterestingFacts();
process.exit(0); process.exit(0);
} }
const port = args.port || getDefaultPort(args); if (!args.host) {
const timeout = args.timeout || 500; throw new Error("The host argument not found! Use -h or --help.");
}
if (args.j) { if (!args.j && !args.b) {
await pingJavaServer(args.host, port, timeout) throw new Error("Must specify either -j or -b flag. Use -h or --help.");
.catch(err => console.error(`ERROR: ${err.message}`)); }
} else if (args.b) {
await pingBedrockServer(args.host, port, timeout) if (args.port && (isNaN(args.port) || args.port < 1 || args.port > 65535)) {
.catch(err => console.error(`ERROR: ${err.message}`)); throw new Error("Port must be a number between 1 and 65535");
} else { }
console.error('ERROR: Unsupported flag passed. Use -h or --help.');
if (args.timeout && (isNaN(args.timeout) || args.timeout < 0)) {
throw new Error("Timeout must be a positive number");
}
} }
function shouldShowHelp(args) { function shouldShowHelp(args) {
@ -51,8 +94,13 @@ function printHelp() {
OPTIONS: OPTIONS:
-j Use for Minecraft Java Edition -j Use for Minecraft Java Edition
-b Use for Minecraft Bedrock Edition -b Use for Minecraft Bedrock Edition
-h, --help Show this help message
P.S. Don't use them at the same time!`); --host The server address (required)
--port The server port (default: ${JAVA_DEFAULT_PORT} for Java, ${BEDROCK_DEFAULT_PORT} for Bedrock)
--timeout The socket timeout in milliseconds (default: ${DEFAULT_TIMEOUT})
P.S. Don't use -j and -b at the same time!`);
} }
function printInterestingFacts() { function printInterestingFacts() {
@ -66,37 +114,27 @@ function printInterestingFacts() {
} }
function getDefaultPort(args) { function getDefaultPort(args) {
return args.j ? 25565 : 19132; return args.j ? JAVA_DEFAULT_PORT : BEDROCK_DEFAULT_PORT;
} }
async function pingJavaServer(host, port, timeout) { async function pingJavaServer(host, port, timeout) {
const data = await pingJava(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: ${host}
Version: ${data.version?.name} (protocol: ${data.version?.protocol})
Players: ${data.players?.online}/${data.players?.max}
Description: ${
typeof data.description === "string"
? data.description
: data.description?.text
}`);
} }
async function pingBedrockServer(host, port, timeout) { async function pingBedrockServer(host, port, timeout) {
const data = await pingBedrock(host, { port, timeout }); const data = await pingBedrock(host, { port, timeout });
console.log(`host: ${host}\nprotocol: ${data.version.protocol}\nonline: ${data.players.online}`); console.log(`Host: ${host}
} Edition: ${data.edition}
Version: ${data.version.minecraftVersion} (protocol: ${data.version.protocolVersion})
// parsing command line arguments Players: ${data.players.online}/${data.players.max}
function getArgs() { Name: ${data.name}
const args = {}; Gamemode: ${data.gameMode}`);
process.argv.slice(2).forEach(arg => {
// long arg
if (arg.slice(0, 2) === '--') {
const longArg = arg.split('=');
const longArgFlag = longArg[0].slice(2, longArg[0].length);
const longArgValue = longArg.length > 1 ? longArg[1] : true;
args[longArgFlag] = longArgValue;
// flags
} else if (arg[0] === '-') {
const flags = arg.slice(1, arg.length).split('');
flags.forEach(flag => {
args[flag] = true;
});
}
});
return args;
} }

View File

@ -13,6 +13,7 @@ const results = await Promise.allSettled(pingPromises);
for (let result of results) { for (let result of results) {
if (result.status === 'rejected') { if (result.status === 'rejected') {
console.error(result.reason); console.error(result.reason);
break;
} }
console.log(result.value); console.log(result.value);

5
example/single.js Normal file
View File

@ -0,0 +1,5 @@
import { pingBedrock } from "../index.js";
const host = "mc.nevertime.su";
const ping = await pingBedrock(host);
console.log(ping);

View File

@ -1,124 +1,85 @@
/** /**
* Implementation of the RakNet ping/pong protocol. * Implementation of the RakNet ping/pong protocol.
* @see https://wiki.vg/Raknet_Protocol#Unconnected_Ping * @see https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Raknet_Protocol
*
* Data types:
* @see https://wiki.vg/Raknet_Protocol#Data_types
*/ */
'use strict'; "use strict";
import dgram from 'dgram'; import dgram from "node:dgram";
import crypto from "node:crypto";
const MAGIC = "00ffff00fefefefefdfdfdfd12345678";
const START_TIME = new Date().getTime(); const START_TIME = new Date().getTime();
const UNCONNECTED_PONG = 0x1c;
/** /**
* Creates a buffer with the specified length. * Creates an Unconnected Ping packet.
* @param {number} length - The length of the buffer. * @param {number} pingId
* @returns {Buffer} - The created buffer. * @returns {Buffer}
* @see {@link https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Raknet_Protocol#Unconnected_Ping}
*/ */
const createBuffer = (length) => { const createUnconnectedPingFrame = (timestamp) => {
const buffer = Buffer.alloc(length); const buffer = Buffer.alloc(33);
buffer[0] = 0x01; 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; return buffer;
}; };
/** /**
* Writes a BigInt value to the buffer at the specified offset using big-endian byte order. * Extract Modt from Unconnected Pong Packet and convert to an object
* @param {Buffer} buffer - The buffer to write to. * @param {Buffer} unconnectedPongPacket
* @param {number} value - The BigInt value to write. * @returns {Object}
* @param {number} offset - The offset in the buffer to write the value. * @throws {Error} If packet is malformed or invalid
* @see {@link https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Raknet_Protocol#Unconnected_Pong}
*/ */
const writeBigInt64BE = (buffer, value, offset) => { const extractModt = (unconnectedPongPacket) => {
buffer.writeBigInt64BE(BigInt(value), offset); if (
}; !Buffer.isBuffer(unconnectedPongPacket) ||
unconnectedPongPacket.length < 35
/** ) {
* Copies the specified hex value to the buffer at the specified offset. throw new Error("Invalid pong packet");
* @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);
};
/**
* 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 {
gameId: parts[0],
description: parts[1],
protocolVersion: parts[2],
gameVersion: parts[3],
currentPlayers: parts[4],
maxPlayers: parts[5],
name: parts[7],
mode: parts[8]
};
};
/**
* 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); const offset = 33;
const length = unconnectedPongPacket.readUInt16BE(offset);
return { pingId, advertiseStr, serverId, offset, ...parsedAdvertiseStr }; // Check for buffer bounds
if (offset + 2 + length > unconnectedPongPacket.length) {
throw new Error("Malformed pong packet");
}
let modt = unconnectedPongPacket.toString(
"utf-8",
offset + 2,
offset + 2 + length
);
const components = modt.split(";");
// Validate required components
if (components.length < 9) {
throw new Error("Invalid MODT format");
}
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;
}; };
/** /**
@ -129,12 +90,12 @@ const UNCONNECTED_PONG = (buffer) => {
* @param {number} [timeout=5000] - The timeout duration in milliseconds. * @param {number} [timeout=5000] - The timeout duration in milliseconds.
*/ */
const ping = (host, port = 19132, cb, timeout = 5000) => { const ping = (host, port = 19132, cb, timeout = 5000) => {
const socket = dgram.createSocket('udp4'); const socket = dgram.createSocket("udp4");
// Set manual timeout interval. // Set manual timeout interval.
// This ensures the connection will NEVER hang regardless of internal state // This ensures the connection will NEVER hang regardless of internal state
const timeoutTask = setTimeout(() => { const timeoutTask = setTimeout(() => {
socket.emit('error', new Error('Socket timeout')); socket.emit("error", new Error("Socket timeout"));
}, timeout); }, timeout);
const closeSocket = () => { const closeSocket = () => {
@ -161,44 +122,34 @@ const ping = (host, port = 19132, cb, timeout = 5000) => {
}; };
try { try {
const ping = UNCONNECTED_PING(new Date().getTime() - START_TIME); const ping = createUnconnectedPingFrame(new Date().getTime() - START_TIME);
socket.send(ping, 0, ping.length, port, host); socket.send(ping, 0, ping.length, port, host);
} catch (err) { } catch (err) {
handleError(err); handleError(err);
} }
socket.on('message', (msg) => { socket.on("message", (pongPacket) => {
const id = msg[0]; if (!Buffer.isBuffer(pongPacket) || pongPacket.length === 0) {
handleError(new Error("Invalid packet received"));
return;
}
switch (id) { const id = pongPacket[0];
case 0x1c: { if (id !== UNCONNECTED_PONG) {
const pong = UNCONNECTED_PONG(msg); handleError(new Error(`Unexpected packet ID: 0x${id.toString(16)}`));
const clientData = { return;
version: { }
name: pong.name,
protocol: pong.protocolVersion
},
players: {
max: pong.maxPlayers,
online: pong.currentPlayers
},
description: pong.description.replace(/\xA7[0-9A-FK-OR]/ig, ''),
gamemode: pong.mode
};
try {
const modtObject = extractModt(pongPacket);
closeSocket(); closeSocket();
cb(clientData, null); cb(modtObject, null);
break; } catch (err) {
} handleError(err);
default: {
handleError(new Error('Received unexpected packet'));
break;
}
} }
}); });
socket.on('error', handleError); socket.on("error", handleError);
}; };
/** /**
@ -209,13 +160,18 @@ const ping = (host, port = 19132, cb, timeout = 5000) => {
* @returns {Promise<import('../types/index.js').BedrockPingResponse>} * @returns {Promise<import('../types/index.js').BedrockPingResponse>}
*/ */
export const pingBedrock = (host, options = {}) => { 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) => { return new Promise((resolve, reject) => {
ping(host, port, (res, err) => { ping(
host,
port,
(res, err) => {
err ? reject(err) : resolve(res); err ? reject(err) : resolve(res);
}, timeout); },
timeout
);
}); });
}; };

View File

@ -1,29 +1,29 @@
/** /**
* Implementation of the Java Minecraft ping protocol. * 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 'net'; import net from "node:net";
import varint from './varint.js'; import varint from "./varint.js";
const PROTOCOL_VERSION = 0;
/** /**
* Ping a Minecraft Java server. * Ping a Minecraft Java server.
* @param {string} host The host of the Java server. * @param {string} host The host of the Java server.
* @param {string} virtualHost The host sent in handshake.
* @param {number} [port=25565] The port 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 {function} cb The callback function to handle the ping response.
* @param {number} [timeout=5000] The timeout duration in milliseconds. * @param {number} [timeout=5000] The timeout duration in milliseconds.
* @param {number} [protocolVersion=-1] The protocol version of the Java client.
*/ */
function ping(host, port = 25565, cb, timeout = 5000) { function ping(host, virtualHost, port = 25565, cb, timeout = 5000, protocolVersion = -1) {
const socket = net.createConnection(({ host, port })); const socket = net.createConnection({ host, port });
// Set manual timeout interval. // Set manual timeout interval.
// This ensures the connection will NEVER hang regardless of internal state // This ensures the connection will NEVER hang regardless of internal state
const timeoutTask = setTimeout(() => { const timeoutTask = setTimeout(() => {
socket.emit('error', new Error('Socket timeout')); socket.emit("error", new Error("Socket timeout"));
}, timeout); }, timeout);
const closeSocket = () => { const closeSocket = () => {
@ -53,34 +53,32 @@ function ping(host, port = 25565, cb, timeout = 5000) {
// This prevents the runtime from delaying the write at all // This prevents the runtime from delaying the write at all
socket.setNoDelay(true); socket.setNoDelay(true);
socket.on('connect', () => { socket.on("connect", () => {
const handshake = varint.concat([ const handshake = varint.concat([
varint.encodeInt(0), varint.encodeInt(0),
varint.encodeInt(PROTOCOL_VERSION), varint.encodeInt(protocolVersion),
varint.encodeInt(host.length), varint.encodeInt(virtualHost.length),
varint.encodeString(host), varint.encodeString(virtualHost),
varint.encodeUShort(port), varint.encodeUShort(port),
varint.encodeInt(1) varint.encodeInt(1),
]); ]);
socket.write(handshake); socket.write(handshake);
const request = varint.concat([ const request = varint.concat([varint.encodeInt(0)]);
varint.encodeInt(0)
]);
socket.write(request); socket.write(request);
}); });
let incomingBuffer = Buffer.alloc(0); let incomingBuffer = Buffer.alloc(0);
socket.on('data', (data) => { socket.on("data", (data) => {
incomingBuffer = Buffer.concat([incomingBuffer, data]); incomingBuffer = Buffer.concat([incomingBuffer, data]);
// Wait until incomingBuffer is at least 5 bytes long to ensure it has captured the first VarInt value // 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 // This value is used to determine the full read length of the response
// "VarInts are never longer than 5 bytes" // "VarInts are never longer than 5 bytes"
// https://wiki.vg/Data_types#VarInt_and_VarLong // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Data_types#VarInt_and_VarLong
if (incomingBuffer.length < 5) { if (incomingBuffer.length < 5) {
return; return;
} }
@ -93,12 +91,20 @@ function ping(host, port = 25565, cb, timeout = 5000) {
return; return;
} }
const packetId = varint.decodeInt(incomingBuffer, varint.decodeLength(packetLength)); const packetId = varint.decodeInt(
incomingBuffer,
varint.decodeLength(packetLength)
);
if (packetId === 0) { if (packetId === 0) {
const data = incomingBuffer.subarray(varint.decodeLength(packetLength) + varint.decodeLength(packetId)); const data = incomingBuffer.subarray(
varint.decodeLength(packetLength) + varint.decodeLength(packetId)
);
const responseLength = varint.decodeInt(data, 0); const responseLength = varint.decodeInt(data, 0);
const response = data.subarray(varint.decodeLength(responseLength), varint.decodeLength(responseLength) + responseLength); const response = data.subarray(
varint.decodeLength(responseLength),
varint.decodeLength(responseLength) + responseLength
);
try { try {
const message = JSON.parse(response); const message = JSON.parse(response);
@ -109,28 +115,35 @@ function ping(host, port = 25565, cb, timeout = 5000) {
handleError(err); handleError(err);
} }
} else { } else {
handleError(new Error('Received unexpected packet')); handleError(new Error("Received unexpected packet"));
} }
}); });
socket.on('error', handleError); socket.on("error", handleError);
} }
/** /**
* Asynchronously ping Minecraft Java server. * 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. * The optional `options` argument can be an object with a `port` (default is `25565`) or/and `timeout` (default is `5000`) or/and `protocolVersion` (default is `-1`) property.
* @param {string} host The Java server address. * @param {string} host The Java server address.
* @param {import('../types/index.js').PingOptions} options The configuration for pinging Minecraft Java server. * @param {import('../types/index.js').PingOptions} options The configuration for pinging Minecraft Java server.
* @returns {Promise<import('../types/index.js').JavaPingResponse>} * @returns {Promise<import('../types/index.js').JavaPingResponse>}
*/ */
export function pingJava(host, options = {}) { 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 } = options; const { port = 25565, timeout = 5000, protocolVersion = -1, virtualHost = null } = options;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
ping(host, port, (res, err) => { ping(
host,
virtualHost || host,
port,
(res, err) => {
err ? reject(err) : resolve(res); err ? reject(err) : resolve(res);
}, timeout); },
timeout,
protocolVersion
);
}); });
} }

View File

@ -1,4 +1,4 @@
// 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. * A utility object for encoding and decoding varints.
@ -10,13 +10,13 @@ const varint = {
* @returns {Buffer} * @returns {Buffer}
*/ */
encodeInt: (val) => { encodeInt: (val) => {
// "constInts are never longer than 5 bytes" // "VarInts are never longer than 5 bytes"
// https://wiki.vg/Data_types#constInt_and_constLong // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Data_types#VarInt_and_VarLong
const buf = Buffer.alloc(5); const buf = Buffer.alloc(5);
let written = 0; let written = 0;
while (true) { while (true) {
const byte = val & 0x7F; const byte = val & 0x7f;
val >>>= 7; val >>>= 7;
if (val === 0) { if (val === 0) {
@ -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);
}, },
/** /**
@ -36,7 +36,7 @@ const varint = {
* @returns {Buffer} * @returns {Buffer}
*/ */
encodeString: (val) => { encodeString: (val) => {
return Buffer.from(val, 'utf-8'); return Buffer.from(val, "utf-8");
}, },
/** /**
@ -45,7 +45,7 @@ const varint = {
* @returns {Buffer} * @returns {Buffer}
*/ */
encodeUShort: (val) => { encodeUShort: (val) => {
return Buffer.from([val >> 8, val & 0xFF]); return Buffer.from([val >> 8, val & 0xff]);
}, },
/** /**
@ -60,68 +60,64 @@ const varint = {
length += chunk.length; length += chunk.length;
} }
const buffer = [ const buffer = [varint.encodeInt(length), ...chunks];
varint.encodeInt(length),
...chunks
];
return Buffer.concat(buffer); return Buffer.concat(buffer);
}, },
/** /**
* Decodes a varint integer value from a byte buffer. * Decodes a varint integer value from a buffer.
* @param {Buffer} buffer - The byte buffer to decode from. * @param {Buffer} buffer - The byte buffer to decode from.
* @param {number} offset - The offset in the buffer to start decoding from. * @param {number} offset - The offset in the buffer to start decoding from.
* @returns {number} * @returns {number}
*/ */
decodeInt: (buffer, offset) => { decodeInt: (buffer, offset) => {
let val = 0; // Fast path for single-byte varints
let count = 0; const firstByte = buffer.readUInt8(offset);
if (firstByte < 0x80) {
while (true) { return firstByte;
const byte = buffer.readUInt8(offset++);
val |= (byte & 0x7F) << count++ * 7;
if ((byte & 0x80) !== 0x80) {
break;
}
} }
let val = firstByte & 0x7f;
let position = 7;
while (position < 32) {
const byte = buffer.readUInt8(++offset);
val |= (byte & 0x7f) << position;
if ((byte & 0x80) === 0) {
return val; return val;
}
position += 7;
}
throw new Error("VarInt is too big");
}, },
/** /**
* Calculates the number of bytes required to decode a varint integer value. * Calculates how many bytes are needed to encode a number as a VarInt
* @param {number} val - The varint integer value. * VarInts use a variable number of bytes to efficiently encode integers
* @returns {5 | 7 | 8 | 1 | 2 | 3 | 4 | 6 | 9 | 10} * Each byte uses 7 bits for the value and 1 bit to indicate if more bytes follow
* VarInts are never longer than 5 bytes
*
* @param {number} val - The number to calculate the VarInt length for
* @returns {1|2|3|4|5} The number of bytes needed to encode the value
*/ */
decodeLength: (val) => { decodeLength: (val) => {
// Constants representing the powers of 2 used for comparison // Using bit shifts to calculate power of 2 thresholds
const N1 = Math.pow(2, 7); // 1 << 7 = 2^7 = 128 - Numbers below this fit in 1 byte
const N2 = Math.pow(2, 14); // 1 << 14 = 2^14 = 16,384 - Numbers below this fit in 2 bytes
const N3 = Math.pow(2, 21); // 1 << 21 = 2^21 = 2,097,152 - Numbers below this fit in 3 bytes
const N4 = Math.pow(2, 28); // 1 << 28 = 2^28 = 268,435,456 - Numbers below this fit in 4 bytes
const N5 = Math.pow(2, 35); // Any larger number needs 5 bytes (maximum VarInt size)
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 if (val < 1 << 7) return 1;
return ( if (val < 1 << 14) return 2;
val < N1 ? 1 if (val < 1 << 21) return 3;
: val < N2 ? 2 if (val < 1 << 28) return 4;
: val < N3 ? 3 return 5;
: val < N4 ? 4 },
: val < N5 ? 5
: val < N6 ? 6
: val < N7 ? 7
: val < N8 ? 8
: val < N9 ? 9
: 10
);
}
}; };
export default varint; export default varint;

View File

@ -1,6 +1,6 @@
{ {
"name": "@minescope/mineping", "name": "@minescope/mineping",
"version": "1.1.1", "version": "1.6.1",
"description": "Ping both Minecraft Bedrock and Java servers.", "description": "Ping both Minecraft Bedrock and Java servers.",
"main": "index.js", "main": "index.js",
"types": "types/index.d.ts", "types": "types/index.d.ts",

View File

@ -1,31 +1,33 @@
/** /**
* @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 PingOptions = { export type BedrockPingOptions = {
port: number, port?: number & { _brand: "Port" }; // 1-65535
timeout: number; timeout?: number & { _brand: "Timeout" }; // > 0
}; };
export type BedrockPingResponse = { export type BedrockPingResponse = {
version: { edition: string;
name: string; name: string;
protocol: string; version: {
protocolVersion: number;
minecraftVersion: string;
}; };
players: { players: {
max: string; online: number;
online: string; max: number;
}; };
description: string; serverId: string;
gamemode: string; mapName: 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.
* *
* ```js * ```js
* import { pingBedrock } from '@minescope/mineping'; * import { pingBedrock } from '@minescope/mineping';
@ -37,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?: PingOptions): Promise<BedrockPingResponse>; export function pingBedrock(
host: string,
options?: BedrockPingOptions
): Promise<BedrockPingResponse>;

24
types/lib/java.d.ts vendored
View File

@ -1,8 +1,18 @@
import { PingOptions } from "./bedrock"; /**
* @param port The server port.
* @param timeout The read/write socket timeout.
* @param protocolVersion The protocol version.
*/
export type JavaPingOptions = {
port?: number | undefined;
timeout?: number | undefined;
protocolVersion?: number | undefined;
virtualHost?: string | undefined;
};
/** /**
* JSON format chat component used for description field. * 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 = { export type ChatComponent = {
text: string; text: string;
@ -16,13 +26,13 @@ export type ChatComponent = {
}; };
export type SampleProp = { export type SampleProp = {
name: string, name: string;
id: string; id: string;
}; };
/** /**
* `JSON Response` field of Response packet. * `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 = { export type JavaPingResponse = {
version: { version: {
@ -67,5 +77,7 @@ export type JavaPingResponse = {
* ``` * ```
* @see [source](https://github.com/minescope/mineping/blob/915edbec9c9ad811459458600af3531ec0836911/lib/java.js#L117) * @see [source](https://github.com/minescope/mineping/blob/915edbec9c9ad811459458600af3531ec0836911/lib/java.js#L117)
*/ */
export function pingJava(host: string, options?: PingOptions): Promise<JavaPingResponse>; export function pingJava(
host: string,
options?: JavaPingOptions
): Promise<JavaPingResponse>;

38
types/lib/varint.d.ts vendored
View File

@ -1,10 +1,44 @@
export default varint; export default varint;
declare namespace varint { declare namespace varint {
/**
* Encodes an integer value into a varint byte buffer.
* @param val - The integer value to encode.
*/
function encodeInt(val: number): Buffer; function encodeInt(val: number): Buffer;
/**
* Encodes a string value into a UTF-8 byte buffer.
* @param val - The string value to encode.
*/
function encodeString(val: string): Buffer; function encodeString(val: string): Buffer;
/**
* Encodes an unsigned short value into a byte buffer.
* @param val - The unsigned short value to encode.
*/
function encodeUShort(val: number): Buffer; function encodeUShort(val: number): Buffer;
/**
* Concatenates multiple byte buffers into a single byte buffer.
* @param chunks - An array of byte buffers to concatenate.
*/
function concat(chunks: Buffer[]): Buffer; function concat(chunks: Buffer[]): Buffer;
/**
* Decodes a varint integer value from a buffer.
* @param buffer - The byte buffer to decode from.
* @param offset - The offset in the buffer to start decoding from.
*/
function decodeInt(buffer: Buffer, offset: number): number; function decodeInt(buffer: Buffer, offset: number): number;
function decodeString(val: Buffer, offset?: number): string;
function decodeLength(val: number): 5 | 7 | 8 | 1 | 2 | 3 | 4 | 6 | 9 | 10; /**
* Calculates how many bytes are needed to encode a number as a VarInt.
* VarInts use a variable number of bytes to efficiently encode integers.
* Each byte uses 7 bits for the value and 1 bit to indicate if more bytes follow.
* VarInts are never longer than 5 bytes.
*
* @param val - The number to calculate the VarInt length for.
* @returns The number of bytes needed to encode the value (1-5).
*/
function decodeLength(val: number): 1 | 2 | 3 | 4 | 5;
} }