initial commit

This commit is contained in:
Timofey Gelazoniya 2022-01-10 23:21:47 +03:00
commit e9cd348935
Signed by: zeldon
GPG Key ID: 047886915281DD2A
15 changed files with 676 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Timofey Gelazoniya
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# mineping
Collect information about Minecraft (both Java and Bedrock) using **[Node.js](http://nodejs.org)**.
## Description
`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.
## Requirements
> **[Node.js](https://nodejs.org/) 14 or newer is required**
## Example
Ping a Java server with default options:
```js
import { pingJava } from 'mineping'
const data = await pingJava('mc.hypixel.net');
console.log(data);
```
Ping a Bedrock server with custom options:
```js
import { pingBedrock } from 'mineping'
const data = await pingBedrock('mco.mineplex.com', {
port: 19132,
timeout: 500
})
```
> More complex example can be found in the `examples` folder!
## Acknowledgements
- [mcping](https://github.com/Scetch/mcping) crate for Rust
- [mcping-js](https://github.com/Cryptkeeper/mcping-js) library for quering Minecraft Java Edition servers

5
SECURITY.md Normal file
View File

@ -0,0 +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.
Please don't open an issue to or discuss this security vulnerability in a public place. Thanks for understanding!

75
example/cli.js Normal file
View File

@ -0,0 +1,75 @@
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;
}

2
index.js Normal file
View File

@ -0,0 +1,2 @@
export { pingJava } from './lib/java.js';
export { pingBedrock } from './lib/bedrock.js';

168
lib/bedrock.js Normal file
View File

@ -0,0 +1,168 @@
/**
* 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
*/
'use strict';
import dgram from 'dgram';
import ByteBuffer from 'bytebuffer';
const START_TIME = new Date().getTime();
/**
* Decode Unconnected Ping
* @param {number} pingId
* @returns {import('bytebuffer')}
* @see https://wiki.vg/Raknet_Protocol#Unconnected_Ping
*/
const UNCONNECTED_PING = (pingId) => {
// 0x01
const bb = new ByteBuffer();
bb.buffer[0] = 0x01;
bb.offset = 1;
return bb.writeLong(pingId).append('00ffff00fefefefefdfdfdfd12345678', 'hex').writeLong(0).flip().compact();
};
/**
* Decode Unconnected Pong
* @param {import('bytebuffer')} buffer
* @see https://wiki.vg/Raknet_Protocol#Unconnected_Pong
*/
const UNCONNECTED_PONG = (buffer) => {
// 0x1c
buffer.offset = 1;
const pingId = buffer.readLong();
const serverId = buffer.readLong();
const offset = buffer.offset += 16;
const nameLength = buffer.readShort();
let advertiseStr;
try {
advertiseStr = buffer.readUTF8String(nameLength);
} catch (err) {
advertiseStr = buffer.readUTF8String(parseInt(err.message.substr(err.message.indexOf(',') + 2, 3)));
}
advertiseStr = advertiseStr.split(/;/g);
const gameId = advertiseStr[0];
const description = advertiseStr[1];
const protocolVersion = advertiseStr[2];
const gameVersion = advertiseStr[3];
const currentPlayers = advertiseStr[4];
const maxPlayers = advertiseStr[5];
const name = advertiseStr[7];
const mode = advertiseStr[8];
return {
pingId,
advertiseStr,
serverId,
offset,
gameId,
description,
protocolVersion,
gameVersion,
currentPlayers,
maxPlayers,
name,
mode
};
};
function ping(host, port = 19132, cb, timeout = 5000) {
const socket = dgram.createSocket('udp4');
// 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.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.buffer, 0, ping.buffer.length, port, host);
} catch (err) {
handleError(err);
}
socket.on('message', (msg) => {
const buffer = new ByteBuffer().append(msg, 'hex').flip();
const id = buffer.buffer[0];
switch (id) {
// https://wiki.vg/Raknet_Protocol#Unconnected_Ping
case 0x1c: {
const pong = UNCONNECTED_PONG(buffer);
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
};
// Close the socket and clear the timeout task
// This is a general cleanup for success conditions
closeSocket();
cb(null, clientData);
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/index.js').PingOptions} options The configuration for pinging Minecraft Bedrock server.
* @returns {Promise<import('../types/lib/bedrock.js').BedrockPingResponse>}
*/
export function pingBedrock(host, options = {}) {
if (!host) throw new Error('Host argument is not provided');
const { port = 19132, timeout = 5000 } = options;
return new Promise((resilve, reject) => {
ping(host, port, (err, res) => {
err ? reject(err) : resilve(res);
}, timeout);
});
}

127
lib/java.js Normal file
View File

@ -0,0 +1,127 @@
/**
* Implementation of the Java Minecraft ping protocol.
* @see https://wiki.vg/Server_List_Ping
*/
'use strict';
import net from 'net';
import varint from './varint.js';
const PROTOCOL_VERSION = 0;
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);
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;
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);
}
/**
* 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>}
*/
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);
});
}

90
lib/varint.js Normal file
View File

@ -0,0 +1,90 @@
// https://wiki.vg/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;
while (true) {
if ((val & 0xFFFFFF80) === 0) {
buf.writeUInt8(val, written++);
break;
} else {
buf.writeUInt8(val & 0x7F | 0x80, written++);
val >>>= 7;
}
}
return buf.slice(0, written);
},
encodeString: (val) => {
return Buffer.from(val, 'utf-8');
},
encodeUShort: (val) => {
return Buffer.from([val >> 8, val & 0xFF]);
},
concat: (chunks) => {
let length = 0;
for (const chunk of chunks) {
length += chunk.length;
}
const buffer = [
varint.encodeInt(length),
...chunks
];
return Buffer.concat(buffer);
},
decodeInt: (buffer, offset) => {
let val = 0;
let count = 0;
while (true) {
const b = buffer.readUInt8(offset++);
val |= (b & 0x7F) << count++ * 7;
if ((b & 0x80) != 128) {
break;
}
}
return val;
},
// 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);
return (
val < N1 ? 1
: val < N2 ? 2
: val < N3 ? 3
: val < N4 ? 4
: val < N5 ? 5
: val < N6 ? 6
: val < N7 ? 7
: val < N8 ? 8
: val < N9 ? 9
: 10
);
}
};
export default varint;

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "mineping",
"version": "1.0.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"
},
"type": "module",
"engines": {
"node": ">=14"
},
"license": "MIT",
"dependencies": {
"bytebuffer": "^5.0.1"
}
}

11
types/index.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
export { pingJava } from "./lib/java.js";
export { pingBedrock } from "./lib/bedrock.js";
/**
* @param port The server port.
* @param timeout The read/write socket timeout.
*/
export type PingOptions = {
port: number,
timeout: number;
};

41
types/lib/bedrock.d.ts vendored Normal file
View File

@ -0,0 +1,41 @@
import { PingOptions } from "..";
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 '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'
* }
* ```
*/
export function pingBedrock(host: string, options?: PingOptions): Promise<BedrockPingResponse>;

53
types/lib/java.d.ts vendored Normal file
View File

@ -0,0 +1,53 @@
import { PingOptions } from "..";
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;
favicon: string;
};
/**
* 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 '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...
}
* ```
*/
export function pingJava(host: string, options?: PingOptions): Promise<JavaPingResponse>;

10
types/lib/varint.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
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;
}

15
yarn.lock Normal file
View File

@ -0,0 +1,15 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
bytebuffer@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd"
integrity sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0=
dependencies:
long "~3"
long@~3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b"
integrity sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=