mirror of
https://github.com/minescope/mineping.git
synced 2025-07-13 12:14:36 +03:00
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.
139 lines
3.7 KiB
JavaScript
139 lines
3.7 KiB
JavaScript
// https://minecraft.wiki/w/Java_Edition_protocol/Data_types
|
|
|
|
"use strict";
|
|
|
|
export const ERR_VARINT_BUFFER_UNDERFLOW = "VARINT_BUFFER_UNDERFLOW";
|
|
export const ERR_VARINT_MALFORMED = "VARINT_MALFORMED";
|
|
export const ERR_VARINT_ENCODE_TOO_LARGE = "VARINT_ENCODE_TOO_LARGE";
|
|
|
|
/**
|
|
* Creates a custom error object with a code property.
|
|
* @param {string} message The error message
|
|
* @param {string} code The error code
|
|
* @returns {Error}
|
|
*/
|
|
function createVarIntError(message, code) {
|
|
const err = new Error(message);
|
|
err.code = code;
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* 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 {Error} if the value is too large to be encoded
|
|
*/
|
|
export function encodeVarInt(value) {
|
|
const buf = Buffer.alloc(5);
|
|
let written = 0;
|
|
let val = value;
|
|
|
|
while (true) {
|
|
const byte = val & 0x7f;
|
|
val >>>= 7;
|
|
|
|
if (val === 0) {
|
|
buf.writeUInt8(byte, written++);
|
|
break;
|
|
}
|
|
|
|
buf.writeUInt8(byte | 0x80, written++);
|
|
|
|
if (written >= 5 && val > 0) {
|
|
throw createVarIntError(
|
|
"Value too large for a 5-byte VarInt",
|
|
ERR_VARINT_ENCODE_TOO_LARGE
|
|
);
|
|
}
|
|
}
|
|
|
|
return buf.subarray(0, written);
|
|
}
|
|
|
|
/**
|
|
* Encodes a string into a UTF-8 buffer.
|
|
* @param {string} value The string to encode
|
|
* @returns {Buffer}
|
|
*/
|
|
export function encodeString(value) {
|
|
return Buffer.from(value, "utf-8");
|
|
}
|
|
|
|
/**
|
|
* Encodes an unsigned short (16-bit big-endian) into a 2-byte buffer.
|
|
* @param {number} value The number to encode
|
|
* @returns {Buffer}
|
|
*/
|
|
export function encodeUShort(value) {
|
|
const buf = Buffer.alloc(2);
|
|
buf.writeUInt16BE(value, 0);
|
|
return buf;
|
|
}
|
|
|
|
/**
|
|
* Creates a Minecraft-style packet by concatenating chunks and prefixing the total length as a VarInt.
|
|
* @param {Buffer[]} chunks An array of buffers to include in the packet payload
|
|
* @returns {Buffer} The complete packet with its length prefix
|
|
*/
|
|
export function concatPackets(chunks) {
|
|
const payload = Buffer.concat(chunks);
|
|
const lengthPrefix = encodeVarInt(payload.length);
|
|
return Buffer.concat([lengthPrefix, payload]);
|
|
}
|
|
|
|
/**
|
|
* Decodes a VarInt from a buffer.
|
|
* Returns the decoded value and the number of bytes it consumed.
|
|
* @param {Buffer} buffer The buffer to read from
|
|
* @param {number} [offset=0] The starting offset in the buffer
|
|
* @returns {{ value: number, bytesRead: number }}
|
|
* @throws {Error} if the buffer is too short or the VarInt is malformed
|
|
*/
|
|
export function decodeVarInt(buffer, offset = 0) {
|
|
if (offset >= buffer.length) {
|
|
throw createVarIntError(
|
|
"Buffer underflow: Cannot decode VarInt at or beyond buffer length.",
|
|
ERR_VARINT_BUFFER_UNDERFLOW
|
|
);
|
|
}
|
|
|
|
// Fast path for single-byte VarInts, which are very common.
|
|
const firstByte = buffer.readUInt8(offset);
|
|
if ((firstByte & 0x80) === 0) {
|
|
return { value: firstByte, bytesRead: 1 };
|
|
}
|
|
|
|
let val = firstByte & 0x7f; // Get the first 7 bits
|
|
let position = 7; // Bit position for the next byte's data
|
|
let bytesRead = 1; // We've read one byte so far
|
|
let currentOffset = offset + 1; // Start reading from the next
|
|
|
|
// Max 4 more bytes (total 5 bytes for a VarInt)
|
|
for (let i = 0; i < 4; i++) {
|
|
if (currentOffset >= buffer.length) {
|
|
throw createVarIntError(
|
|
"Buffer underflow: Incomplete VarInt, expected more bytes.",
|
|
ERR_VARINT_BUFFER_UNDERFLOW
|
|
);
|
|
}
|
|
|
|
const byte = buffer.readUInt8(currentOffset);
|
|
bytesRead++;
|
|
currentOffset++;
|
|
|
|
val |= (byte & 0x7f) << position;
|
|
position += 7;
|
|
|
|
if ((byte & 0x80) === 0) {
|
|
return { value: val, bytesRead: bytesRead };
|
|
}
|
|
}
|
|
|
|
throw createVarIntError(
|
|
"VarInt is too big or malformed: 5 bytes read with continuation bit still set.",
|
|
ERR_VARINT_MALFORMED
|
|
);
|
|
}
|