49 Commits

Author SHA1 Message Date
65d375cd86 chore: bump version to 2.0.0 2025-06-25 13:32:39 +03:00
ed802661a4 docs: change code block language 2025-06-25 13:30:09 +03:00
c533bfe40d chore: bump version to 1.7.0-beta.5 2025-06-25 13:27:27 +03:00
7222eeae3c docs: overhaul README and provide new usage examples
The README.md has been completely rewritten. Also the `example`
directory has been renamed to `examples` and its content
has been replaced with a more structured and practical set of scripts.
2025-06-25 13:18:38 +03:00
0bf30140b7 chore: bump version to 1.7.0-beta.4 2025-06-25 01:49:31 +03:00
371fb9daa7 feat: skip SRV lookup for IP addresses and localhost
Performing DNS SRV lookups for hosts that are already direct IP
addresses or special-case hostnames like 'localhost' is
unnecessary.

In addition, several related test suite improvements
have been included:

- A bug in the Bedrock mock packet creation was fixed where the magic
  GUID was being written to an incorrect buffer offset.
- The DNS mocking strategy in the Java tests has been refactored for
  better accuracy by mocking the `dns.promises.Resolver` class.
- An error test for the Bedrock pinger was corrected to properly
  handle a promise rejection instead of a synchronous throw.
2025-06-25 01:46:53 +03:00
1e6b5b3973 chore: bump version to 1.7.0-beta.3 2025-06-25 01:13:17 +03:00
23299a9a07 fix: add timeout to SRV record lookup
The `dns.promises.resolveSrv` function used for SRV record lookups
does not have a user-configurable timeout. This could cause the entire
`pingJava` operation to hang for an extended period if the target's
DNS server unresponsive.

To make the SRV lookup more robust and respect the user-provided
timeout, this commit switches to using the `dns.promises.Resolver`
class.
2025-06-25 01:09:07 +03:00
2bd5d9c9bf chore: bump version to 1.7.0-beta.2 2025-06-22 01:50:58 +03:00
7248a0096c feat: add typescript declaration files
Types are automatically generated from the existsing
JSDoc comments in the js source code. Use `types:build`
script from `package.json` to produce d.ts files.
2025-06-22 01:50:29 +03:00
a1b999ca4e refactor: introduce typescript for static type checking
We use JSDoc for documentation, but these annotations
were not being validated. This meant that type information
could become outdated or incorrect without any warning.

This commit introduces the TypeScript compiler (`tsc`) as a static
analysis tool to leverage our existing JSDoc comments.

To support this, JSDoc annotations across the codebase have been
improved for accuracy. Additionally, the `varint` module now uses a
custom `VarIntError` class for better type inference and error handling.
A new `typecheck` script has been added to `package.json` to run this
validation.
2025-06-19 03:55:24 +03:00
7322034aba feat: add debug logging
Add debug logs using the `debug` library.

To use this new feature, set the `DEBUG` environment variable:
- `DEBUG=mineping:*` enables all logs from this library.
- `DEBUG=mineping:java` enables logs for the Java module only.
- `DEBUG=mineping:bedrock` enables logs for the Bedrock module only.
2025-06-19 02:14:04 +03:00
c7b99cb6db feat: implement SRV record lookup and improve ping logic
Add support for DNS SRV record lookups (`_minecraft._tcp`) to
automatically resolve the correct server host and port, falling back
to the provided address if no record is found. This makes server
discovery compliant with standard Minecraft client behavior.

The Java ping implementation has been refactored:
- The `varint` module is rewritten to throw specific error codes and
  its `decodeVarInt` function now returns bytes read, which simplifies
  parsing logic.
- The core ping logic is now promise-based and modular, breaking out
  packet creation and response processing into helper functions.
- The TCP stream handler now robustly processes chunked data by
  catching recoverable decoder errors and waiting for more data,
  preventing crashes on incomplete packets.
- Error handling is improved.
2025-06-19 01:45:27 +03:00
51b4771305 docs: add guide how to install beta version 2025-06-16 03:39:21 +03:00
3c2c049c19 refactor!: decouple Raknet MOTD parsing and response shaping
The previous implementation of the RakNet ping was monolithic, mixing
socket management, raw packet validation, and data transformation into
a single, complex flow.

This refactor introduces a clear, multi-stage
processing pipeline that separates these concerns. The logic is now
broken down into multi-stage pipeline: extracting the MOTD string
from the raw pong packet -> parsing that string into a raw
object -> transforming the raw data into a
user-friendly response object.

Additionally, the socket handling logic is improved
with idempotent cleanup function to prevent resource
leaks or race conditions.

As part of this overhaul, external TypeScript definition (`.d.ts`)
files have been removed in favor of rich JSDoc annotations.

