@deitylamb/mcping 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import { ping } from "./index.js";
3
+ import { mcToAnsi } from "./ansi.js";
4
+
5
+ const colors = {
6
+ reset: "\x1b[0m",
7
+ bright: "\x1b[1m",
8
+ cyan: "\x1b[36m",
9
+ green: "\x1b[32m",
10
+ yellow: "\x1b[33m",
11
+ magenta: "\x1b[35m",
12
+ white: "\x1b[37m",
13
+ gray: "\x1b[90m",
14
+ };
15
+
16
+ /**
17
+ * Poor man's argument parser
18
+ */
19
+ function parseArgs(args: string[]) {
20
+ const result: {
21
+ target?: string;
22
+ type?: "java" | "bedrock";
23
+ timeout?: number;
24
+ } = {};
25
+
26
+ for (let i = 0; i < args.length; i++) {
27
+ const arg = args[i];
28
+ if (arg === "--type" || arg === "-t") {
29
+ const val = args[++i]?.toLowerCase();
30
+ if (val === "java" || val === "bedrock") {
31
+ result.type = val;
32
+ }
33
+ } else if (arg === "--timeout") {
34
+ const val = parseInt(args[++i]);
35
+ if (!isNaN(val)) {
36
+ result.timeout = val;
37
+ }
38
+ } else if (!result.target && !arg.startsWith("-")) {
39
+ result.target = arg;
40
+ }
41
+ }
42
+ return result;
43
+ }
44
+
45
+ async function main() {
46
+ const args = process.argv.slice(2);
47
+ const parsed = parseArgs(args);
48
+
49
+ if (!parsed.target) {
50
+ console.log(
51
+ `\n${colors.bright}Usage:${colors.reset} mcping <host[:port]> [options]`,
52
+ );
53
+ console.log(`\n${colors.bright}Options:${colors.reset}`);
54
+ console.log(` --type, -t <java|bedrock> Force protocol type`);
55
+ console.log(
56
+ ` --timeout <ms> Connection timeout (default: 5000)`,
57
+ );
58
+ console.log(`\n${colors.bright}Examples:${colors.reset}`);
59
+ console.log(` mcping mc.hypixel.net`);
60
+ console.log(` mcping geo.hivebedrock.network --type bedrock`);
61
+ console.log(` mcping play.example.com --timeout 2000\n`);
62
+ process.exit(1);
63
+ }
64
+
65
+ try {
66
+ const res = await ping(parsed.target, {
67
+ type: parsed.type,
68
+ timeout: parsed.timeout,
69
+ });
70
+
71
+ // Header Line: Input -> IP:Port
72
+ const resolvedStr = `${res.target.ip}:${res.target.port}`;
73
+ const targetDisplay =
74
+ parsed.target === resolvedStr
75
+ ? parsed.target
76
+ : `${parsed.target} ${colors.gray}→${colors.reset} ${resolvedStr}`;
77
+
78
+ console.log(`\n ${colors.bright}${targetDisplay}${colors.reset}`);
79
+
80
+ // Edition line
81
+ const editionStr = `${colors.bright}${res.type.toUpperCase()}${colors.reset}`;
82
+ const versionStr = `${colors.gray}${res.version.name}${colors.reset}`;
83
+ const latencyStr =
84
+ res.type === "java"
85
+ ? ` ${colors.magenta}${res.latency}ms${colors.reset}`
86
+ : "";
87
+
88
+ console.log(
89
+ ` ${editionStr} ${colors.gray}•${colors.reset} ${versionStr}${latencyStr}`,
90
+ );
91
+
92
+ // Players line
93
+ console.log(
94
+ ` ${colors.bright}${res.players.online}${colors.reset} ${colors.gray}/ ${res.players.max} players${colors.reset}`,
95
+ );
96
+
97
+ // MOTD with indent and colors
98
+ const coloredMotd = mcToAnsi(res.motd);
99
+ const cleanMotd = coloredMotd
100
+ .trim()
101
+ .split("\n")
102
+ .map((line) => line.trim())
103
+ .filter((line) => line.length > 0)
104
+ .join(`\n `);
105
+
106
+ console.log(`\n ${cleanMotd}${colors.reset}\n`);
107
+ } catch (err: any) {
108
+ console.error(`\n ${colors.yellow}Error:${colors.reset} ${err.message}\n`);
109
+ process.exit(1);
110
+ }
111
+ }
112
+
113
+ main();
package/src/index.ts ADDED
@@ -0,0 +1,143 @@
1
+ import { resolveTarget } from "./target.js";
2
+ import { pingJava, JavaRawResponse } from "./java-ping.js";
3
+ import { pingBedrock, BedrockRawResponse } from "./bedrock-ping.js";
4
+ import { parseChat } from "./chat.js";
5
+ import * as cache from "./cache.js";
6
+
7
+ export interface PingOptions {
8
+ timeout?: number;
9
+ type?: "java" | "bedrock" | null;
10
+ cache?: cache.CacheOptions;
11
+ }
12
+
13
+ export interface PingResponse {
14
+ type: "java" | "bedrock";
15
+ version: {
16
+ name: string;
17
+ protocol: number;
18
+ };
19
+ players: {
20
+ online: number;
21
+ max: number;
22
+ };
23
+ motd: string;
24
+ latency: number;
25
+ target: {
26
+ host: string;
27
+ port: number;
28
+ ip: string;
29
+ };
30
+ raw: JavaRawResponse | BedrockRawResponse;
31
+ cached?: boolean;
32
+ }
33
+
34
+ export async function ping(
35
+ target: string,
36
+ options?: PingOptions,
37
+ ): Promise<PingResponse> {
38
+ const timeout = options?.timeout || 5000;
39
+ const type = options?.type || null;
40
+ const cacheOptions = options?.cache;
41
+
42
+ if (cacheOptions) {
43
+ const cached = cache.getCache(target, cacheOptions);
44
+ if (cached) {
45
+ if (
46
+ cacheOptions.strategy === "swr" &&
47
+ cache.isExpired(target, cacheOptions)
48
+ ) {
49
+ // Refresh in background
50
+ pingServer(target, timeout, type)
51
+ .then((refreshed) => cache.setCache(target, refreshed))
52
+ .catch(() => {});
53
+ }
54
+ return cached;
55
+ }
56
+ }
57
+
58
+ const result = await pingServer(target, timeout, type);
59
+ if (cacheOptions) {
60
+ cache.setCache(target, result);
61
+ }
62
+
63
+ return result;
64
+ }
65
+
66
+ async function pingServer(
67
+ target: string,
68
+ timeout: number,
69
+ type: "java" | "bedrock" | null,
70
+ ): Promise<PingResponse> {
71
+ const resolved = await resolveTarget(target, type);
72
+
73
+ let javaError: any;
74
+ if (type === "java" || type === null) {
75
+ try {
76
+ const { response, latency } = await pingJava(
77
+ resolved.host,
78
+ resolved.port,
79
+ timeout,
80
+ );
81
+ return {
82
+ type: "java",
83
+ version: {
84
+ name: response.version.name,
85
+ protocol: response.version.protocol,
86
+ },
87
+ players: {
88
+ online: response.players.online,
89
+ max: response.players.max,
90
+ },
91
+ motd: parseChat(response.description),
92
+ latency,
93
+ target: {
94
+ host: resolved.host,
95
+ port: resolved.port,
96
+ ip: resolved.ip,
97
+ },
98
+ raw: response,
99
+ };
100
+ } catch (e) {
101
+ javaError = e;
102
+ if (type === "java") throw e;
103
+ }
104
+ }
105
+
106
+ try {
107
+ const bedrockResolved =
108
+ type === "bedrock" ? resolved : await resolveTarget(target, "bedrock");
109
+ const response = await pingBedrock(
110
+ bedrockResolved.host,
111
+ bedrockResolved.port,
112
+ timeout,
113
+ );
114
+ return {
115
+ type: "bedrock",
116
+ version: {
117
+ name: response.versionName,
118
+ protocol: response.protocolVersion,
119
+ },
120
+ players: {
121
+ online: response.playerCount,
122
+ max: response.maxPlayerCount,
123
+ },
124
+ motd: response.motdLine2
125
+ ? `${response.motdLine1}\n${response.motdLine2}`
126
+ : response.motdLine1,
127
+ latency: 0,
128
+ target: {
129
+ host: bedrockResolved.host,
130
+ port: bedrockResolved.port,
131
+ ip: bedrockResolved.ip,
132
+ },
133
+ raw: response,
134
+ };
135
+ } catch (bedrockErr: any) {
136
+ if (type === null && javaError) {
137
+ throw new Error(
138
+ `Both Java and Bedrock pings failed. Java: ${javaError.message}. Bedrock: ${bedrockErr.message}`,
139
+ );
140
+ }
141
+ throw bedrockErr;
142
+ }
143
+ }
@@ -0,0 +1,114 @@
1
+ import * as net from "node:net";
2
+ import {
3
+ encodeVarInt,
4
+ encodeString,
5
+ decodeVarInt,
6
+ decodeString,
7
+ } from "./varint.js";
8
+
9
+ export interface JavaRawResponse {
10
+ version: { name: string; protocol: number };
11
+ players: {
12
+ max: number;
13
+ online: number;
14
+ sample?: { name: string; id: string }[];
15
+ };
16
+ description: any;
17
+ favicon?: string;
18
+ enforcesSecureChat?: boolean;
19
+ previewsChat?: boolean;
20
+ }
21
+
22
+ export async function pingJava(
23
+ host: string,
24
+ port: number,
25
+ timeout: number,
26
+ ): Promise<{ response: JavaRawResponse; latency: number }> {
27
+ return new Promise((resolve, reject) => {
28
+ const client = new net.Socket();
29
+ let startTime: number;
30
+ let response: JavaRawResponse;
31
+
32
+ const timeoutHandler = setTimeout(() => {
33
+ client.destroy();
34
+ reject(new Error(`Connection timed out after ${timeout}ms`));
35
+ }, timeout);
36
+
37
+ client.connect(port, host, () => {
38
+ // 1. Handshake
39
+ const protocolVersion = encodeVarInt(763); // 1.20.1
40
+ const serverAddress = encodeString(host);
41
+ const serverPort = Buffer.alloc(2);
42
+ serverPort.writeUInt16BE(port);
43
+ const nextState = encodeVarInt(1);
44
+
45
+ const handshakePayload = Buffer.concat([
46
+ protocolVersion,
47
+ serverAddress,
48
+ serverPort,
49
+ nextState,
50
+ ]);
51
+ const packetIdBuffer = encodeVarInt(0);
52
+ const handshake = Buffer.concat([
53
+ encodeVarInt(handshakePayload.length + packetIdBuffer.length),
54
+ packetIdBuffer,
55
+ handshakePayload,
56
+ ]);
57
+
58
+ client.write(handshake);
59
+
60
+ // 2. Status Request
61
+ const statusRequest = Buffer.concat([encodeVarInt(1), encodeVarInt(0)]);
62
+ client.write(statusRequest);
63
+ startTime = Date.now();
64
+ });
65
+
66
+ let data = Buffer.alloc(0);
67
+ client.on("data", (chunk: Buffer) => {
68
+ data = Buffer.concat([data, chunk]);
69
+
70
+ while (data.length > 0) {
71
+ try {
72
+ // Read packet length
73
+ const { value: packetLen, length: varIntLen } = decodeVarInt(data, 0);
74
+ if (data.length < packetLen + varIntLen) return; // Wait for more data
75
+
76
+ const packet = data.subarray(varIntLen, varIntLen + packetLen);
77
+ data = data.subarray(varIntLen + packetLen); // Consume packet
78
+
79
+ // Read packet ID
80
+ const { value: packetId, length: idLen } = decodeVarInt(packet, 0);
81
+
82
+ if (packetId === 0) {
83
+ const { value: jsonStr } = decodeString(packet, idLen);
84
+ response = JSON.parse(jsonStr);
85
+
86
+ // 3. Ping
87
+ const pingPayload = Buffer.alloc(8);
88
+ pingPayload.writeBigInt64BE(BigInt(startTime));
89
+ const pingPacket = Buffer.concat([
90
+ encodeVarInt(9),
91
+ encodeVarInt(1),
92
+ pingPayload,
93
+ ]);
94
+ client.write(pingPacket);
95
+ } else if (packetId === 1) {
96
+ const latency = Date.now() - startTime;
97
+ clearTimeout(timeoutHandler);
98
+ client.destroy();
99
+ resolve({ response, latency });
100
+ return;
101
+ }
102
+ } catch (e) {
103
+ // Return if we can't decode yet
104
+ return;
105
+ }
106
+ }
107
+ });
108
+
109
+ client.on("error", (err) => {
110
+ clearTimeout(timeoutHandler);
111
+ reject(err);
112
+ });
113
+ });
114
+ }
package/src/target.ts ADDED
@@ -0,0 +1,65 @@
1
+ import * as dns from "node:dns/promises";
2
+
3
+ export interface Target {
4
+ host: string;
5
+ port: number;
6
+ protocol: "java" | "bedrock";
7
+ ip: string;
8
+ }
9
+
10
+ /**
11
+ * Resolves host and port, handling SRV records and lookups.
12
+ */
13
+ export async function resolveTarget(
14
+ targetStr: string,
15
+ type: "java" | "bedrock" | null,
16
+ ): Promise<Target> {
17
+ let host = targetStr;
18
+ let port = type === "bedrock" ? 19132 : 25565;
19
+ const parts = targetStr.split(":");
20
+
21
+ if (parts.length === 2 && !isNaN(parseInt(parts[1]))) {
22
+ host = parts[0];
23
+ port = parseInt(parts[1]);
24
+ const ip = await resolveIp(host);
25
+ return { host, port, protocol: type === null ? "java" : type, ip };
26
+ }
27
+
28
+ // Attempt SRV resolution
29
+ if (type !== "bedrock") {
30
+ try {
31
+ const srv = await dns.resolveSrv(`_minecraft._tcp.${host}`);
32
+ if (srv && srv.length > 0) {
33
+ const srvHost = srv[0].name;
34
+ const srvPort = srv[0].port;
35
+ const ip = await resolveIp(srvHost);
36
+ return { host: srvHost, port: srvPort, protocol: "java", ip };
37
+ }
38
+ } catch (e) {}
39
+ }
40
+
41
+ if (type === "bedrock" && parts.length === 1) {
42
+ try {
43
+ const srv = await dns.resolveSrv(`_minecraft._udp.${host}`);
44
+ if (srv && srv.length > 0) {
45
+ const srvHost = srv[0].name;
46
+ const srvPort = srv[0].port;
47
+ const ip = await resolveIp(srvHost);
48
+ return { host: srvHost, port: srvPort, protocol: "bedrock", ip };
49
+ }
50
+ } catch (e) {}
51
+ }
52
+
53
+ const ip = await resolveIp(host);
54
+ return { host, port, protocol: type || "java", ip };
55
+ }
56
+
57
+ async function resolveIp(host: string): Promise<string> {
58
+ try {
59
+ const addresses = await dns.lookup(host);
60
+ return addresses.address;
61
+ } catch (e) {
62
+ // If lookup fails, return the host itself as a fallback
63
+ return host;
64
+ }
65
+ }
package/src/varint.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * VarInt encoding and decoding for Minecraft protocol.
3
+ * Based on the specification at https://wiki.vg/Protocol#VarInt_and_VarLong
4
+ */
5
+
6
+ export function encodeVarInt(value: number): Buffer {
7
+ const bytes: number[] = [];
8
+ let temp = value >>> 0;
9
+
10
+ while (temp >= 0x80) {
11
+ bytes.push((temp & 0x7f) | 0x80);
12
+ temp >>>= 7;
13
+ }
14
+ bytes.push(temp);
15
+ return Buffer.from(bytes);
16
+ }
17
+
18
+ export function decodeVarInt(
19
+ buffer: Buffer,
20
+ offset = 0,
21
+ ): { value: number; length: number } {
22
+ let value = 0;
23
+ let length = 0;
24
+ let currentByte: number;
25
+
26
+ while (true) {
27
+ currentByte = buffer.readUInt8(offset + length);
28
+ value |= (currentByte & 0x7f) << (length * 7);
29
+ length++;
30
+ if (length > 5) throw new Error("VarInt is too big");
31
+ if ((currentByte & 0x80) !== 0x80) break;
32
+ }
33
+
34
+ return { value: value | 0, length };
35
+ }
36
+
37
+ export function encodeString(str: string): Buffer {
38
+ const content = Buffer.from(str, "utf8");
39
+ return Buffer.concat([encodeVarInt(content.length), content]);
40
+ }
41
+
42
+ export function decodeString(
43
+ buffer: Buffer,
44
+ offset = 0,
45
+ ): { value: string; length: number } {
46
+ const { value: strLen, length: varIntLen } = decodeVarInt(buffer, offset);
47
+ const value = buffer.toString(
48
+ "utf8",
49
+ offset + varIntLen,
50
+ offset + varIntLen + strLen,
51
+ );
52
+ return { value, length: varIntLen + strLen };
53
+ }