UNPKG

mcpe-ping

Version:

Pings an MCPE server for simple data.

327 lines (306 loc) 8.74 kB
'use strict'; var dgram = require('dgram'); var ByteBuffer = require('bytebuffer'); var portfinder = require('portfinder'); var dns = require('dns'); var START_TIME = new Date().getTime(); var RAKNET = { STRUCTURE: 5, MAGIC: "00ffff00fefefefefdfdfdfd12345678", SERVER_ID: 925686942, UNCONNECTED_PING: 0x01, UNCONNECTED_PING_OPEN_CONNECTIONS: 0x02, OPEN_CONNECTION_REQUEST_1: 0x05, OPEN_CONNECTION_REPLY_1: 0x06, OPEN_CONNECTION_REQUEST_2: 0x07, OPEN_CONNECTION_REPLY_2: 0x08, INCOMPATIBLE_PROTOCOL_VERSION: 0x1a, //CHECK THIS UNCONNECTED_PONG: 0x1c, ADVERTISE_SYSTEM: 0x1d, DATA_PACKET_0: 0x80, DATA_PACKET_1: 0x81, DATA_PACKET_2: 0x82, DATA_PACKET_3: 0x83, DATA_PACKET_4: 0x84, DATA_PACKET_5: 0x85, DATA_PACKET_6: 0x86, DATA_PACKET_7: 0x87, DATA_PACKET_8: 0x88, DATA_PACKET_9: 0x89, DATA_PACKET_A: 0x8a, DATA_PACKET_B: 0x8b, DATA_PACKET_C: 0x8c, DATA_PACKET_D: 0x8d, DATA_PACKET_E: 0x8e, DATA_PACKET_F: 0x8f, NACK: 0xa0, ACK: 0xc0 }; var UNCONNECTED_PING = function (pingId) { this.bb = new ByteBuffer(); this.bb.buffer[0] = RAKNET.UNCONNECTED_PING; this.bb.offset = 1; this.pingId = pingId; }; UNCONNECTED_PING.prototype.encode = function () { //console.log(this.pingId); this.bb .writeLong(this.pingId) .append(RAKNET.MAGIC, "hex") .writeLong(0) .flip() .compact(); }; var UNCONNECTED_PONG = function (buf) { this.bb = buf; this.bb.offset = 1; }; UNCONNECTED_PONG.prototype.decode = function () { this.pingId = this.bb.readLong(); this.serverId = this.bb.readLong(); this.bb.offset += 16; this.nameLength = this.bb.readShort(); try { this.advertiseString = this.bb.readUTF8String(this.nameLength); } catch(e){ //FIXME this.advertiseString = this.bb.readUTF8String(parseInt(e.message.substr(e.message.indexOf(",")+2, 3))); } var splitString = this.advertiseString.split(/;/g); this.gameId = splitString[0]; this.name = splitString[1]; this.unknownId = splitString[2]; this.gameVersion = splitString[3]; this.currentPlayers = splitString[4]; this.maxPlayers = splitString[5]; }; var QUERY = { STATISTIC: 0x00, HANDSHAKE: 0x09, MAGIC: 'fefd', KEYVAL_START: 128, KEYVAL_END: 1 }; var CHALLENGE = function (buf) { this.bb = new ByteBuffer(); }; CHALLENGE.prototype.encode = function () { //console.log(this.pingId); this.bb .append(QUERY.MAGIC, "hex") .writeByte(QUERY.HANDSHAKE) .writeInt32(1) .flip() .compact(); }; var CHALLENGE_RESPONSE = function (buf) { this.bb = buf; this.bb.offset = 1; }; CHALLENGE_RESPONSE.prototype.decode = function () { this.clientId = this.bb.readInt32(); //console.log(this.pingId); var bb = this.bb.slice(5); this.challengeToken = parseInt(bb.toString('utf8'), 10); }; var STAT_REQUEST = function(challengeToken){ this.bb = new ByteBuffer(); this.challengeToken = challengeToken; }; STAT_REQUEST.prototype.encode = function () { //console.log(this.pingId); this.bb .append(QUERY.MAGIC, "hex") .writeByte(QUERY.STATISTIC) .writeInt32(1) .writeInt32(this.challengeToken) .writeInt32(0) .flip() .compact(); }; var STAT_RESPONSE = function(buf){ this.bb = buf; this.bb.offset = 16; }; STAT_RESPONSE.prototype.decode = function () { this.data = {}; this.players = []; var key; var value; while (this.bb.readUint16(this.bb.offset) !== QUERY.KEYVAL_END) { key = readString(this.bb); value = readString(this.bb); this.data[key] = value; } this.bb.offset += 11; var player = readString(this.bb); while (player.length >= 1) { this.players.push(player); player = readString(this.bb); } }; var ping = function (server, port, callback, timeout, fullQuery) { if(typeof timeout == "boolean" && typeof timeout === "undefined"){ fullQuery = timeout; timeout = undefined; } var MCPE_DEFAULT_PORT = 19132; if (typeof port === "function") { callback = port; port = MCPE_DEFAULT_PORT; } if (typeof port !== "number") { port = MCPE_DEFAULT_PORT; } if (typeof timeout === "undefined") { timeout = 5000; } if(checkIsIPV4(server)){ if(fullQuery){ query(server, port, callback, timeout); } else{ pingIP(server, port, callback, timeout); } } else{ dns.lookup(server, function(err, res){ if(err === null){ if(fullQuery){ query(res, port, callback, timeout); } else{ pingIP(res, port, callback, timeout); } } else{ callback({error: true, description: "DNS lookup failed."}, null); } }); } }; var query = function(server, port, callback, timeout){ var client = dgram.createSocket("udp4"); client.on("message", ((function (msg, rinfo) { var buf = new ByteBuffer().append(msg, "hex").flip(); var id = buf.buffer[0]; switch (id) { case QUERY.HANDSHAKE: var pong = new CHALLENGE_RESPONSE(buf); pong.decode(); var statRequest = new STAT_REQUEST(pong.challengeToken); statRequest.encode(); client.send(statRequest.bb.buffer, 0, statRequest.bb.buffer.length, port, server); break; case QUERY.STATISTIC: var stats = new STAT_RESPONSE(buf); stats.decode(); var clientData = { 'rinfo': rinfo, 'hostname': stats.data.hostname, 'gametype': stats.data.gametype, 'game': stats.data.gameId, 'version': stats.data.version, 'serverEngine': stats.data['server_engine'], 'plugins': stats.data.plugins, 'map': stats.data.map, 'currentPlayers': stats.data.numplayers, 'maxPlayers': stats.data.maxplayers, 'whitelist': (stats.data.whitelist === "on"), 'hostIp': stats.data.hostip, 'hostPort': stats.data.hostport, 'ackId': new Date().getTime() - START_TIME, 'players': stats.players, 'connected': true }; client.close(); callback(null, clientData); break; default: callback({error: true, description: "Bad packet response."}, null); client.close(); break; } }).bind(this))); try { var challenge = new CHALLENGE(); challenge.encode(); client.send(challenge.bb.buffer, 0, challenge.bb.buffer.length, port, server); } catch(e){ client.close(); callback({error: true, description: "Error sending ping."}, null); } }; var pingIP = function (server, port, callback, timeout) { var client = dgram.createSocket("udp4"); var broadcastPing = (function () { try { var ping = new UNCONNECTED_PING(new Date().getTime() - START_TIME); ping.encode(); client.send(ping.bb.buffer, 0, ping.bb.buffer.length, port, server); } catch(e){ clearInterval(broadcastIntervalId); clearTimeout(timeoutId); client.close(); callback({error: true, description: "Error sending ping."}, null); } }); broadcastPing(); var broadcastIntervalId = setInterval(broadcastPing.bind(this), 400); var timeoutId = setTimeout(function(){ clearInterval(broadcastIntervalId); client.close(); callback({error: true, description: "Ping session timed out."}, null); }, timeout); client.on("message", ((function (msg, rinfo) { var buf = new ByteBuffer().append(msg, "hex").flip(); var id = buf.buffer[0]; switch (id) { case RAKNET.UNCONNECTED_PONG: var pong = new UNCONNECTED_PONG(buf); pong.decode(); var clientData = { 'rinfo': rinfo, 'advertise': pong.advertiseString, 'serverId': pong.serverId, 'pingId': pong.pingId, 'game': pong.gameId, 'version': pong.gameVersion, 'name': pong.name, 'cleanName': pong.name.replace(/\xA7[0-9A-FK-OR]/ig, ''), 'currentPlayers': pong.currentPlayers, 'maxPlayers': pong.maxPlayers, 'ackId': new Date().getTime() - START_TIME, 'connected': true }; clearInterval(broadcastIntervalId); clearTimeout(timeoutId); client.close(); callback(null, clientData); break; default: console.log(id); break; } }).bind(this))); }; function checkIsIPV4(entry) { var blocks = entry.split("."); if(blocks.length === 4) { return blocks.every(function(block) { return parseInt(block,10) >=0 && parseInt(block,10) <= 255; }); } return false; } function readString(bb) { var start = bb.offset; var b = bb.readUInt8(); while (b !== 0x0) { b = bb.readUInt8(); } return bb.toString('utf8', start, bb.offset - 1); } module.exports = ping;