BREAKING CHANGE: The structure of the resolved `BedrockPingResponse`
object has been significantly changed to improve clarity and
consistency.
2025-06-16 03:36:26 +03:00
cbaa1a3e3e docs: update email in security policy 2025-06-15 03:00:19 +03:00
435e59739c test: implement tests using vitest framework 2025-06-15 02:56:07 +03:00
ef2bebe755 fix: only perform cleanup and fire the callback on the first error 2025-06-15 02:56:07 +03:00
27011d4091 fix: change minimum motd components to 5 and fix typos
A valid motd message has at least 5 components, not 9
2025-06-14 23:26:11 +03:00
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
78ca03b004 chore(package.json): update version from 1.1.0 to 1.1.1 2023-10-22 21:32:33 +03:00
910184bf5f refactoring and comments
fix(cli.js): refactor help and error handling logic for better readability and maintainability
feat(cli.js): add support for custom port and timeout options
fix(parallel.js): update list of hosts to ping
fix(bedrock.js): add comments and improve error handling in ping function
fix(java.js): add comments and improve error handling in ping function
fix(varint.js): add comments to functions and improve readability
fix(index.d.ts): export all functions from java.js and bedrock.js
fix(lib/bedrock.d.ts): update source link
fix(lib/java.d.ts): update source link
2023-10-22 21:32:00 +03:00
21 changed files with 3180 additions and 604 deletions

155
README.md
View File

