├── LICENSE ├── README.md ├── index.js ├── lib ├── ping.js ├── protocol.js └── reader.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nick Krecklow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mcping-js 2 | A modern JavaScript library for querying [Minecraft Java Edition](https://minecraft.net) servers using the vanilla [Server List Ping](https://wiki.vg/Server_List_Ping) protocol. 3 | 4 | ## Usage 5 | ```javascript 6 | const mcping = require('mcping-js') 7 | 8 | // 25565 is the default Minecraft Java Edition multiplayer server port 9 | // The port may be omitted and will default to 25565 10 | const server = new mcping.MinecraftServer('mc.hypixel.net', 25565) 11 | 12 | server.ping(timeout, protocolVersion, (err, res) => { 13 | // ... 14 | }) 15 | ``` 16 | 17 | `protocolVersion` is ever changing as Minecraft updates. See [protocol version numbers](https://wiki.vg/Protocol_version_numbers) for a complete and updated listing. 18 | 19 | If successful, `res` will be a parsed copy of the [Response](https://wiki.vg/Server_List_Ping#Response) packet. 20 | 21 | ## Compatibility 22 | 1. This does not support Minecraft's [legacy ping protocol](https://wiki.vg/Server_List_Ping#1.6) for pre-Minecraft version 1.6 servers. 23 | 2. This does not support the ```Ping``` or ```Pong``` behavior of the [Server List Ping](https://wiki.vg/Server_List_Ping) protocol. If you wish to determine the latency of the connection do you should do so manually. 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const MinecraftServer = require('./lib/ping') 2 | 3 | module.exports = { 4 | MinecraftServer 5 | } -------------------------------------------------------------------------------- /lib/ping.js: -------------------------------------------------------------------------------- 1 | const net = require('net') 2 | 3 | const MinecraftProtocol = require('./protocol') 4 | const MinecraftBufferReader = require('./reader') 5 | 6 | class MinecraftServer { 7 | constructor (host, port) { 8 | this.host = host 9 | this.port = port || 25565 10 | } 11 | 12 | ping (timeout, protocolVersion, callback) { 13 | const socket = net.createConnection({ 14 | host: this.host, 15 | port: this.port 16 | }) 17 | 18 | // Set a manual timeout interval 19 | // This ensures the connection will NEVER hang regardless of internal state 20 | const timeoutTask = setTimeout(() => { 21 | socket.emit('error', new Error('Socket timeout')) 22 | }, timeout) 23 | 24 | const closeSocket = () => { 25 | socket.destroy() 26 | 27 | // Prevent the timeout task from running 28 | clearTimeout(timeoutTask) 29 | } 30 | 31 | // Generic error handler 32 | // This protects multiple error callbacks given the complex socket state 33 | // This is mostly dangerous since it can swallow errors 34 | let didFireError = false 35 | 36 | const handleErr = (err) => { 37 | // Always attempt to destroy the socket 38 | closeSocket() 39 | 40 | if (!didFireError) { 41 | didFireError = true 42 | 43 | // Push the err into the callback 44 | callback(err) 45 | } 46 | } 47 | 48 | // #setNoDelay instantly flushes data during read/writes 49 | // This prevents the runtime from delaying the write at all 50 | socket.setNoDelay(true) 51 | 52 | socket.on('connect', () => { 53 | const handshake = MinecraftProtocol.concat([ 54 | MinecraftProtocol.writeVarInt(0), 55 | MinecraftProtocol.writeVarInt(protocolVersion), 56 | MinecraftProtocol.writeVarInt(this.host.length), 57 | MinecraftProtocol.writeString(this.host), 58 | MinecraftProtocol.writeUShort(this.port), 59 | MinecraftProtocol.writeVarInt(1) 60 | ]) 61 | 62 | socket.write(handshake) 63 | 64 | const request = MinecraftProtocol.concat([ 65 | MinecraftProtocol.writeVarInt(0) 66 | ]) 67 | 68 | socket.write(request) 69 | }) 70 | 71 | let incomingBuffer = Buffer.alloc(0) 72 | 73 | socket.on('data', data => { 74 | incomingBuffer = Buffer.concat([incomingBuffer, data]) 75 | 76 | // Wait until incomingBuffer is at least 5 bytes long to ensure it has captured the first VarInt value 77 | // This value is used to determine the full read length of the response 78 | // "VarInts are never longer than 5 bytes" 79 | // https://wiki.vg/Data_types#VarInt_and_VarLong 80 | if (incomingBuffer.length < 5) { 81 | return 82 | } 83 | 84 | // Always allocate a new MinecraftBufferReader, even if the operation fails 85 | // It tracks the read offset so a new allocation ensures it is reset 86 | const bufferReader = new MinecraftBufferReader(incomingBuffer) 87 | 88 | const length = bufferReader.readVarInt() 89 | 90 | // Ensure incomingBuffer contains the full response 91 | // Offset incomingBuffer.length by bufferReader#offset since length does not include itself 92 | if (incomingBuffer.length - bufferReader.offset() < length) { 93 | return 94 | } 95 | 96 | // Validate the incoming packet ID is a response 97 | const id = bufferReader.readVarInt() 98 | 99 | if (id === 0) { 100 | const reply = bufferReader.readString() 101 | 102 | try { 103 | const message = JSON.parse(reply) 104 | 105 | callback(null, message) 106 | 107 | // Close the socket and clear the timeout task 108 | // This is a general cleanup for success conditions 109 | closeSocket() 110 | } catch (err) { 111 | // Safely propagate JSON parse errors to the callback 112 | handleErr(err) 113 | } 114 | } else { 115 | handleErr(new Error('Received unexpected packet')) 116 | } 117 | }) 118 | 119 | socket.on('error', handleErr) 120 | } 121 | } 122 | 123 | module.exports = MinecraftServer -------------------------------------------------------------------------------- /lib/protocol.js: -------------------------------------------------------------------------------- 1 | class MinecraftProtocol { 2 | static writeVarInt (val) { 3 | // "VarInts are never longer than 5 bytes" 4 | // https://wiki.vg/Data_types#VarInt_and_VarLong 5 | const buf = Buffer.alloc(5) 6 | let written = 0 7 | 8 | while (true) { 9 | if ((val & 0xFFFFFF80) === 0) { 10 | buf.writeUInt8(val, written++) 11 | break 12 | } else { 13 | buf.writeUInt8(val & 0x7F | 0x80, written++) 14 | val >>>= 7 15 | } 16 | } 17 | 18 | return buf.slice(0, written) 19 | } 20 | 21 | static writeString (val) { 22 | return Buffer.from(val, 'UTF-8') 23 | } 24 | 25 | static writeUShort (val) { 26 | return Buffer.from([val >> 8, val & 0xFF]) 27 | } 28 | 29 | static concat (chunks) { 30 | let length = 0 31 | 32 | for (const chunk of chunks) { 33 | length += chunk.length 34 | } 35 | 36 | const buf = [ 37 | MinecraftProtocol.writeVarInt(length), 38 | ...chunks 39 | ] 40 | 41 | return Buffer.concat(buf) 42 | } 43 | } 44 | 45 | module.exports = MinecraftProtocol -------------------------------------------------------------------------------- /lib/reader.js: -------------------------------------------------------------------------------- 1 | class MinecraftBufferReader { 2 | constructor (buffer) { 3 | this._buffer = buffer 4 | this._offset = 0 5 | } 6 | 7 | readVarInt () { 8 | let val = 0 9 | let count = 0 10 | 11 | while (true) { 12 | const b = this._buffer.readUInt8(this._offset++) 13 | 14 | val |= (b & 0x7F) << count++ * 7; 15 | 16 | if ((b & 0x80) != 128) { 17 | break 18 | } 19 | } 20 | 21 | return val 22 | } 23 | 24 | readString () { 25 | const length = this.readVarInt() 26 | const val = this._buffer.toString('UTF-8', this._offset, this._offset + length) 27 | 28 | // Advance the reader index forward by the string length 29 | this._offset += length 30 | 31 | return val 32 | } 33 | 34 | offset () { 35 | return this._offset 36 | } 37 | } 38 | 39 | module.exports = MinecraftBufferReader -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcping-js", 3 | "version": "1.5.0", 4 | "description": "Library for pinging Minecraft Java Edition multiplayer servers", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Cryptkeeper/mcping-js.git" 12 | }, 13 | "keywords": [ 14 | "minecraft", 15 | "ping" 16 | ], 17 | "author": "Nick Krecklow ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/Cryptkeeper/mcping-js/issues" 21 | }, 22 | "homepage": "https://github.com/Cryptkeeper/mcping-js#readme" 23 | } 24 | --------------------------------------------------------------------------------