mirror of
https://github.com/minescope/mineping.git
synced 2025-07-18 20:46:36 +03:00
Compare commits
41 Commits
v1.1.1
...
1e6b5b3973
Author | SHA1 | Date | |
---|---|---|---|
1e6b5b3973
|
|||
23299a9a07
|
|||
2bd5d9c9bf
|
|||
7248a0096c
|
|||
a1b999ca4e
|
|||
7322034aba
|
|||
c7b99cb6db
|
|||
51b4771305
|
|||
3c2c049c19
|
|||
cbaa1a3e3e
|
|||
435e59739c
|
|||
ef2bebe755
|
|||
27011d4091
|
|||
d8d4a9a467
|
|||
d90a916fa5
|
|||
0959403b1b
|
|||
c71236f223
|
|||
0b5c5e2938
|
|||
73a2fffe8b | |||
9469564736
|
|||
838ffc497a
|
|||
502029869a
|
|||
e3e7e293ed
|
|||
88ad92e59d
|
|||
009f542c55
|
|||
0b0bed4e71
|
|||
fa4c34d896
|
|||
296294ca96
|
|||
9d25aaf4ea
|
|||
c735604c38
|
|||
afdaa9eb3e
|
|||
435899309f | |||
13e6b8c6ff | |||
d7256eabe7
|
|||
afa2c3025f
|
|||
6c297d0b8c
|
|||
283e9b32c6 | |||
354fa212a6 | |||
d9bf4cfb3f | |||
9dace3748b
|
|||
0aa73655b1
|
28
README.md
28
README.md
@ -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,16 +10,22 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> To install _beta_ version (if available), run: `npm i @minescope/mineping@next`
|
||||||
|
|
||||||
## 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";
|
||||||
```
|
```
|
||||||
|
|
||||||
### CommonJS
|
### CommonJS
|
||||||
@ -28,8 +34,10 @@ import { pingJava, pingBedrock } from '@minescope/mineping';
|
|||||||
If you cannot switch to ESM, you can use the async `import()` function from CommonJS to load `mineping` asynchronously:
|
If you cannot switch to ESM, you can use the async `import()` function from CommonJS to load `mineping` asynchronously:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const pingJava = (...args) => import('@minescope/mineping').then(module => module.pingJava(...args));
|
const pingJava = (...args) =>
|
||||||
const pingBedrock = (...args) => import('@minescope/mineping').then(module => module.pingBedrock(...args));
|
import("@minescope/mineping").then((module) => module.pingJava(...args));
|
||||||
|
const pingBedrock = (...args) =>
|
||||||
|
import("@minescope/mineping").then((module) => module.pingBedrock(...args));
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -37,20 +45,20 @@ const pingBedrock = (...args) => import('@minescope/mineping').then(module => mo
|
|||||||
Ping a Java server with default options:
|
Ping a Java server with default options:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { pingJava } from '@minescope/mineping';
|
import { pingJava } from "@minescope/mineping";
|
||||||
|
|
||||||
const data = await pingJava('mc.hypixel.net');
|
const data = await pingJava("mc.hypixel.net");
|
||||||
console.log(data);
|
console.log(data);
|
||||||
```
|
```
|
||||||
|
|
||||||
Ping a Bedrock server with custom options:
|
Ping a Bedrock server with custom options:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { pingBedrock } from '@minescope/mineping';
|
import { pingBedrock } from "@minescope/mineping";
|
||||||
|
|
||||||
const data = await pingBedrock('mco.mineplex.com', {
|
const data = await pingBedrock("mco.mineplex.com", {
|
||||||
port: 19132,
|
port: 19132,
|
||||||
timeout: 500
|
timeout: 500,
|
||||||
});
|
});
|
||||||
console.log(data);
|
console.log(data);
|
||||||
```
|
```
|
||||||
@ -59,5 +67,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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Reporting a Vulnerability
|
# Reporting a Vulnerability
|
||||||
|
|
||||||
If you believe you have found a security vulnerability, let us know by sending email to contact@zeldon.ru We will investigate that and do our best to quickly fix the problem.
|
If you believe you have found a security vulnerability, let me know by sending email to timofey@z4n.me I will investigate that and do my best to quickly fix the problem.
|
||||||
|
|
||||||
Please don't open an issue to or discuss this security vulnerability in a public place. Thanks for understanding!
|
Please don't open an issue to or discuss this security vulnerability in a public place. Thanks for understanding!
|
128
example/cli.js
128
example/cli.js
@ -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;
|
|
||||||
}
|
}
|
@ -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
5
example/single.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { pingBedrock } from "../index.js";
|
||||||
|
|
||||||
|
const host = "0.0.0.0";
|
||||||
|
const motd = await pingBedrock(host);
|
||||||
|
console.log(motd);
|
380
lib/bedrock.js
380
lib/bedrock.js
@ -1,221 +1,261 @@
|
|||||||
/**
|
/**
|
||||||
* 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/RakNet
|
||||||
*
|
|
||||||
* 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";
|
||||||
|
import createDebug from "debug";
|
||||||
|
|
||||||
const START_TIME = new Date().getTime();
|
const debug = createDebug("mineping:bedrock");
|
||||||
|
|
||||||
|
const MAGIC = "00ffff00fefefefefdfdfdfd12345678";
|
||||||
|
const START_TIME = Date.now();
|
||||||
|
const UNCONNECTED_PONG = 0x1c;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a buffer with the specified length.
|
* Representation of raw, semicolon-delimited MOTD string.
|
||||||
* @param {number} length - The length of the buffer.
|
* This struct directly mirrors the fields and order from the server response.
|
||||||
* @returns {Buffer} - The created buffer.
|
* See [`Unconnected Pong Documentation`](https://minecraft.wiki/w/RakNet#Unconnected_Pong) for more details.
|
||||||
|
* @typedef {object} BedrockMotd
|
||||||
|
* @property {string} edition - The edition of the server (MCPE or MCEE).
|
||||||
|
* @property {string} name - The primary name of the server (first line of MOTD).
|
||||||
|
* @property {number} protocol - The protocol version.
|
||||||
|
* @property {string} version - The game version (e.g., "1.21.2").
|
||||||
|
* @property {number} playerCount - The current number of players online.
|
||||||
|
* @property {number} playerMax - The maximum number of players allowed.
|
||||||
|
* @property {bigint} serverGuid - The server's GUID.
|
||||||
|
* @property {string} subName - The secondary name of the server (second line of MOTD).
|
||||||
|
* @property {string} gamemode - The default gamemode (e.g., "Survival").
|
||||||
|
* @property {boolean} [nintendoLimited] - Whether the server is Nintendo limited.
|
||||||
|
* @property {number} [port] - The server's IPv4 port, if provided.
|
||||||
|
* @property {number} [ipv6Port] - The server's IPv6 port, if provided.
|
||||||
|
* @property {boolean} [editorMode] - Whether the server is in editor mode, if provided. See [Minecraft Editor Mode Documentation](https://learn.microsoft.com/en-us/minecraft/creator/documents/bedrockeditor/editoroverview?view=minecraft-bedrock-stable) for more details.
|
||||||
*/
|
*/
|
||||||
const createBuffer = (length) => {
|
|
||||||
const buffer = Buffer.alloc(length);
|
/**
|
||||||
buffer[0] = 0x01;
|
* Represents the structured and user-friendly response from a server ping.
|
||||||
|
* This is the public-facing object that users of the library will receive.
|
||||||
|
* @typedef {object} BedrockPingResponse
|
||||||
|
* @property {string} edition - The edition of the server (MCPE or MCEE).
|
||||||
|
* @property {string} name - The primary name of the server (first line of MOTD).
|
||||||
|
* @property {string} levelName - The name of the world or level being hosted.
|
||||||
|
* @property {string} gamemode - The default gamemode of the server.
|
||||||
|
* @property {{ protocol: number, minecraft: string }} version - Game and protocol versions.
|
||||||
|
* @property {{ online: number, max: number }} players - Current and maximum player counts.
|
||||||
|
* @property {{ v4?: number, v6?: number }} port - Announced IPv4 and IPv6 ports.
|
||||||
|
* @property {bigint} guid - The server's unique 64-bit GUID.
|
||||||
|
* @property {boolean} [isNintendoLimited] - True if the server restricts Nintendo Switch players.
|
||||||
|
* @property {boolean} [isEditorModeEnabled] - True if the server is in editor mode. See [Minecraft Editor Mode Documentation](https://learn.microsoft.com/en-us/minecraft/creator/documents/bedrockeditor/editoroverview?view=minecraft-bedrock-stable) for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} BedrockPingOptions
|
||||||
|
* @property {number} [port=19132] - The server port to ping.
|
||||||
|
* @property {number} [timeout=5000] - The timeout in milliseconds for the request.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Unconnected Ping packet.
|
||||||
|
* See [Unconnected Ping Documentation](https://minecraft.wiki/w/RakNet#Unconnected_Ping) for more details.
|
||||||
|
* @param {number} timestamp - The current time delta since the script started.
|
||||||
|
* @returns {Buffer}
|
||||||
|
*/
|
||||||
|
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 bytes)
|
||||||
|
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.
|
* Parses the semicolon-delimited MOTD string into a structured object.
|
||||||
* @param {Buffer} buffer - The buffer to write to.
|
* @param {string} motdString - The raw MOTD string from the server.
|
||||||
* @param {number} value - The BigInt value to write.
|
* @returns {BedrockMotd} The parsed internal MOTD object.
|
||||||
* @param {number} offset - The offset in the buffer to write the value.
|
* @throws {Error} If the MOTD string is missing required fields.
|
||||||
*/
|
*/
|
||||||
const writeBigInt64BE = (buffer, value, offset) => {
|
const parseMotd = (motdString) => {
|
||||||
buffer.writeBigInt64BE(BigInt(value), offset);
|
const parts = motdString.split(";");
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
if (parts.length < 5) {
|
||||||
* Copies the specified hex value to the buffer at the specified offset.
|
throw new Error(
|
||||||
* @param {Buffer} buffer - The buffer to copy to.
|
`Invalid MOTD format: Expected at least 5 fields, but got ${parts.length}.`
|
||||||
* @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);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
const [
|
||||||
* Reads a BigInt value from the buffer at the specified offset using big-endian byte order.
|
edition,
|
||||||
* @param {Buffer} buffer - The buffer to read from.
|
name,
|
||||||
* @param {number} offset - The offset in the buffer to read the value.
|
protocolStr,
|
||||||
* @returns {BigInt} - The read BigInt value.
|
version,
|
||||||
*/
|
playerCountStr,
|
||||||
const readBigInt64BE = (buffer, offset) => {
|
playerMaxStr,
|
||||||
return buffer.readBigInt64BE(offset);
|
serverGuidStr,
|
||||||
};
|
subName,
|
||||||
|
gamemode,
|
||||||
|
nintendoLimitedStr,
|
||||||
|
port,
|
||||||
|
ipv6Port,
|
||||||
|
editorModeStr,
|
||||||
|
] = parts;
|
||||||
|
|
||||||
/**
|
let nintendoLimited;
|
||||||
* Reads a string from the buffer at the specified offset.
|
if (nintendoLimitedStr === "0") {
|
||||||
* @param {Buffer} buffer - The buffer to read from.
|
nintendoLimited = true;
|
||||||
* @param {number} offset - The offset in the buffer to read the string.
|
} else if (nintendoLimitedStr === "1") {
|
||||||
* @returns {string} - The read string.
|
nintendoLimited = false;
|
||||||
*/
|
}
|
||||||
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 {
|
return {
|
||||||
gameId: parts[0],
|
edition,
|
||||||
description: parts[1],
|
name,
|
||||||
protocolVersion: parts[2],
|
protocol: Number(protocolStr),
|
||||||
gameVersion: parts[3],
|
version,
|
||||||
currentPlayers: parts[4],
|
playerCount: Number(playerCountStr),
|
||||||
maxPlayers: parts[5],
|
playerMax: Number(playerMaxStr),
|
||||||
name: parts[7],
|
serverGuid: BigInt(serverGuidStr),
|
||||||
mode: parts[8]
|
subName,
|
||||||
|
gamemode,
|
||||||
|
nintendoLimited,
|
||||||
|
port: port ? Number(port) : undefined,
|
||||||
|
ipv6Port: ipv6Port ? Number(ipv6Port) : undefined,
|
||||||
|
editorMode: editorModeStr ? Boolean(Number(editorModeStr)) : undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Unconnected Ping buffer.
|
* Transforms the raw MOTD object into a user-friendly, nested structure.
|
||||||
* @param {number} pingId - The ping ID.
|
* @param {BedrockMotd} motd - The parsed MOTD object.
|
||||||
* @returns {Buffer} - The Unconnected Ping buffer.
|
* @returns {BedrockPingResponse} The final, user-facing response object.
|
||||||
* @see {@link https://wiki.vg/Raknet_Protocol#Unconnected_Ping}
|
|
||||||
*/
|
*/
|
||||||
const UNCONNECTED_PING = (pingId) => {
|
const transformMotd = (motd) => {
|
||||||
const buffer = createBuffer(35);
|
return {
|
||||||
writeBigInt64BE(buffer, pingId, 1);
|
edition: motd.edition,
|
||||||
copyHexToBuffer(buffer, '00ffff00fefefefefdfdfdfd12345678', 9);
|
name: motd.name,
|
||||||
writeBigInt64BE(buffer, 0, 25);
|
levelName: motd.subName,
|
||||||
return buffer;
|
gamemode: motd.gamemode,
|
||||||
|
version: {
|
||||||
|
protocol: motd.protocol,
|
||||||
|
minecraft: motd.version,
|
||||||
|
},
|
||||||
|
players: {
|
||||||
|
online: motd.playerCount,
|
||||||
|
max: motd.playerMax,
|
||||||
|
},
|
||||||
|
port: {
|
||||||
|
v4: motd.port,
|
||||||
|
v6: motd.ipv6Port,
|
||||||
|
},
|
||||||
|
guid: motd.serverGuid,
|
||||||
|
isNintendoLimited: motd.nintendoLimited,
|
||||||
|
isEditorModeEnabled: motd.editorMode,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes an Unconnected Pong buffer and returns the parsed data.
|
* Extracts the MOTD string from an Unconnected Pong packet and parses it.
|
||||||
* @param {Buffer} buffer - The Unconnected Pong buffer.
|
* @param {Buffer} pongPacket - The raw pong packet from the server.
|
||||||
* @returns {Object} - The parsed Unconnected Pong data.
|
* @returns {BedrockPingResponse} The final response object.
|
||||||
* @see {@link https://wiki.vg/Raknet_Protocol#Unconnected_Pong}
|
* @throws {Error} If the packet is malformed.
|
||||||
*/
|
*/
|
||||||
const UNCONNECTED_PONG = (buffer) => {
|
const parseUnconnectedPong = (pongPacket) => {
|
||||||
const pingId = readBigInt64BE(buffer, 1);
|
if (!Buffer.isBuffer(pongPacket) || pongPacket.length < 35) {
|
||||||
const serverId = readBigInt64BE(buffer, 9);
|
throw new Error("Invalid pong packet: buffer is too small.");
|
||||||
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 packetId = pongPacket.readUInt8(0);
|
||||||
|
if (packetId !== UNCONNECTED_PONG) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected packet ID: 0x${packetId.toString(16)}. Expected 0x1c.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return { pingId, advertiseStr, serverId, offset, ...parsedAdvertiseStr };
|
// The MOTD string is prefixed with its length as a 16-bit big-endian integer
|
||||||
|
const motdLength = pongPacket.readUInt16BE(33);
|
||||||
|
const motdOffset = 35;
|
||||||
|
|
||||||
|
if (motdOffset + motdLength > pongPacket.length) {
|
||||||
|
throw new Error("Malformed pong packet: MOTD length exceeds buffer size.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const motdString = pongPacket.toString(
|
||||||
|
"utf-8",
|
||||||
|
motdOffset,
|
||||||
|
motdOffset + motdLength
|
||||||
|
);
|
||||||
|
debug("received raw MOTD string: %s", motdString);
|
||||||
|
|
||||||
|
const rawMotd = parseMotd(motdString);
|
||||||
|
const motd = transformMotd(rawMotd);
|
||||||
|
return motd;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a ping request to the specified host and port.
|
* Asynchronously pings a Minecraft Bedrock server.
|
||||||
* @param {string} host - The IP address or hostname of the server.
|
* @param {string} host - The IP address or hostname of the server.
|
||||||
* @param {number} [port=19132] - The port number.
|
* @param {BedrockPingOptions} [options={}] - Optional configuration.
|
||||||
* @param {function} cb - The callback function to handle the response.
|
* @returns {Promise<BedrockPingResponse>} A promise that resolves with the server's parsed MOTD.
|
||||||
* @param {number} [timeout=5000] - The timeout duration in milliseconds.
|
|
||||||
*/
|
*/
|
||||||
const ping = (host, port = 19132, cb, timeout = 5000) => {
|
export async function pingBedrock(host, options = {}) {
|
||||||
const socket = dgram.createSocket('udp4');
|
if (!host) {
|
||||||
|
throw new Error("Host argument is required.");
|
||||||
|
}
|
||||||
|
|
||||||
// Set manual timeout interval.
|
const { port = 19132, timeout = 5000 } = options;
|
||||||
// This ensures the connection will NEVER hang regardless of internal state
|
debug("pinging Bedrock server %s:%d with %dms timeout", host, port, timeout);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const socket = dgram.createSocket("udp4");
|
||||||
|
|
||||||
|
// Prevent cleanup tasks from running more than once
|
||||||
|
// in case of multiple error callbacks
|
||||||
|
let isCleanupCompleted = false;
|
||||||
|
|
||||||
|
// Set a manual timeout interval to ensure
|
||||||
|
// 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 = () => {
|
// Idempotent function to handle cleanup tasks, we can safely call it multiple times without side effects
|
||||||
socket.close();
|
const cleanup = () => {
|
||||||
|
if (isCleanupCompleted) return;
|
||||||
|
isCleanupCompleted = true;
|
||||||
|
debug("cleaning up resources for %s:%d", host, port);
|
||||||
clearTimeout(timeoutTask);
|
clearTimeout(timeoutTask);
|
||||||
|
socket.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generic error handler
|
// Generic error handler
|
||||||
// This protects multiple error callbacks given the complex socket state
|
socket.on("error", (err) => {
|
||||||
// This is mostly dangerous since it can swallow errors
|
debug("socket error for %s:%d - %s", host, port, err.message);
|
||||||
let didFireError = false;
|
cleanup();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
socket.on("message", (pongPacket) => {
|
||||||
* Handle any error that occurs during the ping process.
|
debug("received %d bytes from %s:%d", pongPacket.length, host, port);
|
||||||
* @param {Error} err The error that occurred.
|
try {
|
||||||
*/
|
const motd = parseUnconnectedPong(pongPacket);
|
||||||
const handleError = (err) => {
|
cleanup();
|
||||||
closeSocket();
|
resolve(motd);
|
||||||
|
} catch (err) {
|
||||||
if (!didFireError) {
|
socket.emit("error", err);
|
||||||
didFireError = true;
|
|
||||||
cb(null, err);
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ping = UNCONNECTED_PING(new Date().getTime() - START_TIME);
|
const pingPacket = createUnconnectedPingFrame(Date.now() - START_TIME);
|
||||||
socket.send(ping, 0, ping.length, port, host);
|
debug("sending Unconnected Ping packet to %s:%d", host, port);
|
||||||
|
debug("packet: %o", pingPacket);
|
||||||
|
socket.send(pingPacket, 0, pingPacket.length, port, host);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err);
|
// Handle any immediate, synchronous errors that might occur when sending the ping packet
|
||||||
}
|
socket.emit("error", err);
|
||||||
|
|
||||||
socket.on('message', (msg) => {
|
|
||||||
const id = msg[0];
|
|
||||||
|
|
||||||
switch (id) {
|
|
||||||
case 0x1c: {
|
|
||||||
const pong = UNCONNECTED_PONG(msg);
|
|
||||||
const clientData = {
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
closeSocket();
|
|
||||||
cb(clientData, null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
handleError(new Error('Received unexpected packet'));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
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/index.js').PingOptions} options The configuration for pinging Minecraft Bedrock server.
|
|
||||||
* @returns {Promise<import('../types/index.js').BedrockPingResponse>}
|
|
||||||
*/
|
|
||||||
export const pingBedrock = (host, options = {}) => {
|
|
||||||
if (!host) throw new Error('Host argument is not provided');
|
|
||||||
|
|
||||||
const { port = 19132, timeout = 5000 } = options;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
ping(host, port, (res, err) => {
|
|
||||||
err ? reject(err) : resolve(res);
|
|
||||||
}, timeout);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
335
lib/java.js
335
lib/java.js
@ -1,136 +1,267 @@
|
|||||||
/**
|
/**
|
||||||
* 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/Java_Edition_protocol/Server_List_Ping
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
import net from 'net';
|
import net from "node:net";
|
||||||
import varint from './varint.js';
|
import { Resolver } from "node:dns/promises";
|
||||||
|
import createDebug from "debug";
|
||||||
|
import * as varint from "./varint.js";
|
||||||
|
|
||||||
const PROTOCOL_VERSION = 0;
|
const debug = createDebug("mineping:java");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ping a Minecraft Java server.
|
* Represents the structured and user-friendly response from a server ping.
|
||||||
* @param {string} host The host of the Java server.
|
* The fields and their optionality are based on the protocol documentation.
|
||||||
* @param {number} [port=25565] The port of the Java server.
|
* See [Status Response Documentation](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Status_Response) for more details.
|
||||||
* @param {function} cb The callback function to handle the ping response.
|
* @typedef {object} JavaPingResponse
|
||||||
* @param {number} [timeout=5000] The timeout duration in milliseconds.
|
* @property {{ name: string, protocol: number }} version - Contains the server's version name and protocol number.
|
||||||
|
* @property {{ max: number, online: number, sample?: Array<{ name: string, id: string }> }} [players] - Player count and a sample of online players.
|
||||||
|
* @property {object | string} [description] - The server's Message of the Day (MOTD).
|
||||||
|
* @property {string} [favicon] - A Base64-encoded 64x64 PNG image data URI.
|
||||||
|
* @property {boolean} [enforcesSecureChat] - True if the server requires clients to have a Mojang-signed public key.
|
||||||
|
* @property {boolean} [preventsChatReports] - True if a mod is installed to disable chat reporting.
|
||||||
*/
|
*/
|
||||||
function ping(host, port = 25565, cb, timeout = 5000) {
|
|
||||||
const socket = net.createConnection(({ host, port }));
|
|
||||||
|
|
||||||
// Set manual timeout interval.
|
/**
|
||||||
// This ensures the connection will NEVER hang regardless of internal state
|
* @typedef {object} JavaPingOptions
|
||||||
|
* @property {number} [port=25565] - The fallback port if an SRV record is not found.
|
||||||
|
* @property {number} [timeout=5000] - The connection timeout in milliseconds.
|
||||||
|
* @property {number} [protocolVersion=-1] - The protocol version to use in the handshake. `-1` is for auto-detection.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Handshake packet.
|
||||||
|
* @param {string} host The hostname to connect to
|
||||||
|
* @param {number} port The port to connect to
|
||||||
|
* @param {number} protocolVersion The protocol version to use
|
||||||
|
* @returns {Buffer} The complete Handshake packet
|
||||||
|
*/
|
||||||
|
function createHandshakePacket(host, port, protocolVersion) {
|
||||||
|
const hostBuffer = varint.encodeString(host);
|
||||||
|
|
||||||
|
const payload = [
|
||||||
|
varint.encodeVarInt(0x00), // Packet ID
|
||||||
|
varint.encodeVarInt(protocolVersion),
|
||||||
|
varint.encodeVarInt(hostBuffer.length),
|
||||||
|
hostBuffer,
|
||||||
|
varint.encodeUShort(port),
|
||||||
|
varint.encodeVarInt(1), // Next state: 1 for Status
|
||||||
|
];
|
||||||
|
|
||||||
|
return varint.concatPackets(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Status Request packet.
|
||||||
|
* @returns {Buffer} The complete Status Request packet
|
||||||
|
*/
|
||||||
|
function createStatusRequestPacket() {
|
||||||
|
const payload = [
|
||||||
|
varint.encodeVarInt(0x00), // Packet ID
|
||||||
|
];
|
||||||
|
return varint.concatPackets(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to parse the server status response from the buffer.
|
||||||
|
* @param {Buffer} buffer The incoming data buffer
|
||||||
|
* @returns {{ response: JavaPingResponse, remainder: Buffer } | null} The parsed response and the remaining buffer, or null if the packet is incomplete
|
||||||
|
*/
|
||||||
|
function processResponse(buffer) {
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packetLengthResult = varint.decodeVarInt(buffer, offset);
|
||||||
|
const packetLength = packetLengthResult.value;
|
||||||
|
offset += packetLengthResult.bytesRead;
|
||||||
|
|
||||||
|
// Check if the full packet has arrived yet.
|
||||||
|
if (buffer.length < offset + packetLength) {
|
||||||
|
debug("packet incomplete, waiting for more data");
|
||||||
|
return null; // Incomplete packet, wait for more data.
|
||||||
|
}
|
||||||
|
|
||||||
|
const packetIdResult = varint.decodeVarInt(buffer, offset);
|
||||||
|
if (packetIdResult.value !== 0x00) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected packet ID: ${packetIdResult.value}. Expected 0x00.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
offset += packetIdResult.bytesRead;
|
||||||
|
|
||||||
|
const jsonLengthResult = varint.decodeVarInt(buffer, offset);
|
||||||
|
const jsonLength = jsonLengthResult.value;
|
||||||
|
offset += jsonLengthResult.bytesRead;
|
||||||
|
|
||||||
|
if (buffer.length < offset + jsonLength) {
|
||||||
|
debug("JSON string incomplete, waiting for more data");
|
||||||
|
return null; // Incomplete JSON string, wait for more data.
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonString = buffer
|
||||||
|
.subarray(offset, offset + jsonLength)
|
||||||
|
.toString("utf8");
|
||||||
|
debug("received raw JSON response");
|
||||||
|
const response = JSON.parse(jsonString);
|
||||||
|
|
||||||
|
// Return the response and any data that came after this packet.
|
||||||
|
const remainder = buffer.subarray(offset + jsonLength);
|
||||||
|
|
||||||
|
return { response, remainder };
|
||||||
|
} catch (err) {
|
||||||
|
// If the buffer is too short for a VarInt, it's a recoverable state.
|
||||||
|
if (err instanceof varint.VarIntError) {
|
||||||
|
if (err.code === varint.ERR_VARINT_BUFFER_UNDERFLOW) {
|
||||||
|
debug("buffer underflow while parsing VarInt, waiting for more data");
|
||||||
|
return null; // Wait for more data.
|
||||||
|
}
|
||||||
|
// For malformed VarInts or JSON, throw the error to reject the promise.
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously Pings a Minecraft Java Edition server.
|
||||||
|
* This function performs an SRV lookup and then attempts to connect and retrieve the server status.
|
||||||
|
* @param {string} host - The server address to ping.
|
||||||
|
* @param {JavaPingOptions} [options={}] - Optional configuration.
|
||||||
|
* @returns {Promise<JavaPingResponse>} A promise that resolves with the server's status.
|
||||||
|
*/
|
||||||
|
export async function pingJava(host, options = {}) {
|
||||||
|
if (typeof host !== "string" || host.trim() === "") {
|
||||||
|
throw new Error("Host argument is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
port: fallbackPort = 25565,
|
||||||
|
timeout = 5000,
|
||||||
|
protocolVersion = -1,
|
||||||
|
} = options;
|
||||||
|
debug("pinging Java server %s with options: %o", host, options);
|
||||||
|
|
||||||
|
let targetHost = host;
|
||||||
|
let targetPort = fallbackPort;
|
||||||
|
|
||||||
|
try {
|
||||||
|
debug(
|
||||||
|
"attempting SRV lookup for _minecraft._tcp.%s with %dms timeout",
|
||||||
|
host,
|
||||||
|
timeout
|
||||||
|
);
|
||||||
|
const resolver = new Resolver({ timeout, tries: 3 });
|
||||||
|
const srvRecords = await resolver.resolveSrv(`_minecraft._tcp.${host}`);
|
||||||
|
if (srvRecords.length > 0) {
|
||||||
|
targetHost = srvRecords[0].name;
|
||||||
|
targetPort = srvRecords[0].port;
|
||||||
|
debug("SRV lookup successful, new target: %s:%d", targetHost, targetPort);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Common errors like ENODATA, ENOTFOUND, or a DNS timeout (ETIMEOUT) are expected
|
||||||
|
// when a server does not have an SRV record, so we ignore them and proceed.
|
||||||
|
const nonFatalDnsCodes = ["ENODATA", "ENOTFOUND", "ETIMEOUT"];
|
||||||
|
if (
|
||||||
|
err instanceof Error &&
|
||||||
|
"code" in err &&
|
||||||
|
nonFatalDnsCodes.includes(err.code)
|
||||||
|
) {
|
||||||
|
debug("SRV lookup for %s failed (%s), using fallback.", host, err.code);
|
||||||
|
} else {
|
||||||
|
// Re-throw anything else to fail the operation
|
||||||
|
debug("SRV lookup for %s failed unexpectedly, re-throwing.", host, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
debug("creating TCP connection to %s:%d", targetHost, targetPort);
|
||||||
|
const socket = net.createConnection({ host: targetHost, port: targetPort });
|
||||||
|
|
||||||
|
// Prevent cleanup tasks from running more than once
|
||||||
|
// in case of multiple error callbacks
|
||||||
|
let isCleanupCompleted = false;
|
||||||
|
|
||||||
|
// Set a manual timeout interval to ensure
|
||||||
|
// 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 = () => {
|
// Idempotent function to handle cleanup tasks, we can safely call it multiple times without side effects
|
||||||
socket.destroy();
|
const cleanup = () => {
|
||||||
|
if (isCleanupCompleted) return;
|
||||||
|
isCleanupCompleted = true;
|
||||||
|
debug("cleaning up resources for %s:%d", targetHost, targetPort);
|
||||||
clearTimeout(timeoutTask);
|
clearTimeout(timeoutTask);
|
||||||
};
|
socket.destroy();
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
if (!didFireError) {
|
|
||||||
didFireError = true;
|
|
||||||
cb(null, err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// #setNoDelay instantly flushes data during read/writes
|
// #setNoDelay instantly flushes data during read/writes
|
||||||
// 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', () => {
|
// Generic error handler
|
||||||
const handshake = varint.concat([
|
socket.on("error", (err) => {
|
||||||
varint.encodeInt(0),
|
debug("socket error for %s:%d - %s", targetHost, targetPort, err.message);
|
||||||
varint.encodeInt(PROTOCOL_VERSION),
|
cleanup();
|
||||||
varint.encodeInt(host.length),
|
reject(err);
|
||||||
varint.encodeString(host),
|
});
|
||||||
varint.encodeUShort(port),
|
|
||||||
varint.encodeInt(1)
|
|
||||||
]);
|
|
||||||
|
|
||||||
socket.write(handshake);
|
socket.on("close", () => {
|
||||||
|
if (!isCleanupCompleted) {
|
||||||
|
debug("socket for %s:%d closed prematurely", targetHost, targetPort);
|
||||||
|
cleanup();
|
||||||
|
reject(new Error("Socket closed unexpectedly without a response."));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const request = varint.concat([
|
socket.on("connect", () => {
|
||||||
varint.encodeInt(0)
|
debug(
|
||||||
]);
|
"socket connected to %s:%d, sending packets...",
|
||||||
|
targetHost,
|
||||||
socket.write(request);
|
targetPort
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const handshakePacket = createHandshakePacket(
|
||||||
|
host,
|
||||||
|
targetPort,
|
||||||
|
protocolVersion
|
||||||
|
);
|
||||||
|
const statusRequestPacket = createStatusRequestPacket();
|
||||||
|
socket.write(handshakePacket);
|
||||||
|
socket.write(statusRequestPacket);
|
||||||
|
} catch (err) {
|
||||||
|
// Handle synchronous errors during packet creation/writing
|
||||||
|
socket.emit("error", err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let incomingBuffer = Buffer.alloc(0);
|
let incomingBuffer = Buffer.alloc(0);
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
socket.on("data", (data) => {
|
||||||
|
debug(
|
||||||
|
"received %d bytes of data, total buffer size is now %d bytes",
|
||||||
|
data.length,
|
||||||
|
incomingBuffer.length + data.length
|
||||||
|
);
|
||||||
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
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let offset = 0;
|
|
||||||
const packetLength = varint.decodeInt(incomingBuffer, offset);
|
|
||||||
|
|
||||||
// Ensure incomingBuffer contains the full response
|
|
||||||
if (incomingBuffer.length - offset < packetLength) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(response);
|
const result = processResponse(incomingBuffer);
|
||||||
|
if (result) {
|
||||||
closeSocket();
|
debug("successfully parsed full response");
|
||||||
cb(message, null);
|
// We successfully parsed a response. Clean up before resolving.
|
||||||
|
cleanup();
|
||||||
|
resolve(result.response);
|
||||||
|
}
|
||||||
|
// If result is null, we just wait for more data to arrive.
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err);
|
socket.emit("error", err);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handleError(new Error('Received unexpected packet'));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', handleError);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<import('../types/index.js').JavaPingResponse>}
|
|
||||||
*/
|
|
||||||
export function pingJava(host, options = {}) {
|
|
||||||
if (!host) throw new Error('Host argument is not provided');
|
|
||||||
|
|
||||||
const { port = 25565, timeout = 5000 } = options;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
ping(host, port, (res, err) => {
|
|
||||||
err ? reject(err) : resolve(res);
|
|
||||||
}, timeout);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
227
lib/varint.js
227
lib/varint.js
@ -1,22 +1,37 @@
|
|||||||
// https://wiki.vg/Data_types
|
// https://minecraft.wiki/w/Java_Edition_protocol/Data_types
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
export const ERR_VARINT_BUFFER_UNDERFLOW = "VARINT_BUFFER_UNDERFLOW";
|
||||||
|
export const ERR_VARINT_MALFORMED = "VARINT_MALFORMED";
|
||||||
|
export const ERR_VARINT_ENCODE_TOO_LARGE = "VARINT_ENCODE_TOO_LARGE";
|
||||||
|
|
||||||
|
export class VarIntError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {string} message The error message.
|
||||||
|
* @param {string} code The error code.
|
||||||
|
*/
|
||||||
|
constructor(message, code) {
|
||||||
|
super(message);
|
||||||
|
this.name = "VarIntError";
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility object for encoding and decoding varints.
|
* Encodes an integer into a VarInt buffer.
|
||||||
|
* VarInts are never longer than 5 bytes for the Minecraft protocol.
|
||||||
|
* @param {number} value The integer to encode
|
||||||
|
* @returns {Buffer} The encoded VarInt as a buffer
|
||||||
|
* @throws {VarIntError} if the value is too large to be encoded
|
||||||
*/
|
*/
|
||||||
const varint = {
|
export function encodeVarInt(value) {
|
||||||
/**
|
|
||||||
* 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);
|
const buf = Buffer.alloc(5);
|
||||||
let written = 0;
|
let written = 0;
|
||||||
|
let val = value;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const byte = val & 0x7F;
|
const byte = val & 0x7f;
|
||||||
val >>>= 7;
|
val >>>= 7;
|
||||||
|
|
||||||
if (val === 0) {
|
if (val === 0) {
|
||||||
@ -25,103 +40,99 @@ const varint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf.writeUInt8(byte | 0x80, written++);
|
buf.writeUInt8(byte | 0x80, written++);
|
||||||
}
|
|
||||||
|
|
||||||
return buf.slice(0, written);
|
if (written >= 5 && val > 0) {
|
||||||
},
|
throw new VarIntError(
|
||||||
|
"Value too large for a 5-byte VarInt",
|
||||||
/**
|
ERR_VARINT_ENCODE_TOO_LARGE
|
||||||
* 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;
|
|
||||||
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
length += chunk.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = [
|
|
||||||
varint.encodeInt(length),
|
|
||||||
...chunks
|
|
||||||
];
|
|
||||||
|
|
||||||
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 byte = buffer.readUInt8(offset++);
|
|
||||||
|
|
||||||
val |= (byte & 0x7F) << count++ * 7;
|
|
||||||
|
|
||||||
if ((byte & 0x80) !== 0x80) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 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;
|
return buf.subarray(0, written);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a string into a UTF-8 buffer.
|
||||||
|
* @param {string} value The string to encode
|
||||||
|
* @returns {Buffer}
|
||||||
|
*/
|
||||||
|
export function encodeString(value) {
|
||||||
|
return Buffer.from(value, "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes an unsigned short (16-bit big-endian) into a 2-byte buffer.
|
||||||
|
* @param {number} value The number to encode
|
||||||
|
* @returns {Buffer}
|
||||||
|
*/
|
||||||
|
export function encodeUShort(value) {
|
||||||
|
const buf = Buffer.alloc(2);
|
||||||
|
buf.writeUInt16BE(value, 0);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Minecraft-style packet by concatenating chunks and prefixing the total length as a VarInt.
|
||||||
|
* @param {Buffer[]} chunks An array of buffers to include in the packet payload
|
||||||
|
* @returns {Buffer} The complete packet with its length prefix
|
||||||
|
*/
|
||||||
|
export function concatPackets(chunks) {
|
||||||
|
const payload = Buffer.concat(chunks);
|
||||||
|
const lengthPrefix = encodeVarInt(payload.length);
|
||||||
|
return Buffer.concat([lengthPrefix, payload]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a VarInt from a buffer.
|
||||||
|
* Returns the decoded value and the number of bytes it consumed.
|
||||||
|
* @param {Buffer} buffer The buffer to read from
|
||||||
|
* @param {number} [offset=0] The starting offset in the buffer
|
||||||
|
* @returns {{ value: number, bytesRead: number }}
|
||||||
|
* @throws {VarIntError} if the buffer is too short or the VarInt is malformed
|
||||||
|
*/
|
||||||
|
export function decodeVarInt(buffer, offset = 0) {
|
||||||
|
if (offset >= buffer.length) {
|
||||||
|
throw new VarIntError(
|
||||||
|
"Buffer underflow: Cannot decode VarInt at or beyond buffer length.",
|
||||||
|
ERR_VARINT_BUFFER_UNDERFLOW
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast path for single-byte VarInts, which are very common.
|
||||||
|
const firstByte = buffer.readUInt8(offset);
|
||||||
|
if ((firstByte & 0x80) === 0) {
|
||||||
|
return { value: firstByte, bytesRead: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
let val = firstByte & 0x7f; // Get the first 7 bits
|
||||||
|
let position = 7; // Bit position for the next byte's data
|
||||||
|
let bytesRead = 1; // We've read one byte so far
|
||||||
|
let currentOffset = offset + 1; // Start reading from the next
|
||||||
|
|
||||||
|
// Max 4 more bytes (total 5 bytes for a VarInt)
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (currentOffset >= buffer.length) {
|
||||||
|
throw new VarIntError(
|
||||||
|
"Buffer underflow: Incomplete VarInt, expected more bytes.",
|
||||||
|
ERR_VARINT_BUFFER_UNDERFLOW
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byte = buffer.readUInt8(currentOffset);
|
||||||
|
bytesRead++;
|
||||||
|
currentOffset++;
|
||||||
|
|
||||||
|
val |= (byte & 0x7f) << position;
|
||||||
|
position += 7;
|
||||||
|
|
||||||
|
if ((byte & 0x80) === 0) {
|
||||||
|
return { value: val, bytesRead: bytesRead };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new VarIntError(
|
||||||
|
"VarInt is too big or malformed: 5 bytes read with continuation bit still set.",
|
||||||
|
ERR_VARINT_MALFORMED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
1551
package-lock.json
generated
Normal file
1551
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@ -1,22 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "@minescope/mineping",
|
"name": "@minescope/mineping",
|
||||||
"version": "1.1.1",
|
"version": "1.7.0-beta.3",
|
||||||
"description": "Ping both Minecraft Bedrock and Java servers.",
|
"description": "Ping both Minecraft Bedrock and Java servers.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
"types": "types/index.d.ts",
|
"types": "types/index.d.ts",
|
||||||
"keywords": [],
|
"scripts": {
|
||||||
"author": {
|
"test": "vitest run",
|
||||||
"name": "Timofey (xzeldon)",
|
"test:watch": "vitest",
|
||||||
"email": "contact@zeldon.ru",
|
"types:check": "tsc --noEmit",
|
||||||
"url": "https://zeldon.ru"
|
"types:build": "tsc"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/minescope/mineping.git"
|
"url": "git://github.com/minescope/mineping.git"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"minecraft",
|
||||||
|
"raknet",
|
||||||
|
"node",
|
||||||
|
"mcpe",
|
||||||
|
"mcbe",
|
||||||
|
"ping",
|
||||||
|
"bedrock"
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"name": "Timofey Gelazoniya",
|
||||||
|
"email": "timofey@z4n.me",
|
||||||
|
"url": "https://zeldon.ru"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
},
|
},
|
||||||
"license": "MIT"
|
"dependencies": {
|
||||||
|
"debug": "^4.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
|
"@types/node": "^24.0.3",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vitest": "^3.2.3"
|
||||||
|
}
|
||||||
}
|
}
|
174
test/bedrock.test.js
Normal file
174
test/bedrock.test.js
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import dgram from "node:dgram";
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import { pingBedrock } from "../lib/bedrock.js";
|
||||||
|
|
||||||
|
vi.mock("node:dgram");
|
||||||
|
|
||||||
|
describe("bedrock.js", () => {
|
||||||
|
let mockSocket;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// A store for event handlers, closed over by the mockSocket.
|
||||||
|
const handlers = {};
|
||||||
|
|
||||||
|
// Create a stateful mock socket to simulate EventEmitter.
|
||||||
|
mockSocket = {
|
||||||
|
send: vi.fn(),
|
||||||
|
close: vi.fn(),
|
||||||
|
on: vi.fn((event, handler) => {
|
||||||
|
handlers[event] = handler;
|
||||||
|
}),
|
||||||
|
emit: vi.fn((event, ...args) => {
|
||||||
|
if (handlers[event]) {
|
||||||
|
handlers[event](...args);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
dgram.createSocket = vi.fn().mockReturnValue(mockSocket);
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ping a 3rd party server and parse MOTD", async () => {
|
||||||
|
const host = "play.example.com";
|
||||||
|
const options = { port: 25565, timeout: 10000 };
|
||||||
|
const pingPromise = pingBedrock(host, options);
|
||||||
|
|
||||||
|
const motd =
|
||||||
|
"MCPE;§l§bOasys§fPE §eГриф§7, §cДуэли§7, §aКейсы;0;1337;1070;1999;-138584171542148188;oasys-pe.ru;Adventure;1";
|
||||||
|
const mockPongPacket = createMockPongPacket(motd);
|
||||||
|
|
||||||
|
mockSocket.emit("message", mockPongPacket);
|
||||||
|
|
||||||
|
const result = await pingPromise;
|
||||||
|
|
||||||
|
expect(dgram.createSocket).toHaveBeenCalledWith("udp4");
|
||||||
|
expect(mockSocket.send).toHaveBeenCalledWith(
|
||||||
|
expect.any(Buffer),
|
||||||
|
0,
|
||||||
|
33,
|
||||||
|
options.port,
|
||||||
|
host
|
||||||
|
);
|
||||||
|
expect(mockSocket.close).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
edition: "MCPE",
|
||||||
|
name: "§l§bOasys§fPE §eГриф§7, §cДуэли§7, §aКейсы",
|
||||||
|
levelName: "oasys-pe.ru",
|
||||||
|
gamemode: "Adventure",
|
||||||
|
version: {
|
||||||
|
protocol: 0,
|
||||||
|
minecraft: "1337",
|
||||||
|
},
|
||||||
|
players: {
|
||||||
|
online: 1070,
|
||||||
|
max: 1999,
|
||||||
|
},
|
||||||
|
port: {
|
||||||
|
v4: undefined,
|
||||||
|
v6: undefined,
|
||||||
|
},
|
||||||
|
guid: -138584171542148188n,
|
||||||
|
isNintendoLimited: false,
|
||||||
|
isEditorModeEnabled: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ping a BDS server with default `server.properties` and parse MOTD", async () => {
|
||||||
|
const host = "play.example.com";
|
||||||
|
const options = { port: 25565, timeout: 10000 };
|
||||||
|
const pingPromise = pingBedrock(host, options);
|
||||||
|
|
||||||
|
const motd =
|
||||||
|
"MCPE;Dedicated Server;800;1.21.84;0;10;11546321190880321782;Bedrock level;Survival;1;19132;19133;0;";
|
||||||
|
const mockPongPacket = createMockPongPacket(motd);
|
||||||
|
|
||||||
|
mockSocket.emit("message", mockPongPacket);
|
||||||
|
|
||||||
|
const result = await pingPromise;
|
||||||
|
|
||||||
|
expect(dgram.createSocket).toHaveBeenCalledWith("udp4");
|
||||||
|
expect(mockSocket.send).toHaveBeenCalledWith(
|
||||||
|
expect.any(Buffer),
|
||||||
|
0,
|
||||||
|
33,
|
||||||
|
options.port,
|
||||||
|
host
|
||||||
|
);
|
||||||
|
expect(mockSocket.close).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
edition: "MCPE",
|
||||||
|
name: "Dedicated Server",
|
||||||
|
levelName: "Bedrock level",
|
||||||
|
gamemode: "Survival",
|
||||||
|
version: {
|
||||||
|
protocol: 800,
|
||||||
|
minecraft: "1.21.84",
|
||||||
|
},
|
||||||
|
players: {
|
||||||
|
online: 0,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
|
port: {
|
||||||
|
v4: 19132,
|
||||||
|
v6: 19133,
|
||||||
|
},
|
||||||
|
guid: 11546321190880321782n,
|
||||||
|
isNintendoLimited: false,
|
||||||
|
isEditorModeEnabled: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("errors", () => {
|
||||||
|
it("should throw an error if host is not provided", () => {
|
||||||
|
expect(() => pingBedrock(null)).toThrow("Host argument is required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject on socket timeout", async () => {
|
||||||
|
const pingPromise = pingBedrock("play.example.com", { timeout: 1000 });
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
|
||||||
|
await expect(pingPromise).rejects.toThrow("Socket timeout");
|
||||||
|
expect(mockSocket.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject on a generic socket error", async () => {
|
||||||
|
const pingPromise = pingBedrock("play.example.com");
|
||||||
|
|
||||||
|
// Simulate a DNS or network error by emitting it.
|
||||||
|
mockSocket.emit("error", new Error("EHOSTUNREACH"));
|
||||||
|
|
||||||
|
await expect(pingPromise).rejects.toThrow("EHOSTUNREACH");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only reject once, even if multiple errors occur", async () => {
|
||||||
|
const pingPromise = pingBedrock("play.example.com");
|
||||||
|
|
||||||
|
// Fire a socket error first.
|
||||||
|
mockSocket.emit("error", new Error("First error"));
|
||||||
|
|
||||||
|
// Then, try to trigger another error by sending a bad message.
|
||||||
|
mockSocket.emit("message", Buffer.alloc(0));
|
||||||
|
|
||||||
|
await expect(pingPromise).rejects.toThrow("First error");
|
||||||
|
expect(mockSocket.close).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createMockPongPacket(motd) {
|
||||||
|
const motdBuffer = Buffer.from(motd, "utf-8");
|
||||||
|
const packet = Buffer.alloc(35 + motdBuffer.length);
|
||||||
|
packet.writeUInt8(0x1c, 0);
|
||||||
|
packet.writeBigInt64LE(BigInt(Date.now()), 1);
|
||||||
|
Buffer.from("00ffff00fefefefefdfdfdfd12345678", "hex").copy(packet, 17);
|
||||||
|
packet.writeUInt16BE(motdBuffer.length, 33);
|
||||||
|
motdBuffer.copy(packet, 35);
|
||||||
|
return packet;
|
||||||
|
}
|
133
test/java.test.js
Normal file
133
test/java.test.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import net from "node:net";
|
||||||
|
import dns from "node:dns/promises";
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import { pingJava } from "../lib/java.js";
|
||||||
|
import * as varint from "../lib/varint.js";
|
||||||
|
|
||||||
|
vi.mock("node:net");
|
||||||
|
vi.mock("node:dns/promises");
|
||||||
|
|
||||||
|
describe("pingJava", () => {
|
||||||
|
let mockSocket;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Simulate no SRV record found.
|
||||||
|
dns.resolveSrv.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const mockHandlers = {};
|
||||||
|
mockSocket = {
|
||||||
|
write: vi.fn(),
|
||||||
|
// Make `destroy` emit 'error' if an error is passed.
|
||||||
|
destroy: vi.fn((err) => {
|
||||||
|
if (err) {
|
||||||
|
mockSocket.emit("error", err);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
setNoDelay: vi.fn(),
|
||||||
|
on: vi.fn((event, handler) => (mockHandlers[event] = handler)),
|
||||||
|
emit: vi.fn((event, ...args) => mockHandlers[event]?.(...args)),
|
||||||
|
};
|
||||||
|
net.createConnection.mockReturnValue(mockSocket);
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ping a server and handle a chunked response", async () => {
|
||||||
|
const host = "mc.hypixel.net";
|
||||||
|
const options = { port: 25565 };
|
||||||
|
const pingPromise = pingJava(host, options);
|
||||||
|
|
||||||
|
// Allow the async SRV lookup to complete
|
||||||
|
await vi.runAllTicks();
|
||||||
|
|
||||||
|
expect(dns.resolveSrv).toHaveBeenCalledWith(`_minecraft._tcp.${host}`);
|
||||||
|
expect(net.createConnection).toHaveBeenCalledWith({
|
||||||
|
host,
|
||||||
|
port: options.port,
|
||||||
|
});
|
||||||
|
|
||||||
|
mockSocket.emit("connect");
|
||||||
|
expect(mockSocket.write).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
version: { name: "1.21", protocol: 765 },
|
||||||
|
players: { max: 20, online: 5, sample: [] },
|
||||||
|
description: "A Minecraft Server",
|
||||||
|
};
|
||||||
|
|
||||||
|
const fullPacket = createMockJavaResponse(mockResponse);
|
||||||
|
const chunk1 = fullPacket.subarray(0, 10);
|
||||||
|
const chunk2 = fullPacket.subarray(10);
|
||||||
|
|
||||||
|
// Simulate receiving data in chunks
|
||||||
|
mockSocket.emit("data", chunk1);
|
||||||
|
mockSocket.emit("data", chunk2);
|
||||||
|
|
||||||
|
const result = await pingPromise;
|
||||||
|
expect(result).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("errors", () => {
|
||||||
|
it("should throw an error if host is not provided", async () => {
|
||||||
|
await expect(pingJava(null)).rejects.toThrow("Host argument is required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject on socket timeout", async () => {
|
||||||
|
const pingPromise = pingJava("localhost", { timeout: 1000 });
|
||||||
|
await vi.runAllTicks();
|
||||||
|
mockSocket.emit("connect");
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
await expect(pingPromise).rejects.toThrow("Socket timeout");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject on connection error", async () => {
|
||||||
|
const pingPromise = pingJava("localhost");
|
||||||
|
await vi.runAllTicks();
|
||||||
|
mockSocket.emit("error", new Error("ECONNREFUSED"));
|
||||||
|
await expect(pingPromise).rejects.toThrow("ECONNREFUSED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject if the socket closes prematurely without a response", async () => {
|
||||||
|
const pingPromise = pingJava("localhost");
|
||||||
|
|
||||||
|
// Allow the initial async operations to complete
|
||||||
|
await vi.runAllTicks();
|
||||||
|
|
||||||
|
// Simulate the server accepting the connection and then immediately closing it
|
||||||
|
mockSocket.emit("connect");
|
||||||
|
mockSocket.emit("close");
|
||||||
|
|
||||||
|
// The promise should reject with our specific 'close' handler message
|
||||||
|
await expect(pingPromise).rejects.toThrow(
|
||||||
|
"Socket closed unexpectedly without a response."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only reject once, even if multiple errors occur", async () => {
|
||||||
|
const pingPromise = pingJava("localhost");
|
||||||
|
await vi.runAllTicks();
|
||||||
|
mockSocket.emit("error", new Error("First error"));
|
||||||
|
mockSocket.emit("error", new Error("Second error")); // Should be ignored
|
||||||
|
await expect(pingPromise).rejects.toThrow("First error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock Java status response packet according to the protocol.
|
||||||
|
* Structure: [Overall Length] [Packet ID] [JSON Length] [JSON String]
|
||||||
|
* @param {object} response The JSON response object
|
||||||
|
* @returns {Buffer}
|
||||||
|
*/
|
||||||
|
function createMockJavaResponse(response) {
|
||||||
|
const jsonString = JSON.stringify(response);
|
||||||
|
const jsonBuffer = varint.encodeString(jsonString);
|
||||||
|
const jsonLength = varint.encodeVarInt(jsonBuffer.length);
|
||||||
|
const packetId = varint.encodeVarInt(0x00);
|
||||||
|
const payloadParts = [packetId, jsonLength, jsonBuffer];
|
||||||
|
return varint.concatPackets(payloadParts);
|
||||||
|
}
|
61
test/varint.test.js
Normal file
61
test/varint.test.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import * as varint from "../lib/varint.js";
|
||||||
|
|
||||||
|
describe("varint.js", () => {
|
||||||
|
it("should encode and decode integers symmetrically (round-trip)", () => {
|
||||||
|
const testValues = [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
127, // Max 1-byte
|
||||||
|
128, // Min 2-byte
|
||||||
|
255,
|
||||||
|
16383, // Max 2-byte
|
||||||
|
16384, // Min 3-byte
|
||||||
|
2147483647, // Max signed 32-bit int
|
||||||
|
-1, // Critical edge case (encodes as max unsigned int)
|
||||||
|
];
|
||||||
|
|
||||||
|
testValues.forEach((value) => {
|
||||||
|
const encoded = varint.encodeVarInt(value);
|
||||||
|
const { value: decoded } = varint.decodeVarInt(encoded, 0);
|
||||||
|
expect(decoded, `Value ${value} failed round-trip`).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should decode an integer from a non-zero offset", () => {
|
||||||
|
// [255 (invalid varint), 128 (valid varint), 127 (valid varint)]
|
||||||
|
const buffer = Buffer.from([0xff, 0x80, 0x01, 0x7f]);
|
||||||
|
const { value: decoded } = varint.decodeVarInt(buffer, 1);
|
||||||
|
expect(decoded).toBe(128);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error for a malformed varint that is too long", () => {
|
||||||
|
const invalidBuffer = Buffer.from([0x80, 0x80, 0x80, 0x80, 0x80, 0x80]);
|
||||||
|
expect(() => varint.decodeVarInt(invalidBuffer, 0)).toThrow(
|
||||||
|
"VarInt is too big or malformed"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should encode 16-bit unsigned shorts in big-endian format", () => {
|
||||||
|
expect(varint.encodeUShort(0)).toEqual(Buffer.from([0x00, 0x00]));
|
||||||
|
expect(varint.encodeUShort(256)).toEqual(Buffer.from([0x01, 0x00]));
|
||||||
|
expect(varint.encodeUShort(65535)).toEqual(Buffer.from([0xff, 0xff]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly assemble a Minecraft packet with a length prefix", () => {
|
||||||
|
const payloadParts = [
|
||||||
|
varint.encodeVarInt(0), // protocol
|
||||||
|
varint.encodeString("mc.example.com"), // host
|
||||||
|
varint.encodeUShort(25565), // port
|
||||||
|
];
|
||||||
|
const payload = Buffer.concat(payloadParts);
|
||||||
|
const finalPacket = varint.concatPackets(payloadParts);
|
||||||
|
const { value: decodedPacketLength, bytesRead } = varint.decodeVarInt(
|
||||||
|
finalPacket,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
expect(decodedPacketLength).toBe(payload.length);
|
||||||
|
const decodedPayload = finalPacket.subarray(bytesRead);
|
||||||
|
expect(decodedPayload).toEqual(payload);
|
||||||
|
});
|
||||||
|
});
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"include": ["lib/**/*.js", "index.js"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "types",
|
||||||
|
"removeComments": false
|
||||||
|
}
|
||||||
|
}
|
4
types/index.d.ts
vendored
4
types/index.d.ts
vendored
@ -1,2 +1,2 @@
|
|||||||
export * from "./lib/java.js";
|
export { pingJava } from "./lib/java.js";
|
||||||
export * from "./lib/bedrock.js";
|
export { pingBedrock } from "./lib/bedrock.js";
|
||||||
|
172
types/lib/bedrock.d.ts
vendored
172
types/lib/bedrock.d.ts
vendored
@ -1,49 +1,131 @@
|
|||||||
/**
|
/**
|
||||||
* @param port The server port.
|
* Asynchronously pings a Minecraft Bedrock server.
|
||||||
* @param timeout The read/write socket timeout.
|
* @param {string} host - The IP address or hostname of the server.
|
||||||
|
* @param {BedrockPingOptions} [options={}] - Optional configuration.
|
||||||
|
* @returns {Promise<BedrockPingResponse>} A promise that resolves with the server's parsed MOTD.
|
||||||
|
*/
|
||||||
|
export function pingBedrock(host: string, options?: BedrockPingOptions): Promise<BedrockPingResponse>;
|
||||||
|
/**
|
||||||
|
* Representation of raw, semicolon-delimited MOTD string.
|
||||||
|
* This struct directly mirrors the fields and order from the server response.
|
||||||
|
* See [`Unconnected Pong Documentation`](https://minecraft.wiki/w/RakNet#Unconnected_Pong) for more details.
|
||||||
|
*/
|
||||||
|
export type BedrockMotd = {
|
||||||
|
/**
|
||||||
|
* - The edition of the server (MCPE or MCEE).
|
||||||
|
*/
|
||||||
|
edition: string;
|
||||||
|
/**
|
||||||
|
* - The primary name of the server (first line of MOTD).
|
||||||
*/
|
*/
|
||||||
export type PingOptions = {
|
|
||||||
port: number,
|
|
||||||
timeout: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BedrockPingResponse = {
|
|
||||||
version: {
|
|
||||||
name: string;
|
name: string;
|
||||||
protocol: string;
|
/**
|
||||||
};
|
* - The protocol version.
|
||||||
players: {
|
|
||||||
max: string;
|
|
||||||
online: string;
|
|
||||||
};
|
|
||||||
description: string;
|
|
||||||
gamemode: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { pingBedrock } from '@minescope/mineping';
|
|
||||||
*
|
|
||||||
* const data = await pingBedrock('mco.mineplex.com');
|
|
||||||
* console.log(data);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The resulting output will resemble:
|
|
||||||
* ```console
|
|
||||||
* {
|
|
||||||
* version: { name: 'Mineplex', protocol: '475' },
|
|
||||||
* players: { max: '5207', online: '5206' },
|
|
||||||
* description: ' New Costumes',
|
|
||||||
* gamemode: 'Survival'
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @see [source](https://github.com/minescope/mineping/blob/915edbec9c9ad811459458600af3531ec0836911/lib/bedrock.js#L204)
|
|
||||||
*/
|
*/
|
||||||
export function pingBedrock(host: string, options?: PingOptions): Promise<BedrockPingResponse>;
|
protocol: number;
|
||||||
|
/**
|
||||||
|
* - The game version (e.g., "1.21.2").
|
||||||
|
*/
|
||||||
|
version: string;
|
||||||
|
/**
|
||||||
|
* - The current number of players online.
|
||||||
|
*/
|
||||||
|
playerCount: number;
|
||||||
|
/**
|
||||||
|
* - The maximum number of players allowed.
|
||||||
|
*/
|
||||||
|
playerMax: number;
|
||||||
|
/**
|
||||||
|
* - The server's GUID.
|
||||||
|
*/
|
||||||
|
serverGuid: bigint;
|
||||||
|
/**
|
||||||
|
* - The secondary name of the server (second line of MOTD).
|
||||||
|
*/
|
||||||
|
subName: string;
|
||||||
|
/**
|
||||||
|
* - The default gamemode (e.g., "Survival").
|
||||||
|
*/
|
||||||
|
gamemode: string;
|
||||||
|
/**
|
||||||
|
* - Whether the server is Nintendo limited.
|
||||||
|
*/
|
||||||
|
nintendoLimited?: boolean;
|
||||||
|
/**
|
||||||
|
* - The server's IPv4 port, if provided.
|
||||||
|
*/
|
||||||
|
port?: number;
|
||||||
|
/**
|
||||||
|
* - The server's IPv6 port, if provided.
|
||||||
|
*/
|
||||||
|
ipv6Port?: number;
|
||||||
|
/**
|
||||||
|
* - Whether the server is in editor mode, if provided. See [Minecraft Editor Mode Documentation](https://learn.microsoft.com/en-us/minecraft/creator/documents/bedrockeditor/editoroverview?view=minecraft-bedrock-stable) for more details.
|
||||||
|
*/
|
||||||
|
editorMode?: boolean;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Represents the structured and user-friendly response from a server ping.
|
||||||
|
* This is the public-facing object that users of the library will receive.
|
||||||
|
*/
|
||||||
|
export type BedrockPingResponse = {
|
||||||
|
/**
|
||||||
|
* - The edition of the server (MCPE or MCEE).
|
||||||
|
*/
|
||||||
|
edition: string;
|
||||||
|
/**
|
||||||
|
* - The primary name of the server (first line of MOTD).
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* - The name of the world or level being hosted.
|
||||||
|
*/
|
||||||
|
levelName: string;
|
||||||
|
/**
|
||||||
|
* - The default gamemode of the server.
|
||||||
|
*/
|
||||||
|
gamemode: string;
|
||||||
|
/**
|
||||||
|
* - Game and protocol versions.
|
||||||
|
*/
|
||||||
|
version: {
|
||||||
|
protocol: number;
|
||||||
|
minecraft: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* - Current and maximum player counts.
|
||||||
|
*/
|
||||||
|
players: {
|
||||||
|
online: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* - Announced IPv4 and IPv6 ports.
|
||||||
|
*/
|
||||||
|
port: {
|
||||||
|
v4?: number;
|
||||||
|
v6?: number;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* - The server's unique 64-bit GUID.
|
||||||
|
*/
|
||||||
|
guid: bigint;
|
||||||
|
/**
|
||||||
|
* - True if the server restricts Nintendo Switch players.
|
||||||
|
*/
|
||||||
|
isNintendoLimited?: boolean;
|
||||||
|
/**
|
||||||
|
* - True if the server is in editor mode. See [Minecraft Editor Mode Documentation](https://learn.microsoft.com/en-us/minecraft/creator/documents/bedrockeditor/editoroverview?view=minecraft-bedrock-stable) for more details.
|
||||||
|
*/
|
||||||
|
isEditorModeEnabled?: boolean;
|
||||||
|
};
|
||||||
|
export type BedrockPingOptions = {
|
||||||
|
/**
|
||||||
|
* - The server port to ping.
|
||||||
|
*/
|
||||||
|
port?: number;
|
||||||
|
/**
|
||||||
|
* - The timeout in milliseconds for the request.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
108
types/lib/java.d.ts
vendored
108
types/lib/java.d.ts
vendored
@ -1,71 +1,63 @@
|
|||||||
import { PingOptions } from "./bedrock";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON format chat component used for description field.
|
* Asynchronously Pings a Minecraft Java Edition server.
|
||||||
* @see https://wiki.vg/Chat
|
* This function performs an SRV lookup and then attempts to connect and retrieve the server status.
|
||||||
|
* @param {string} host - The server address to ping.
|
||||||
|
* @param {JavaPingOptions} [options={}] - Optional configuration.
|
||||||
|
* @returns {Promise<JavaPingResponse>} A promise that resolves with the server's status.
|
||||||
*/
|
*/
|
||||||
export type ChatComponent = {
|
export function pingJava(host: string, options?: JavaPingOptions): Promise<JavaPingResponse>;
|
||||||
text: string;
|
|
||||||
bold?: boolean;
|
|
||||||
italic?: boolean;
|
|
||||||
underlined?: boolean;
|
|
||||||
strikethrough?: boolean;
|
|
||||||
obfuscated?: boolean;
|
|
||||||
color?: string;
|
|
||||||
extra?: ChatComponent[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SampleProp = {
|
|
||||||
name: string,
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `JSON Response` field of Response packet.
|
* Represents the structured and user-friendly response from a server ping.
|
||||||
* @see https://wiki.vg/Server_List_Ping#Response
|
* The fields and their optionality are based on the protocol documentation.
|
||||||
|
* See [Status Response Documentation](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Status_Response) for more details.
|
||||||
*/
|
*/
|
||||||
export type JavaPingResponse = {
|
export type JavaPingResponse = {
|
||||||
|
/**
|
||||||
|
* - Contains the server's version name and protocol number.
|
||||||
|
*/
|
||||||
version: {
|
version: {
|
||||||
name: string;
|
name: string;
|
||||||
protocol: number;
|
protocol: number;
|
||||||
};
|
};
|
||||||
players: {
|
/**
|
||||||
|
* - Player count and a sample of online players.
|
||||||
|
*/
|
||||||
|
players?: {
|
||||||
max: number;
|
max: number;
|
||||||
online: number;
|
online: number;
|
||||||
sample?: SampleProp[];
|
sample?: Array<{
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
description: string | ChatComponent;
|
/**
|
||||||
favicon?: string;
|
* - The server's Message of the Day (MOTD).
|
||||||
enforcesSecureChat?: boolean;
|
|
||||||
previewsChat?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 host The Java server address.
|
|
||||||
* @param options The configuration for pinging Minecraft Java server.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { pingJava } from '@minescope/mineping';
|
|
||||||
*
|
|
||||||
* const data = await pingJava('mc.hypixel.net');
|
|
||||||
* console.log(data);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The resulting output will resemble:
|
|
||||||
* ```console
|
|
||||||
* {
|
|
||||||
* version: { name: 'Requires MC 1.8 / 1.18', protocol: 47 },
|
|
||||||
* players: { max: 200000, online: 67336, sample: [] },
|
|
||||||
* description: ' §f☃ §aHypixel Network §eTRIPLE COINS & EXP §f☃\n' +
|
|
||||||
* ' §6✰ §f§lHOLIDAY SALE §c§lUP TO 85% OFF §6✰',
|
|
||||||
* favicon: 'data:image/png;base64,iVBORw0KGg...
|
|
||||||
}
|
|
||||||
* ```
|
|
||||||
* @see [source](https://github.com/minescope/mineping/blob/915edbec9c9ad811459458600af3531ec0836911/lib/java.js#L117)
|
|
||||||
*/
|
*/
|
||||||
export function pingJava(host: string, options?: PingOptions): Promise<JavaPingResponse>;
|
description?: object | string;
|
||||||
|
/**
|
||||||
|
* - A Base64-encoded 64x64 PNG image data URI.
|
||||||
|
*/
|
||||||
|
favicon?: string;
|
||||||
|
/**
|
||||||
|
* - True if the server requires clients to have a Mojang-signed public key.
|
||||||
|
*/
|
||||||
|
enforcesSecureChat?: boolean;
|
||||||
|
/**
|
||||||
|
* - True if a mod is installed to disable chat reporting.
|
||||||
|
*/
|
||||||
|
preventsChatReports?: boolean;
|
||||||
|
};
|
||||||
|
export type JavaPingOptions = {
|
||||||
|
/**
|
||||||
|
* - The fallback port if an SRV record is not found.
|
||||||
|
*/
|
||||||
|
port?: number;
|
||||||
|
/**
|
||||||
|
* - The connection timeout in milliseconds.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
/**
|
||||||
|
* - The protocol version to use in the handshake. `-1` is for auto-detection.
|
||||||
|
*/
|
||||||
|
protocolVersion?: number;
|
||||||
|
};
|
||||||
|
57
types/lib/varint.d.ts
vendored
57
types/lib/varint.d.ts
vendored
@ -1,10 +1,49 @@
|
|||||||
export default varint;
|
/**
|
||||||
declare namespace varint {
|
* Encodes an integer into a VarInt buffer.
|
||||||
function encodeInt(val: number): Buffer;
|
* VarInts are never longer than 5 bytes for the Minecraft protocol.
|
||||||
function encodeString(val: string): Buffer;
|
* @param {number} value The integer to encode
|
||||||
function encodeUShort(val: number): Buffer;
|
* @returns {Buffer} The encoded VarInt as a buffer
|
||||||
function concat(chunks: Buffer[]): Buffer;
|
* @throws {VarIntError} if the value is too large to be encoded
|
||||||
function decodeInt(buffer: Buffer, offset: number): number;
|
*/
|
||||||
function decodeString(val: Buffer, offset?: number): string;
|
export function encodeVarInt(value: number): Buffer;
|
||||||
function decodeLength(val: number): 5 | 7 | 8 | 1 | 2 | 3 | 4 | 6 | 9 | 10;
|
/**
|
||||||
|
* Encodes a string into a UTF-8 buffer.
|
||||||
|
* @param {string} value The string to encode
|
||||||
|
* @returns {Buffer}
|
||||||
|
*/
|
||||||
|
export function encodeString(value: string): Buffer;
|
||||||
|
/**
|
||||||
|
* Encodes an unsigned short (16-bit big-endian) into a 2-byte buffer.
|
||||||
|
* @param {number} value The number to encode
|
||||||
|
* @returns {Buffer}
|
||||||
|
*/
|
||||||
|
export function encodeUShort(value: number): Buffer;
|
||||||
|
/**
|
||||||
|
* Creates a Minecraft-style packet by concatenating chunks and prefixing the total length as a VarInt.
|
||||||
|
* @param {Buffer[]} chunks An array of buffers to include in the packet payload
|
||||||
|
* @returns {Buffer} The complete packet with its length prefix
|
||||||
|
*/
|
||||||
|
export function concatPackets(chunks: Buffer[]): Buffer;
|
||||||
|
/**
|
||||||
|
* Decodes a VarInt from a buffer.
|
||||||
|
* Returns the decoded value and the number of bytes it consumed.
|
||||||
|
* @param {Buffer} buffer The buffer to read from
|
||||||
|
* @param {number} [offset=0] The starting offset in the buffer
|
||||||
|
* @returns {{ value: number, bytesRead: number }}
|
||||||
|
* @throws {VarIntError} if the buffer is too short or the VarInt is malformed
|
||||||
|
*/
|
||||||
|
export function decodeVarInt(buffer: Buffer, offset?: number): {
|
||||||
|
value: number;
|
||||||
|
bytesRead: number;
|
||||||
|
};
|
||||||
|
export const ERR_VARINT_BUFFER_UNDERFLOW: "VARINT_BUFFER_UNDERFLOW";
|
||||||
|
export const ERR_VARINT_MALFORMED: "VARINT_MALFORMED";
|
||||||
|
export const ERR_VARINT_ENCODE_TOO_LARGE: "VARINT_ENCODE_TOO_LARGE";
|
||||||
|
export class VarIntError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {string} message The error message.
|
||||||
|
* @param {string} code The error code.
|
||||||
|
*/
|
||||||
|
constructor(message: string, code: string);
|
||||||
|
code: string;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user