mirror of
https://github.com/minescope/mineping.git
synced 2025-07-10 08:14:35 +03:00
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.
This commit is contained in:
225
lib/varint.js
225
lib/varint.js
@ -1,123 +1,138 @@
|
||||
// https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol#Data_types
|
||||
// https://minecraft.wiki/w/Java_Edition_protocol/Data_types
|
||||
|
||||
"use strict";
|
||||
|
||||
export const ERR_VARINT_BUFFER_UNDERFLOW = "VARINT_BUFFER_UNDERFLOW";
|
||||
export const ERR_VARINT_MALFORMED = "VARINT_MALFORMED";
|
||||
export const ERR_VARINT_ENCODE_TOO_LARGE = "VARINT_ENCODE_TOO_LARGE";
|
||||
|
||||
/**
|
||||
* A utility object for encoding and decoding varints.
|
||||
* Creates a custom error object with a code property.
|
||||
* @param {string} message The error message
|
||||
* @param {string} code The error code
|
||||
* @returns {Error}
|
||||
*/
|
||||
const varint = {
|
||||
/**
|
||||
* 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;
|
||||
function createVarIntError(message, code) {
|
||||
const err = new Error(message);
|
||||
err.code = code;
|
||||
return err;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const byte = val & 0x7f;
|
||||
val >>>= 7;
|
||||
/**
|
||||
* 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;
|
||||
|
||||
if (val === 0) {
|
||||
buf.writeUInt8(byte, written++);
|
||||
break;
|
||||
}
|
||||
while (true) {
|
||||
const byte = val & 0x7f;
|
||||
val >>>= 7;
|
||||
|
||||
buf.writeUInt8(byte | 0x80, written++);
|
||||
if (val === 0) {
|
||||
buf.writeUInt8(byte, written++);
|
||||
break;
|
||||
}
|
||||
|
||||
return buf.subarray(0, written);
|
||||
},
|
||||
buf.writeUInt8(byte | 0x80, written++);
|
||||
|
||||
/**
|
||||
* 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");
|
||||
},
|
||||
if (written >= 5 && val > 0) {
|
||||
throw createVarIntError(
|
||||
"Value too large for a 5-byte VarInt",
|
||||
ERR_VARINT_ENCODE_TOO_LARGE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
},
|
||||
return buf.subarray(0, written);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
|
||||
for (const chunk of chunks) {
|
||||
length += chunk.length;
|
||||
/**
|
||||
* 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 buffer = [varint.encodeInt(length), ...chunks];
|
||||
const byte = buffer.readUInt8(currentOffset);
|
||||
bytesRead++;
|
||||
currentOffset++;
|
||||
|
||||
return Buffer.concat(buffer);
|
||||
},
|
||||
val |= (byte & 0x7f) << position;
|
||||
position += 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 ((byte & 0x80) === 0) {
|
||||
return { value: val, bytesRead: bytesRead };
|
||||
}
|
||||
}
|
||||
|
||||
let val = firstByte & 0x7f;
|
||||
let position = 7;
|
||||
|
||||
while (position < 32) {
|
||||
const byte = buffer.readUInt8(++offset);
|
||||
val |= (byte & 0x7f) << position;
|
||||
|
||||
if ((byte & 0x80) === 0) {
|
||||
return val;
|
||||
}
|
||||
|
||||
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;
|
||||
throw createVarIntError(
|
||||
"VarInt is too big or malformed: 5 bytes read with continuation bit still set.",
|
||||
ERR_VARINT_MALFORMED
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user