mirror of
https://github.com/minescope/mineping.git
synced 2025-07-18 21:26:36 +03:00
Compare commits
36 Commits
v1.1.0
...
51b4771305
Author | SHA1 | Date | |
---|---|---|---|
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
|
|||
78ca03b004
|
|||
910184bf5f
|
30
README.md
30
README.md
@ -1,6 +1,6 @@
|
||||
# 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)
|
||||
|
||||
@ -10,16 +10,22 @@ Mirror on my [<img src="https://git.zeldon.ru/assets/img/logo.svg" align="center
|
||||
|
||||
## Install
|
||||
|
||||
To install `mineping`, simply run the following command:
|
||||
|
||||
```
|
||||
npm i @minescope/mineping
|
||||
```
|
||||
|
||||
> To install _beta_ version (if available), run: `npm i @minescope/mineping@next`
|
||||
|
||||
## Loading and configuration the module
|
||||
|
||||
### ES Modules (ESM)
|
||||
|
||||
If you are using ES Modules, you can import the library like this:
|
||||
|
||||
```js
|
||||
import { pingJava, pingBedrock } from '@minescope/mineping';
|
||||
import { pingJava, pingBedrock } from "@minescope/mineping";
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```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
|
||||
@ -37,20 +45,20 @@ const pingBedrock = (...args) => import('@minescope/mineping').then(module => mo
|
||||
Ping a Java server with default options:
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
Ping a Bedrock server with custom options:
|
||||
|
||||
```js
|
||||
import { pingBedrock } from '@minescope/mineping';
|
||||
import { pingBedrock } from "@minescope/mineping";
|
||||
|
||||
const data = await pingBedrock('mco.mineplex.com', {
|
||||
port: 19132,
|
||||
timeout: 500
|
||||
const data = await pingBedrock("mco.mineplex.com", {
|
||||
port: 19132,
|
||||
timeout: 500,
|
||||
});
|
||||
console.log(data);
|
||||
```
|
||||
@ -59,5 +67,7 @@ console.log(data);
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Special thanks to the following projects:
|
||||
|
||||
- [mcping](https://github.com/Scetch/mcping) crate for Rust
|
||||
- [mcping-js](https://github.com/Cryptkeeper/mcping-js) library for quering Minecraft Java Edition servers
|
||||
|
@ -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!
|
169
example/cli.js
169
example/cli.js
@ -1,9 +1,91 @@
|
||||
import { pingBedrock, pingJava } from '../index.js';
|
||||
#!/usr/bin/env node
|
||||
|
||||
const args = getArgs();
|
||||
/**
|
||||
* 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"
|
||||
*/
|
||||
|
||||
if (args.help || args.h || Object.keys(args).length === 0) {
|
||||
console.log(`node cli.js [..]
|
||||
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:
|
||||
@ -11,65 +93,48 @@ if (args.help || args.h || Object.keys(args).length === 0) {
|
||||
|
||||
OPTIONS:
|
||||
-j Use for Minecraft Java Edition
|
||||
-b Use for Minecraft Bedrock Edition
|
||||
P.S. Don't use them at the same time!`);
|
||||
-b Use for Minecraft Bedrock Edition
|
||||
-h, --help Show this help message
|
||||
|
||||
process.exit(1);
|
||||
--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!`);
|
||||
}
|
||||
|
||||
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:
|
||||
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)`);
|
||||
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.');
|
||||
function getDefaultPort(args) {
|
||||
return args.j ? JAVA_DEFAULT_PORT : BEDROCK_DEFAULT_PORT;
|
||||
}
|
||||
|
||||
// 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;
|
||||
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.minecraftVersion} (protocol: ${data.version.protocolVersion})
|
||||
Players: ${data.players.online}/${data.players.max}
|
||||
Name: ${data.name}
|
||||
Gamemode: ${data.gameMode}`);
|
||||
}
|
@ -1,10 +1,20 @@
|
||||
import { pingBedrock } from '../index.js';
|
||||
|
||||
const [thehive, oasys, frizmine, breadix] = await Promise.allSettled([
|
||||
pingBedrock('geo.hivebedrock.network'),
|
||||
pingBedrock('oasys-pe.com'),
|
||||
pingBedrock('frizmine.ru'),
|
||||
pingBedrock('play.breadixpe.ru')
|
||||
]);
|
||||
const hosts = [
|
||||
'play.timecrack.net',
|
||||
'geo.hivebedrock.network',
|
||||
'oasys-pe.com',
|
||||
'play.galaxite.net',
|
||||
];
|
||||
|
||||
console.dir({ thehive, oasys, frizmine, breadix }, { depth: 3 });
|
||||
const pingPromises = hosts.map(host => pingBedrock(host));
|
||||
const results = await Promise.allSettled(pingPromises);
|
||||
|
||||
for (let result of results) {
|
||||
if (result.status === 'rejected') {
|
||||
console.error(result.reason);
|
||||
break;
|
||||
}
|
||||
|
||||
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);
|
402
lib/bedrock.js
402
lib/bedrock.js
@ -1,214 +1,246 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
const START_TIME = new Date().getTime();
|
||||
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 {@link https://minecraft.wiki/w/RakNet#Unconnected_Pong}
|
||||
* @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 | undefined} nintendoLimited - Whether the server is Nintendo limited
|
||||
* @property {string | undefined} port - The server's IPv4 port, if provided
|
||||
* @property {string | undefined} ipv6Port - The server's IPv6 port, if provided
|
||||
* @property {string | undefined} editorMode - Whether the server is in editor mode, if provided. See: https://learn.microsoft.com/en-us/minecraft/creator/documents/bedrockeditor/editoroverview?view=minecraft-bedrock-stable
|
||||
*/
|
||||
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
|
||||
* @property {string} name
|
||||
* @property {string} levelName
|
||||
* @property {string} gamemode
|
||||
* @property {{ protocol: number, minecraft: string }} version
|
||||
* @property {{ online: number, max: number }} players
|
||||
* @property {{ v4: number | undefined, v6: number | undefined }} port
|
||||
* @property {bigint} guid
|
||||
* @property {boolean | undefined} isNintendoLimited
|
||||
* @property {string | undefined} isEditorModeEnabled
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates an Unconnected Ping packet.
|
||||
* @param {number} timestamp - The current time delta since the script started
|
||||
* @returns {Buffer}
|
||||
* @see {@link https://minecraft.wiki/w/RakNet#Unconnected_Ping}
|
||||
*/
|
||||
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
|
||||
* @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}
|
||||
*/
|
||||
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}
|
||||
* @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
|
||||
);
|
||||
|
||||
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.
|
||||
* @param {string} host - The IP address or hostname of the server.
|
||||
* @param {number} [port=19132] - The port number.
|
||||
* @param {function} cb - The callback function to handle the response.
|
||||
* @param {number} [timeout=5000] - The timeout duration in milliseconds.
|
||||
*/
|
||||
const ping = (host, port = 19132, cb, timeout = 5000) => {
|
||||
const socket = dgram.createSocket('udp4');
|
||||
|
||||
const timeoutTask = setTimeout(() => {
|
||||
socket.emit('error', new Error('Socket timeout'));
|
||||
}, timeout);
|
||||
|
||||
const closeSocket = () => {
|
||||
socket.close(); clearTimeout(timeoutTask);
|
||||
};
|
||||
|
||||
// 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 handleError = (err) => {
|
||||
closeSocket();
|
||||
|
||||
if (!didFireError) {
|
||||
didFireError = true;
|
||||
cb(null, err);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const ping = UNCONNECTED_PING(new Date().getTime() - START_TIME);
|
||||
socket.send(ping, 0, ping.length, port, host);
|
||||
} catch (err) {
|
||||
handleError(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', (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>}
|
||||
* Asynchronously pings a Minecraft Bedrock server.
|
||||
* @param {string} host - The IP address or hostname of the server
|
||||
* @param {object} [options] - Optional configuration
|
||||
* @param {number} [options.port=19132] - The server port
|
||||
* @param {number} [options.timeout=5000] - The request timeout in milliseconds
|
||||
* @returns {Promise<BedrockPingResponse>} A promise that resolves with the server's parsed MOTD
|
||||
*/
|
||||
export const pingBedrock = (host, options = {}) => {
|
||||
if (!host) throw new Error('Host argument is not provided');
|
||||
if (!host) {
|
||||
throw new Error("Host argument is required.");
|
||||
}
|
||||
|
||||
const { port = 19132, timeout = 5000 } = options;
|
||||
const { port = 19132, timeout = 5000 } = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ping(host, port, (res, err) => {
|
||||
err ? reject(err) : resolve(res);
|
||||
}, 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(() => {
|
||||
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;
|
||||
clearTimeout(timeoutTask);
|
||||
socket.close();
|
||||
};
|
||||
|
||||
// Generic error handler
|
||||
socket.on("error", (err) => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
socket.on("message", (pongPacket) => {
|
||||
try {
|
||||
const motd = parseUnconnectedPong(pongPacket);
|
||||
cleanup();
|
||||
resolve(motd);
|
||||
} catch (err) {
|
||||
socket.emit("error", err);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const pingPacket = createUnconnectedPingFrame(Date.now() - START_TIME);
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
215
lib/java.js
215
lib/java.js
@ -1,127 +1,160 @@
|
||||
/**
|
||||
* Implementation of the Java Minecraft ping protocol.
|
||||
* @see https://wiki.vg/Server_List_Ping
|
||||
* @see https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Server_List_Ping
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
import net from 'net';
|
||||
import varint from './varint.js';
|
||||
import net from "node:net";
|
||||
import varint from "./varint.js";
|
||||
|
||||
const PROTOCOL_VERSION = 0;
|
||||
/**
|
||||
* Ping a Minecraft Java server.
|
||||
* @param {string} host The host of the Java server.
|
||||
* @param {string} virtualHost The host sent in handshake.
|
||||
* @param {number} [port=25565] The port of the Java server.
|
||||
* @param {function} cb The callback function to handle the ping response.
|
||||
* @param {number} [timeout=5000] The timeout duration in milliseconds.
|
||||
* @param {number} [protocolVersion=-1] The protocol version of the Java client.
|
||||
*/
|
||||
function ping(
|
||||
host,
|
||||
virtualHost,
|
||||
port = 25565,
|
||||
cb,
|
||||
timeout = 5000,
|
||||
protocolVersion = -1
|
||||
) {
|
||||
const socket = net.createConnection({ host, port });
|
||||
|
||||
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
|
||||
const timeoutTask = setTimeout(() => {
|
||||
socket.emit("error", new Error("Socket timeout"));
|
||||
}, timeout);
|
||||
|
||||
// Set manual timeout interval.
|
||||
// This ensures the connection will NEVER hang regardless of internal state
|
||||
const timeoutTask = setTimeout(() => {
|
||||
socket.emit('error', new Error('Socket timeout'));
|
||||
}, timeout);
|
||||
const closeSocket = () => {
|
||||
socket.destroy();
|
||||
clearTimeout(timeoutTask);
|
||||
};
|
||||
|
||||
const closeSocket = () => {
|
||||
socket.destroy();
|
||||
clearTimeout(timeoutTask);
|
||||
};
|
||||
// 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;
|
||||
|
||||
// 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) => {
|
||||
if (!didFireError) {
|
||||
didFireError = true;
|
||||
closeSocket();
|
||||
cb(null, err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (err) => {
|
||||
closeSocket();
|
||||
// #setNoDelay instantly flushes data during read/writes
|
||||
// This prevents the runtime from delaying the write at all
|
||||
socket.setNoDelay(true);
|
||||
|
||||
if (!didFireError) {
|
||||
didFireError = true;
|
||||
cb(null, err);
|
||||
}
|
||||
};
|
||||
socket.on("connect", () => {
|
||||
const handshake = varint.concat([
|
||||
varint.encodeInt(0),
|
||||
varint.encodeInt(protocolVersion),
|
||||
varint.encodeInt(virtualHost.length),
|
||||
varint.encodeString(virtualHost),
|
||||
varint.encodeUShort(port),
|
||||
varint.encodeInt(1),
|
||||
]);
|
||||
|
||||
// #setNoDelay instantly flushes data during read/writes
|
||||
// This prevents the runtime from delaying the write at all
|
||||
socket.setNoDelay(true);
|
||||
socket.write(handshake);
|
||||
|
||||
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)
|
||||
]);
|
||||
const request = varint.concat([varint.encodeInt(0)]);
|
||||
|
||||
socket.write(handshake);
|
||||
socket.write(request);
|
||||
});
|
||||
|
||||
const request = varint.concat([
|
||||
varint.encodeInt(0)
|
||||
]);
|
||||
let incomingBuffer = Buffer.alloc(0);
|
||||
|
||||
socket.write(request);
|
||||
});
|
||||
socket.on("data", (data) => {
|
||||
incomingBuffer = Buffer.concat([incomingBuffer, data]);
|
||||
|
||||
let incomingBuffer = Buffer.alloc(0);
|
||||
// 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://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Data_types#VarInt_and_VarLong
|
||||
if (incomingBuffer.length < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
let offset = 0;
|
||||
const packetLength = varint.decodeInt(incomingBuffer, offset);
|
||||
// Ensure incomingBuffer contains the full response
|
||||
if (incomingBuffer.length - offset < packetLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure incomingBuffer contains the full response
|
||||
if (incomingBuffer.length - offset < packetLength) {
|
||||
return;
|
||||
}
|
||||
const packetId = varint.decodeInt(
|
||||
incomingBuffer,
|
||||
varint.decodeLength(packetLength)
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
try {
|
||||
const message = JSON.parse(response);
|
||||
cb(null, message);
|
||||
closeSocket();
|
||||
cb(message, null);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
}
|
||||
} else {
|
||||
handleError(new Error("Received unexpected packet"));
|
||||
}
|
||||
});
|
||||
|
||||
// Close the socket and clear the timeout task
|
||||
closeSocket();
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
}
|
||||
} else {
|
||||
handleError(new Error('Received unexpected packet'));
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', handleError);
|
||||
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.
|
||||
*
|
||||
* The optional `options` argument can be an object with a `port` (default is `25565`) or/and `timeout` (default is `5000`) or/and `protocolVersion` (default is `-1`) property.
|
||||
* @param {string} host The Java server address.
|
||||
* @param {import('../types/index.js').PingOptions} options The configuration for pinging Minecraft Java server.
|
||||
* @returns {Promise<import('../types/lib/java.js').JavaPingResponse>}
|
||||
* @returns {Promise<import('../types/index.js').JavaPingResponse>}
|
||||
*/
|
||||
export function pingJava(host, options = {}) {
|
||||
if (!host) throw new Error('Host argument is not provided');
|
||||
if (!host) throw new Error("Host argument is not provided");
|
||||
|
||||
const { port = 25565, timeout = 5000 } = options;
|
||||
const {
|
||||
port = 25565,
|
||||
timeout = 5000,
|
||||
protocolVersion = -1,
|
||||
virtualHost = null,
|
||||
} = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ping(host, port, (err, res) => {
|
||||
err ? reject(err) : resolve(res);
|
||||
}, timeout);
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
ping(
|
||||
host,
|
||||
virtualHost || host,
|
||||
port,
|
||||
(res, err) => {
|
||||
err ? reject(err) : resolve(res);
|
||||
},
|
||||
timeout,
|
||||
protocolVersion
|
||||
);
|
||||
});
|
||||
}
|
173
lib/varint.js
173
lib/varint.js
@ -1,90 +1,123 @@
|
||||
// https://wiki.vg/Data_types
|
||||
// https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol#Data_types
|
||||
|
||||
/**
|
||||
* A utility object for encoding and decoding varints.
|
||||
*/
|
||||
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;
|
||||
/**
|
||||
* Encodes an integer value into a varint byte buffer.
|
||||
* @param {number} val - The integer value to encode.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
encodeInt: (val) => {
|
||||
// "VarInts are never longer than 5 bytes"
|
||||
// https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Data_types#VarInt_and_VarLong
|
||||
const buf = Buffer.alloc(5);
|
||||
let written = 0;
|
||||
|
||||
while (true) {
|
||||
if ((val & 0xFFFFFF80) === 0) {
|
||||
buf.writeUInt8(val, written++);
|
||||
break;
|
||||
} else {
|
||||
buf.writeUInt8(val & 0x7F | 0x80, written++);
|
||||
val >>>= 7;
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
const byte = val & 0x7f;
|
||||
val >>>= 7;
|
||||
|
||||
return buf.slice(0, written);
|
||||
},
|
||||
if (val === 0) {
|
||||
buf.writeUInt8(byte, written++);
|
||||
break;
|
||||
}
|
||||
|
||||
encodeString: (val) => {
|
||||
return Buffer.from(val, 'utf-8');
|
||||
},
|
||||
buf.writeUInt8(byte | 0x80, written++);
|
||||
}
|
||||
|
||||
encodeUShort: (val) => {
|
||||
return Buffer.from([val >> 8, val & 0xFF]);
|
||||
},
|
||||
return buf.subarray(0, written);
|
||||
},
|
||||
|
||||
concat: (chunks) => {
|
||||
let length = 0;
|
||||
/**
|
||||
* 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");
|
||||
},
|
||||
|
||||
for (const chunk of chunks) {
|
||||
length += chunk.length;
|
||||
}
|
||||
/**
|
||||
* 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]);
|
||||
},
|
||||
|
||||
const buffer = [
|
||||
varint.encodeInt(length),
|
||||
...chunks
|
||||
];
|
||||
/**
|
||||
* 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;
|
||||
|
||||
return Buffer.concat(buffer);
|
||||
},
|
||||
for (const chunk of chunks) {
|
||||
length += chunk.length;
|
||||
}
|
||||
|
||||
decodeInt: (buffer, offset) => {
|
||||
let val = 0;
|
||||
let count = 0;
|
||||
const buffer = [varint.encodeInt(length), ...chunks];
|
||||
|
||||
while (true) {
|
||||
const b = buffer.readUInt8(offset++);
|
||||
return Buffer.concat(buffer);
|
||||
},
|
||||
|
||||
val |= (b & 0x7F) << count++ * 7;
|
||||
/**
|
||||
* Decodes a varint integer value from a 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) => {
|
||||
// Fast path for single-byte varints
|
||||
const firstByte = buffer.readUInt8(offset);
|
||||
if (firstByte < 0x80) {
|
||||
return firstByte;
|
||||
}
|
||||
|
||||
if ((b & 0x80) != 128) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let val = firstByte & 0x7f;
|
||||
let position = 7;
|
||||
|
||||
return val;
|
||||
},
|
||||
while (position < 32) {
|
||||
const byte = buffer.readUInt8(++offset);
|
||||
val |= (byte & 0x7f) << position;
|
||||
|
||||
// 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);
|
||||
if ((byte & 0x80) === 0) {
|
||||
return val;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
position += 7;
|
||||
}
|
||||
|
||||
throw new Error("VarInt is too big");
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates how many bytes are needed to encode a number as a VarInt
|
||||
* VarInts use a variable number of bytes to efficiently encode integers
|
||||
* Each byte uses 7 bits for the value and 1 bit to indicate if more bytes follow
|
||||
* VarInts are never longer than 5 bytes
|
||||
*
|
||||
* @param {number} val - The number to calculate the VarInt length for
|
||||
* @returns {1|2|3|4|5} The number of bytes needed to encode the value
|
||||
*/
|
||||
decodeLength: (val) => {
|
||||
// Using bit shifts to calculate power of 2 thresholds
|
||||
// 1 << 7 = 2^7 = 128 - Numbers below this fit in 1 byte
|
||||
// 1 << 14 = 2^14 = 16,384 - Numbers below this fit in 2 bytes
|
||||
// 1 << 21 = 2^21 = 2,097,152 - Numbers below this fit in 3 bytes
|
||||
// 1 << 28 = 2^28 = 268,435,456 - Numbers below this fit in 4 bytes
|
||||
// Any larger number needs 5 bytes (maximum VarInt size)
|
||||
|
||||
if (val < 1 << 7) return 1;
|
||||
if (val < 1 << 14) return 2;
|
||||
if (val < 1 << 21) return 3;
|
||||
if (val < 1 << 28) return 4;
|
||||
return 5;
|
||||
},
|
||||
};
|
||||
|
||||
export default varint;
|
||||
|
1499
package-lock.json
generated
Normal file
1499
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@ -1,22 +1,29 @@
|
||||
{
|
||||
"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": "1.7.0-beta.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"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/minescope/mineping.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [],
|
||||
"author": {
|
||||
"name": "Timofey Gelazoniya",
|
||||
"email": "timofey@z4n.me",
|
||||
"url": "https://zeldon.ru"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"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;
|
||||
}
|
112
test/java.test.js
Normal file
112
test/java.test.js
Normal file
@ -0,0 +1,112 @@
|
||||
import net from "node:net";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { pingJava } from "../lib/java.js";
|
||||
import varint from "../lib/varint.js";
|
||||
|
||||
vi.mock("node:net");
|
||||
|
||||
describe("pingJava", () => {
|
||||
let mockSocket;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockHandlers = {};
|
||||
mockSocket = {
|
||||
write: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
setNoDelay: vi.fn(),
|
||||
on: vi.fn((event, handler) => (mockHandlers[event] = handler)),
|
||||
emit: vi.fn((event, ...args) => mockHandlers[event]?.(...args)),
|
||||
};
|
||||
net.createConnection = vi.fn().mockReturnValue(mockSocket);
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should ping a server and handle a chunked response", async () => {
|
||||
const host = "mc.hypixel.net";
|
||||
const options = {
|
||||
port: 25565,
|
||||
timeout: 5000,
|
||||
protocolVersion: 765,
|
||||
virtualHost: "mc.hypixel.net",
|
||||
};
|
||||
|
||||
const pingPromise = pingJava(host, options);
|
||||
|
||||
mockSocket.emit("connect");
|
||||
|
||||
expect(net.createConnection).toHaveBeenCalledWith({
|
||||
host,
|
||||
port: options.port,
|
||||
});
|
||||
expect(mockSocket.setNoDelay).toHaveBeenCalledWith(true);
|
||||
expect(mockSocket.write).toHaveBeenCalledTimes(2);
|
||||
|
||||
const mockResponse = {
|
||||
version: { name: "1.21", protocol: 765 },
|
||||
players: { max: 20, online: 5, sample: [] },
|
||||
description: "A Minecraft Server",
|
||||
favicon: "data:image/png;base64,iVBORw0KGgo...",
|
||||
};
|
||||
const fullPacket = createMockJavaResponse(mockResponse);
|
||||
const chunk1 = fullPacket.subarray(0, 10);
|
||||
const chunk2 = fullPacket.subarray(10);
|
||||
|
||||
mockSocket.emit("data", chunk1);
|
||||
mockSocket.emit("data", chunk2);
|
||||
|
||||
const result = await pingPromise;
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockSocket.destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
it("should throw an error if host is not provided", () => {
|
||||
expect(() => pingJava(null)).toThrow("Host argument is not provided");
|
||||
});
|
||||
|
||||
it("should reject on socket timeout before data is received", async () => {
|
||||
const pingPromise = pingJava("localhost", { timeout: 1000 });
|
||||
mockSocket.emit("connect");
|
||||
|
||||
// Advance time to trigger the timeout
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
await expect(pingPromise).rejects.toThrow("Socket timeout");
|
||||
expect(mockSocket.destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should reject on connection error", async () => {
|
||||
const pingPromise = pingJava("localhost");
|
||||
|
||||
// Simulate a connection refusal
|
||||
mockSocket.emit("error", new Error("ECONNREFUSED"));
|
||||
|
||||
await expect(pingPromise).rejects.toThrow("ECONNREFUSED");
|
||||
});
|
||||
|
||||
it("should only reject once, even if multiple errors occur", async () => {
|
||||
const pingPromise = pingJava("localhost");
|
||||
|
||||
// Fire two errors back-to-back
|
||||
mockSocket.emit("error", new Error("First error"));
|
||||
mockSocket.emit("error", new Error("Second error"));
|
||||
|
||||
await expect(pingPromise).rejects.toThrow("First error");
|
||||
expect(mockSocket.destroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createMockJavaResponse(response) {
|
||||
const jsonString = JSON.stringify(response);
|
||||
const jsonBuffer = Buffer.from(jsonString, "utf8");
|
||||
const responseLength = varint.encodeInt(jsonBuffer.length);
|
||||
const packetId = varint.encodeInt(0);
|
||||
const packetData = Buffer.concat([packetId, responseLength, jsonBuffer]);
|
||||
const packetLength = varint.encodeInt(packetData.length);
|
||||
return Buffer.concat([packetLength, packetData]);
|
||||
}
|
76
test/varint.test.js
Normal file
76
test/varint.test.js
Normal file
@ -0,0 +1,76 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import 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.encodeInt(value);
|
||||
const decoded = varint.decodeInt(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]);
|
||||
expect(varint.decodeInt(buffer, 1)).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.decodeInt(invalidBuffer, 0)).toThrow(
|
||||
"VarInt is too big"
|
||||
);
|
||||
});
|
||||
|
||||
it("should correctly predict the encoded length of a varint", () => {
|
||||
const boundaries = [0, 127, 128, 16383, 16384, 2097151, 2097152];
|
||||
boundaries.forEach((value) => {
|
||||
const predictedLength = varint.decodeLength(value);
|
||||
const actualLength = varint.encodeInt(value).length;
|
||||
expect(predictedLength).toBe(actualLength);
|
||||
});
|
||||
});
|
||||
|
||||
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 and parse a Minecraft handshake packet", () => {
|
||||
const protocolVersion = -1;
|
||||
const virtualHost = "mc.example.com";
|
||||
const port = 25565;
|
||||
|
||||
const payload = Buffer.concat([
|
||||
varint.encodeInt(0),
|
||||
varint.encodeInt(protocolVersion),
|
||||
varint.encodeInt(virtualHost.length),
|
||||
varint.encodeString(virtualHost),
|
||||
varint.encodeUShort(port),
|
||||
varint.encodeInt(1),
|
||||
]);
|
||||
|
||||
const finalPacket = varint.concat([payload]);
|
||||
|
||||
const decodedPacketLength = varint.decodeInt(finalPacket, 0);
|
||||
expect(decodedPacketLength).toBe(payload.length);
|
||||
|
||||
const lengthOfPacketLength = varint.decodeLength(decodedPacketLength);
|
||||
const decodedPayload = finalPacket.subarray(lengthOfPacketLength);
|
||||
expect(decodedPayload).toEqual(payload);
|
||||
});
|
||||
});
|
2
types/index.d.ts
vendored
2
types/index.d.ts
vendored
@ -1,2 +0,0 @@
|
||||
export { pingJava } from "./lib/java.js";
|
||||
export { pingBedrock } from "./lib/bedrock.js";
|
49
types/lib/bedrock.d.ts
vendored
49
types/lib/bedrock.d.ts
vendored
@ -1,49 +0,0 @@
|
||||
/**
|
||||
* @param port The server port.
|
||||
* @param timeout The read/write socket timeout.
|
||||
*/
|
||||
export type PingOptions = {
|
||||
port: number,
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
export type BedrockPingResponse = {
|
||||
version: {
|
||||
name: string;
|
||||
protocol: string;
|
||||
};
|
||||
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/24a48802300f988d3ae520edbeb4f3e12820dcc9/lib/java.js#L117)
|
||||
*/
|
||||
export function pingBedrock(host: string, options?: PingOptions): Promise<BedrockPingResponse>;
|
||||
|
71
types/lib/java.d.ts
vendored
71
types/lib/java.d.ts
vendored
@ -1,71 +0,0 @@
|
||||
import { PingOptions } from "./bedrock";
|
||||
|
||||
/**
|
||||
* JSON format chat component used for description field.
|
||||
* @see https://wiki.vg/Chat
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* `JSON Response` field of Response packet.
|
||||
* @see https://wiki.vg/Server_List_Ping#Response
|
||||
*/
|
||||
export type JavaPingResponse = {
|
||||
version: {
|
||||
name: string;
|
||||
protocol: number;
|
||||
};
|
||||
players: {
|
||||
max: number;
|
||||
online: number;
|
||||
sample?: SampleProp[];
|
||||
};
|
||||
description: string | ChatComponent;
|
||||
favicon?: string;
|
||||
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/8c84925ef7f5c420a7ef52740cba027491e82934/lib/bedrock.js#L158)
|
||||
*/
|
||||
export function pingJava(host: string, options?: PingOptions): Promise<JavaPingResponse>;
|
||||
|
10
types/lib/varint.d.ts
vendored
10
types/lib/varint.d.ts
vendored
@ -1,10 +0,0 @@
|
||||
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;
|
||||
}
|
Reference in New Issue
Block a user