@ -1,8 +1,19 @@
# 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.
[![npm version](https://img.shields.io/npm/v/@minescope/mineping.svg)](https://www.npmjs.com/package/@minescope/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)
A simple and efficient JavaScript library for pinging Minecraft servers. It supports both Java and Bedrock editions through a async/await-friendly API.
`@minescope/mineping` automatically resolves SRV records for Java servers and parses rich status data, including MOTD, player counts, version info, and server icons. Comes with full TypeScript support.
*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)*
## Features
- **Dual Protocol Support:** Ping both Java and Bedrock servers with a consistent API.
- **SRV Record Resolution:** Automatically resolves SRV records for Java Edition servers, so you don't have to worry about custom ports.
- **Rich Data:** Parses the full response from servers, including player samples, favicons, gamemodes, and more.
- **Lightweight:** Has only **one** runtime dependency — [debug](https://www.npmjs.com/package/debug) (used for tracing).
## Requirements
@ -10,17 +21,57 @@ Mirror on my [<img src="https://git.zeldon.ru/assets/img/logo.svg" align="center
## Install
```
To install `mineping`, simply run the following command:
```bash
npm i @minescope/mineping
```
## Loading and configuration the module
## API & Usage Examples
### ES Modules (ESM)
The library exports two main functions: `pingJava` and `pingBedrock`. Both are asynchronous and return a `Promise`.
### 1. Basic Server Ping
Java:
```js
import { pingJava, pingBedrock } from '@minescope/mineping';
import { pingJava } from "@minescope/mineping";
const data = await pingJava("0.0.0.0");
console.log(data)
```
```js
{
version: { name: '1.21.5', protocol: 770 },
enforcesSecureChat: true,
description: '§1Welcome to §2My Minecraft Server!',
players: { max: 20, online: 0 }
}
```
Bedrock:
```js
import { pingBedrock } from "@minescope/mineping";
const data = await pingBedrock("0.0.0.0");
console.log(data)
```
```js
{
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: 12143264093420916401n,
isNintendoLimited: false,
isEditorModeEnabled: false
}
```
## Loading and configuration the module
### CommonJS
@ -28,36 +79,92 @@ 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:
```js
const pingJava = (...args) => import('@minescope/mineping').then(module => module.pingJava(...args));
const pingBedrock = (...args) => import('@minescope/mineping').then(module => module.pingBedrock(...args));
const pingJava = (...args) =>
import("@minescope/mineping").then((module) => module.pingJava(...args));
const pingBedrock = (...args) =>
import("@minescope/mineping").then((module) => module.pingBedrock(...args));
```
## Usage
## Debugging
Ping a Java server with default options:
`mineping` uses the [`debug`](https://www.npmjs.com/package/debug) library to provide detailed tracing information, which can be useful for diagnosing connection issues or understanding the library's internal workings.
```js
import { pingJava } from '@minescope/mineping';
To enable debug logs, set the `DEBUG` environment variable when running your script. The library uses two namespaces:
const data = await pingJava('mc.hypixel.net');
console.log(data);
- `mineping:java` for the Java Edition pinger.
- `mineping:bedrock` for the Bedrock Edition pinger.
### Examples
**Enable all `mineping` debug logs:**
You can use a wildcard (`*`) to enable all logs from this library.
```bash
DEBUG=mineping:* node your-script.js
```
Ping a Bedrock server with custom options:
<details>
<summary>Click to see output for <code>DEBUG="mineping:*" node examples/01-basic-ping.js</code></summary>
```js
import { pingBedrock } from '@minescope/mineping';
```bash
DEBUG="mineping:*" node examples/01-basic-ping.js
mineping:java pinging Java server hypixel.net with options: {} +0ms
mineping:java attempting SRV lookup for _minecraft._tcp.hypixel.net with 5000ms timeout +2ms
mineping:java SRV lookup successful, new target: mc.hypixel.net:25565 +2ms
mineping:java creating TCP connection to mc.hypixel.net:25565 +0ms
mineping:java socket connected to mc.hypixel.net:25565, sending packets... +182ms
mineping:java received 1440 bytes of data, total buffer size is now 1440 bytes +130ms
mineping:java packet incomplete, waiting for more data +0ms
mineping:java received 12960 bytes of data, total buffer size is now 14400 bytes +1ms
mineping:java packet incomplete, waiting for more data +0ms
mineping:java received 1601 bytes of data, total buffer size is now 16001 bytes +129ms
mineping:java received raw JSON response +0ms
mineping:java successfully parsed full response +0ms
mineping:java cleaning up resources for mc.hypixel.net:25565 +0ms
--- Java Server ---
{
version: { name: 'Requires MC 1.8 / 1.21', protocol: 47 },
players: { max: 200000, online: 28654, sample: [] },
description: ' §aHypixel Network §c[1.8-1.21]\n' +
' §6§lSB 0.23 §2§lFORAGING §8§l- §e§lSUMMER EVENT',
favicon: 'data:image/png;base64,iVBORw0K'... 5738 more characters
}
const data = await pingBedrock('mco.mineplex.com', {
port: 19132,
timeout: 500
});
console.log(data);
====================
mineping:bedrock pinging Bedrock server geo.hivebedrock.network:19132 with 5000ms timeout +0ms
mineping:bedrock sending Unconnected Ping packet to geo.hivebedrock.network:19132 +1ms
mineping:bedrock packet: <Buffer 01 c0 01 00 00 00 00 00 00 00 ff ff 00 fe fe fe fe fd fd fd fd 12 34 56 78 19 20 9f 00 e6 ed ef 96> +0ms
mineping:bedrock received 124 bytes from geo.hivebedrock.network:19132 +104ms
mineping:bedrock received raw MOTD string: MCPE;BEDWARS + BUILD BATTLE;121;1.0;13074;100001;-4669279440237021648;Hive Games;Survival +0ms
mineping:bedrock cleaning up resources for geo.hivebedrock.network:19132 +0ms
--- Bedrock Server ---
{
edition: 'MCPE',
name: 'BEDWARS + BUILD BATTLE',
levelName: 'Hive Games',
gamemode: 'Survival',
version: { protocol: 121, minecraft: '1.0' },
players: { online: 13074, max: 100001 },
port: { v4: undefined, v6: undefined },
guid: -4669279440237021648n,
isNintendoLimited: undefined,
isEditorModeEnabled: undefined
}
```
</details>
> More complex example can be found in the `example` folder!
**_PowerShell_ uses different syntax to set environment variables:**
```powershell
$env:DEBUG="mineping:*";node your-script.js
```
## Acknowledgements
Special thanks to the following projects for inspiration and protocol details:
- [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 querying Minecraft Java Edition servers
- The amazing community at [minecraft.wiki](https://minecraft.wiki/) for documenting the protocols.

View File

@ -1,5 +1,5 @@
# 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!

View File

@ -1,75 +0,0 @@
import { pingBedrock, pingJava } from '../index.js';
const args = getArgs();
if (args.help || args.h || Object.keys(args).length === 0) {
console.log(`node cli.js [..]
A simple to use, efficient, and full-featured Minecraft server info parser!
USAGE:
node cli.js [OPTIONS] --host <HOST> --port <PORT> --timeout <TIMEOUT>
OPTIONS:
-j Use for Minecraft Java Edition
-b Use for Minecraft Bedrock Edition
P.S. Don't use them at the same time!`);
process.exit(1);
}
if (!args.host) {
console.error('ERROR: The host argument not found! Use -h or --help.');
process.exit(1);
}
// easter egg <3
if (args.j && args.b) {
console.log(`Some interesting facts about MOTDs on bedrock:
- so far they seem to exclusively use legacy color codes
- the random style has a special impl for periods, they turn into animated
colons that warp up and down rapidly
- motd_2 is ignored? client displays "motd_1 - v{version}", where the
appended version text is considered part of motd_1 for color code processing
- motd_2 seems to mainly be used to return the server software in use (e.g. PocketMine-MP)`);
process.exit(0);
}
if (args.j) {
const data = await pingJava(args.host, {
port: args.port || 25565,
timeout: args.timeout || 500
});
console.log(`host: ${args.host}\nprotocol: ${data.version.protocol}\nonline: ${data.players.online}`);
} else if (args.b) {
const data = await pingBedrock(args.host, {
port: args.port || 19132,
timeout: args.timeout || 500
});
console.log(`host: ${args.host}\nprotocol: ${data.version.protocol}\nonline: ${data.players.online}`);
} else {
console.error('ERROR: Unsupported flag passed. Use -h or --help.');
}
// parsing command line arguments
function getArgs() {
const args = {};
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

@ -1,10 +0,0 @@
import { pingBedrock } from '../index.js';
const [thehive, oasys, frizmine, breadix] = await Promise.allSettled([
pingBedrock('geo.hivebedrock.network'),
pingBedrock('oasys-pe.com'),
pingBedrock('frizmine.ru'),
pingBedrock('play.breadixpe.ru')
]);
console.dir({ thehive, oasys, frizmine, breadix }, { depth: 3 });

19
examples/01-basic-ping.js Normal file
View File

@ -0,0 +1,19 @@
import { pingJava, pingBedrock } from "../index.js";
try {
const javaData = await pingJava("hypixel.net");
console.log("--- Java Server ---");
console.log(javaData);
} catch (error) {
console.error("Could not ping Java server:", error);
}
console.log("\n" + "=".repeat(20) + "\n");
try {
const motd = await pingBedrock("geo.hivebedrock.network");
console.log("--- Bedrock Server ---");
console.log(motd);
} catch (error) {
console.error("Could not ping Bedrock server:", error);
}

View File

@ -0,0 +1,14 @@
import { pingJava } from "../index.js";
const offlineServer = "this.server.does.not.exist";
const port = 12345;
console.log(`Pinging an offline server: ${offlineServer}:${port}`);
try {
// We set a short timeout to fail faster.
const data = await pingJava(offlineServer, { port, timeout: 500 });
console.log("Success!?", data);
} catch (error) {
console.error("Caught expected error:", error.message);
}

View File

@ -0,0 +1,47 @@
import { pingJava, pingBedrock } from "../index.js";
const servers = [
{ type: "Java", host: "mc.hypixel.net" },
{ type: "Java", host: "play.cubecraft.net" },
{ type: "Java", host: "an-offline-java-server.com" },
{ type: "Bedrock", host: "geo.hivebedrock.network" },
{ type: "Bedrock", host: "buzz.insanitycraft.net" },
{ type: "Bedrock", host: "an.offline.bedrock.server" },
];
console.log("Pinging all servers...");
// Create an array of ping promises
const pingPromises = servers.map((server) => {
if (server.type === "Java") {
return pingJava(server.host, { timeout: 3000 });
} else {
return pingBedrock(server.host, { timeout: 3000 });
}
});
// Wait for all pings to complete (or fail)
const results = await Promise.allSettled(pingPromises);
// Process and display results
const displayData = results.map((result, index) => {
const server = servers[index];
if (result.status === "fulfilled") {
const data = result.value;
return {
Server: `${server.type} - ${server.host}`,
Status: "✅ Online",
Players: `${data.players.online} / ${data.players.max}`,
Version: data.version.name ?? data.version.minecraft,
};
} else {
return {
Server: `${server.type} - ${server.host}`,
Status: "❌ Offline",
Players: "N/A",
Version: `Error: ${result.reason.message.slice(0, 30)}...`,
};
}
});
console.table(displayData);

140
examples/04-cli.js Normal file
View File

@ -0,0 +1,140 @@
#!/usr/bin/env node
/**
* Usage examples:
* - Java (with custom timeout): node cli.js -j --host="mc.hypixel.net" --timeout 1000
* - Bedrock: node cli.js -b --host="play.timecrack.net"
*/
import { pingBedrock, pingJava } from "../index.js";
const DEFAULT_TIMEOUT = 5000;
const JAVA_DEFAULT_PORT = 25565;
const BEDROCK_DEFAULT_PORT = 19132;
try {
const args = parseArgs(process.argv.slice(2));
if (shouldShowHelp(args)) {
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);
}
function parseArgs(rawArgs) {
const args = {};
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;
}
function validateArgs(args) {
if (args.j && args.b) {
printInterestingFacts();
process.exit(0);
}
if (!args.host) {
throw new Error("The host argument not found! Use -h or --help.");
}
if (!args.j && !args.b) {
throw new Error("Must specify either -j or -b flag. Use -h or --help.");
}
if (args.port && (isNaN(args.port) || args.port < 1 || args.port > 65535)) {
throw new Error("Port must be a number between 1 and 65535");
}
if (args.timeout && (isNaN(args.timeout) || args.timeout < 0)) {
throw new Error("Timeout must be a positive number");
}
}
function shouldShowHelp(args) {
return args.help || args.h || Object.keys(args).length === 0;
}
function printHelp() {
console.log(`node cli.js [..]
A simple to use, efficient, and full-featured Minecraft server info parser!
USAGE:
node cli.js [OPTIONS] --host <HOST> --port <PORT> --timeout <TIMEOUT>
OPTIONS:
-j Use for Minecraft Java Edition
-b Use for Minecraft Bedrock Edition
-h, --help Show this help message
--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() {
console.log(`Some interesting facts about MOTDs on bedrock:
- so far they seem to exclusively use legacy color codes
- the random style has a special impl for periods, they turn into animated
colons that warp up and down rapidly
- motd_2 is ignored? client displays "motd_1 - v{version}", where the
appended version text is considered part of motd_1 for color code processing
- motd_2 seems to mainly be used to return the server software in use (e.g. PocketMine-MP)`);
}
function getDefaultPort(args) {
return args.j ? JAVA_DEFAULT_PORT : BEDROCK_DEFAULT_PORT;
}
async function pingJavaServer(host, port, timeout) {
const data = await pingJava(host, { port, timeout });
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) {
const data = await pingBedrock(host, { port, timeout });
console.log(`Host: ${host}
Edition: ${data.edition}
Version: ${data.version.minecraft} (protocol: ${data.version.protocol})
Players: ${data.players.online}/${data.players.max}
Name: ${data.name}
Gamemode: ${data.gamemode}`);
}

View File

@ -1,214 +1,261 @@
/**
* Implementation of the RakNet ping/pong protocol.
* @see https://wiki.vg/Raknet_Protocol#Unconnected_Ping
*
* Data types:
* @see https://wiki.vg/Raknet_Protocol#Data_types
* @see https://minecraft.wiki/w/RakNet
*/
'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.
* @param {number} length - The length of the buffer.
* @returns {Buffer} - The created buffer.
* 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.
* @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;
return buffer;
/**
* 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;
};
/**
* Writes a BigInt value to the buffer at the specified offset using big-endian byte order.
* @param {Buffer} buffer - The buffer to write to.
* @param {number} value - The BigInt value to write.
* @param {number} offset - The offset in the buffer to write the value.
* Parses the semicolon-delimited MOTD string into a structured object.
* @param {string} motdString - The raw MOTD string from the server.
* @returns {BedrockMotd} The parsed internal MOTD object.
* @throws {Error} If the MOTD string is missing required fields.
*/
const writeBigInt64BE = (buffer, value, offset) => {
buffer.writeBigInt64BE(BigInt(value), offset);
const parseMotd = (motdString) => {
const parts = motdString.split(";");
if (parts.length < 5) {
throw new Error(
`Invalid MOTD format: Expected at least 5 fields, but got ${parts.length}.`
);
}
const [
edition,
name,
protocolStr,
version,
playerCountStr,
playerMaxStr,
serverGuidStr,
subName,
gamemode,
nintendoLimitedStr,
port,
ipv6Port,
editorModeStr,
] = parts;
let nintendoLimited;
if (nintendoLimitedStr === "0") {
nintendoLimited = true;
} else if (nintendoLimitedStr === "1") {
nintendoLimited = false;
}
return {
edition,
name,
protocol: Number(protocolStr),
version,
playerCount: Number(playerCountStr),
playerMax: Number(playerMaxStr),
serverGuid: BigInt(serverGuidStr),
subName,
gamemode,
nintendoLimited,
port: port ? Number(port) : undefined,
ipv6Port: ipv6Port ? Number(ipv6Port) : undefined,
editorMode: editorModeStr ? Boolean(Number(editorModeStr)) : undefined,
};
};
/**
* Copies the specified hex value to the buffer at the specified offset.
* @param {Buffer} buffer - The buffer to copy to.
* @param {string} hex - The hex value to copy.
* @param {number} offset - The offset in the buffer to copy the value.
* Transforms the raw MOTD object into a user-friendly, nested structure.
* @param {BedrockMotd} motd - The parsed MOTD object.
* @returns {BedrockPingResponse} The final, user-facing response object.
*/
const copyHexToBuffer = (buffer, hex, offset) => {
Buffer.from(hex, 'hex').copy(buffer, offset);
const transformMotd = (motd) => {
return {
edition: motd.edition,
name: motd.name,
levelName: motd.subName,
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,
};
};
/**
* 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.
* Extracts the MOTD string from an Unconnected Pong packet and parses it.
* @param {Buffer} pongPacket - The raw pong packet from the server.
* @returns {BedrockPingResponse} The final response object.
* @throws {Error} If the packet is malformed.
*/
const readBigInt64BE = (buffer, offset) => {
return buffer.readBigInt64BE(offset);
const parseUnconnectedPong = (pongPacket) => {
if (!Buffer.isBuffer(pongPacket) || pongPacket.length < 35) {
throw new Error("Invalid pong packet: buffer is too small.");
}
const packetId = pongPacket.readUInt8(0);
if (packetId !== UNCONNECTED_PONG) {
throw new Error(
`Unexpected packet ID: 0x${packetId.toString(16)}. Expected 0x1c.`
);
}
// 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;
};
/**
* 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);
return { pingId, advertiseStr, serverId, offset, ...parsedAdvertiseStr };
};
/**
* 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 {number} [port=19132] - The port number.
* @param {function} cb - The callback function to handle the response.
* @param {number} [timeout=5000] - The timeout duration in milliseconds.
* @param {BedrockPingOptions} [options={}] - Optional configuration.
* @returns {Promise<BedrockPingResponse>} A promise that resolves with the server's parsed MOTD.
*/
const ping = (host, port = 19132, cb, timeout = 5000) => {
const socket = dgram.createSocket('udp4');
export async function pingBedrock(host, options = {}) {
if (!host) {
throw new Error("Host argument is required.");
}
const timeoutTask = setTimeout(() => {
socket.emit('error', new Error('Socket timeout'));
}, timeout);
const { port = 19132, timeout = 5000 } = options;
debug("pinging Bedrock server %s:%d with %dms timeout", host, port, timeout);
const closeSocket = () => {
socket.close(); clearTimeout(timeoutTask);
};
return new Promise((resolve, reject) => {
const socket = dgram.createSocket("udp4");
// 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;
// Prevent cleanup tasks from running more than once
// in case of multiple error callbacks
let isCleanupCompleted = false;
const handleError = (err) => {
closeSocket();
// Set a manual timeout interval to ensure
// the connection will NEVER hang regardless of internal state
const timeoutTask = setTimeout(() => {
socket.emit("error", new Error("Socket timeout"));
}, timeout);
if (!didFireError) {
didFireError = true;
cb(null, err);
}
};
// Idempotent function to handle cleanup tasks, we can safely call it multiple times without side effects
const cleanup = () => {
if (isCleanupCompleted) return;
isCleanupCompleted = true;
debug("cleaning up resources for %s:%d", host, port);
clearTimeout(timeoutTask);
socket.close();
};
try {
const ping = UNCONNECTED_PING(new Date().getTime() - START_TIME);
socket.send(ping, 0, ping.length, port, host);
} catch (err) {
handleError(err);
}
// Generic error handler
socket.on("error", (err) => {
debug("socket error for %s:%d - %s", host, port, err.message);
cleanup();
reject(err);
});
socket.on('message', (msg) => {
const id = msg[0];
socket.on("message", (pongPacket) => {
debug("received %d bytes from %s:%d", pongPacket.length, host, port);
try {
const motd = parseUnconnectedPong(pongPacket);
cleanup();
resolve(motd);
} catch (err) {
socket.emit("error", err);
}
});
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', (err) => handleError(err));
};
/**
* Asynchronously ping Minecraft Bedrock server.
* The optional `options` argument can be an object with a `ping` (default is `19132`) or/and `timeout` (default is `5000`) property.
* @param {string} host The Bedrock server address.
* @param {import('../types/lib/bedrock.js').PingOptions} options The configuration for pinging Minecraft Bedrock server.
* @returns {Promise<import('../types/lib/bedrock.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);
});
};
try {
const pingPacket = createUnconnectedPingFrame(Date.now() - START_TIME);
debug("sending Unconnected Ping packet to %s:%d", host, port);
debug("packet: %o", pingPacket);
socket.send(pingPacket, 0, pingPacket.length, port, host);
} catch (err) {
// Handle any immediate, synchronous errors that might occur when sending the ping packet
socket.emit("error", err);
}
});
}

View File

@ -1,127 +1,288 @@
/**
* 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 varint from './varint.js';
import { createConnection, isIP } from "node:net";
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");
function ping(host, port = 25565, cb, timeout = 5000) {
const socket = net.createConnection(({ host, port }));
/**
* Represents the structured and user-friendly response from a server ping.
* 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.
* @typedef {object} JavaPingResponse
* @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.
*/
// Set manual timeout interval.
// This ensures the connection will NEVER hang regardless of internal state
const timeoutTask = setTimeout(() => {
socket.emit('error', new Error('Socket timeout'));
}, timeout);
/**
* @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.
*/
const closeSocket = () => {
socket.destroy();
clearTimeout(timeoutTask);
};
/**
* 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);
// 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;
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
];
const handleError = (err) => {
closeSocket();
if (!didFireError) {
didFireError = true;
cb(null, err);
}
};
// #setNoDelay instantly flushes data during read/writes
// This prevents the runtime from delaying the write at all
socket.setNoDelay(true);
socket.on('connect', () => {
const handshake = varint.concat([
varint.encodeInt(0),
varint.encodeInt(PROTOCOL_VERSION),
varint.encodeInt(host.length),
varint.encodeString(host),
varint.encodeUShort(port),
varint.encodeInt(1)
]);
socket.write(handshake);
const request = varint.concat([
varint.encodeInt(0)
]);
socket.write(request);
});
let incomingBuffer = Buffer.alloc(0);
socket.on('data', (data) => {
incomingBuffer = Buffer.concat([incomingBuffer, data]);
// Wait until incomingBuffer is at least 5 bytes long to ensure it has captured the first VarInt value
// This value is used to determine the full read length of the response
// "VarInts are never longer than 5 bytes"
// https://wiki.vg/Data_types#VarInt_and_VarLong
if (incomingBuffer.length < 5) {
return;
}
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.slice(varint.decodeLength(packetLength) + varint.decodeLength(packetId));
const responseLength = varint.decodeInt(data, 0);
const response = data.slice(varint.decodeLength(responseLength), varint.decodeLength(responseLength) + responseLength);
try {
const message = JSON.parse(response);
cb(null, message);
// Close the socket and clear the timeout task
closeSocket();
} catch (err) {
handleError(err);
}
} else {
handleError(new Error('Received unexpected packet'));
}
});
socket.on('error', handleError);
return varint.concatPackets(payload);
}
/**
* 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/lib/java.js').JavaPingResponse>}
* Creates the Status Request packet.
* @returns {Buffer} The complete Status Request packet
*/
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, (err, res) => {
err ? reject(err) : resolve(res);
}, timeout);
});
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;
// A list of hostnames that should never have an SRV lookup.
const nonSrvLookableHostnames = ["localhost"];
// Check if the host is a valid IP address (v4 or v6).
// net.isIP() returns 0 for invalid IPs, 4 for IPv4, and 6 for IPv6.
const isDirectIp = isIP(host) !== 0;
const isNonLookableHostname = nonSrvLookableHostnames.includes(
host.toLowerCase()
);
if (isDirectIp || isNonLookableHostname) {
debug(
"host '%s' is a direct IP or a non-lookable hostname, skipping SRV lookup.",
host
);
} else {
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 = 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(() => {
socket.emit("error", new Error("Socket timeout"));
}, timeout);
// Idempotent function to handle cleanup tasks, we can safely call it multiple times without side effects
const cleanup = () => {
if (isCleanupCompleted) return;
isCleanupCompleted = true;
debug("cleaning up resources for %s:%d", targetHost, targetPort);
clearTimeout(timeoutTask);
socket.destroy();
};
// #setNoDelay instantly flushes data during read/writes
// This prevents the runtime from delaying the write at all
socket.setNoDelay(true);
// Generic error handler
socket.on("error", (err) => {
debug("socket error for %s:%d - %s", targetHost, targetPort, err.message);
cleanup();
reject(err);
});
socket.on("close", () => {
if (!isCleanupCompleted) {
debug("socket for %s:%d closed prematurely", targetHost, targetPort);
cleanup();
reject(new Error("Socket closed unexpectedly without a response."));
}
});
socket.on("connect", () => {
debug(
"socket connected to %s:%d, sending packets...",
targetHost,
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);
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]);
try {
const result = processResponse(incomingBuffer);
if (result) {
debug("successfully parsed full response");
// 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) {
socket.emit("error", err);
}
});
});
}

View File

@ -1,90 +1,138 @@
// https://wiki.vg/Data_types
// https://minecraft.wiki/w/Java_Edition_protocol/Data_types
const varint = {
encodeInt: (val) => {
// "constInts are never longer than 5 bytes"
// https://wiki.vg/Data_types#constInt_and_constLong
const buf = Buffer.alloc(5);
let written = 0;
"use strict";
while (true) {
if ((val & 0xFFFFFF80) === 0) {
buf.writeUInt8(val, written++);
break;
} else {
buf.writeUInt8(val & 0x7F | 0x80, written++);
val >>>= 7;
}
}
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";
return buf.slice(0, written);
},
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;
}
}
encodeString: (val) => {
return Buffer.from(val, 'utf-8');
},
/**
* 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
*/
export function encodeVarInt(value) {
const buf = Buffer.alloc(5);
let written = 0;
let val = value;
encodeUShort: (val) => {
return Buffer.from([val >> 8, val & 0xFF]);
},
while (true) {
const byte = val & 0x7f;
val >>>= 7;
concat: (chunks) => {
let length = 0;
if (val === 0) {
buf.writeUInt8(byte, written++);
break;
}
for (const chunk of chunks) {
length += chunk.length;
}
buf.writeUInt8(byte | 0x80, written++);
const buffer = [
varint.encodeInt(length),
...chunks
];
if (written >= 5 && val > 0) {
throw new VarIntError(
"Value too large for a 5-byte VarInt",
ERR_VARINT_ENCODE_TOO_LARGE
);
}
}
return Buffer.concat(buffer);
},
return buf.subarray(0, written);
}
decodeInt: (buffer, offset) => {
let val = 0;
let count = 0;
/**
* 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");
}
while (true) {
const b = buffer.readUInt8(offset++);
/**
* 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;
}
val |= (b & 0x7F) << count++ * 7;
/**
* 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]);
}
if ((b & 0x80) != 128) {
break;
}
}
/**
* 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
);
}
return val;
},
// Fast path for single-byte VarInts, which are very common.
const firstByte = buffer.readUInt8(offset);
if ((firstByte & 0x80) === 0) {
return { value: firstByte, bytesRead: 1 };
}
// The number of bytes that the last .decodeInt() call had to use to decode.
decodeLength: (val) => {
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);
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
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
);
}
};
// 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
);
}
export default varint;
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

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,45 @@
{
"name": "@minescope/mineping",
"version": "1.1.0",
"description": "Ping both Minecraft Bedrock and Java servers.",
"main": "index.js",
"types": "types/index.d.ts",
"keywords": [],
"author": {
"name": "Timofey (xzeldon)",
"email": "contact@zeldon.ru",
"url": "https://zeldon.ru"
},
"repository": {
"type": "git",
"url": "git://github.com/minescope/mineping.git"
},
"type": "module",
"engines": {
"node": ">=14"
},
"license": "MIT"
"name": "@minescope/mineping",
"version": "2.0.0",
"description": "Ping both Minecraft Bedrock and Java servers.",
"main": "index.js",
"type": "module",
"types": "types/index.d.ts",
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"types:check": "tsc --noEmit",
"types:build": "tsc"
},
"repository": {
"type": "git",
"url": "git://github.com/minescope/mineping.git"
},
"license": "MIT",
"keywords": [
"minecraft",
"raknet",
"node",
"mcpe",
"mcbe",
"ping",
"bedrock"
],
"author": {
"name": "Timofey Gelazoniya",
"email": "timofey@z4n.me",
"url": "https://zeldon.ru"
},
"engines": {
"node": ">=14"
},
"dependencies": {
"debug": "^4.4.1"
},
"devDependencies": {
"@types/debug": "^4.1.12",
"@types/node": "^24.0.3",
"typescript": "^5.8.3",
"vitest": "^3.2.3"
}
}

176
test/bedrock.test.js Normal file
View File

@ -0,0 +1,176 @@
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§b§f  §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§b§f  §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", async () => {
await expect(pingBedrock(null)).rejects.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, 9);
packet.writeUInt16BE(motdBuffer.length, 33);
motdBuffer.copy(packet, 35);
return packet;
}

144
test/java.test.js Normal file
View File

@ -0,0 +1,144 @@
import net from "node:net";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { pingJava } from "../lib/java.js";
import * as varint from "../lib/varint.js";
const mockResolveSrv = vi.fn();
vi.mock("node:net");
vi.mock("node:dns/promises", () => ({
Resolver: vi.fn().mockImplementation(() => ({
resolveSrv: mockResolveSrv,
})),
}));
describe("pingJava", () => {
let mockSocket;
beforeEach(() => {
// Reset mocks before each test.
mockResolveSrv.mockClear();
// Simulate no SRV record found by default.
mockResolveSrv.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(net.createConnection).toHaveBeenCalledWith({
host: host,
port: options.port,
});
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
View 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
View File

@ -0,0 +1,10 @@
{
"include": ["lib/**/*.js", "index.js"],
"compilerOptions": {
"allowJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "types",
"removeComments": false
}
}

168
types/lib/bedrock.d.ts vendored
View File

@ -1,49 +1,131 @@
/**
* @param port The server port.
* @param timeout The read/write socket timeout.
* Asynchronously pings a Minecraft Bedrock server.
* @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 type PingOptions = {
port: number,
timeout: number;
};
export type BedrockPingResponse = {
version: {
name: string;
protocol: string;
};
players: {
max: string;
online: string;
};
description: string;
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).
*/
name: string;
/**
* - The protocol version.
*/
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;
};
/**
* 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/24a48802300f988d3ae520edbeb4f3e12820dcc9/lib/java.js#L117)
* 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 function pingBedrock(host: string, options?: PingOptions): Promise<BedrockPingResponse>;
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;
};

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

@ -1,71 +1,63 @@
import { PingOptions } from "./bedrock";
/**
* JSON format chat component used for description field.
* @see https://wiki.vg/Chat
* 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 type ChatComponent = {
text: string;
bold?: boolean;
italic?: boolean;
underlined?: boolean;
strikethrough?: boolean;
obfuscated?: boolean;
color?: string;
extra?: ChatComponent[];
};
export type SampleProp = {
name: string,
id: string;
};
export function pingJava(host: string, options?: JavaPingOptions): Promise<JavaPingResponse>;
/**
* `JSON Response` field of Response packet.
* @see https://wiki.vg/Server_List_Ping#Response
* Represents the structured and user-friendly response from a server ping.
* 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 = {
/**
* - Contains the server's version name and protocol number.
*/
version: {
name: string;
protocol: number;
};
players: {
/**
* - Player count and a sample of online players.
*/
players?: {
max: number;
online: number;
sample?: SampleProp[];
sample?: Array<{
name: string;
id: string;
}>;
};
description: string | ChatComponent;
/**
* - The server's Message of the Day (MOTD).
*/
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;
previewsChat?: 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;
};
/**
* 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/8c84925ef7f5c420a7ef52740cba027491e82934/lib/bedrock.js#L158)
*/
export function pingJava(host: string, options?: PingOptions): Promise<JavaPingResponse>;

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

@ -1,10 +1,49 @@
export default varint;
declare namespace varint {
function encodeInt(val: number): Buffer;
function encodeString(val: string): Buffer;
function encodeUShort(val: number): Buffer;
function concat(chunks: Buffer[]): Buffer;
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;
/**
* 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
*/
export function encodeVarInt(value: number): Buffer;
/**
* 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;
}