commit b3f0d8aa182079fac5e097bbaa86bf3c12880218 Author: Cryptkeeper Date: Sun Nov 1 22:56:08 2015 -0600 First commit, most of the backend system! :) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e7aef32 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Cryptkeeper + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac2a497 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +### Minetrack +Minetrack is a Minecraft PC/PE tracker that lets you focus on what's happening *now*. Built to be lightweight and durable, you can easily adapt it to monitor BungeeCord or server instances. + +Try it out: [http://minetrack.me](http://minetrack.me) + +#### Usage +Customize the listing by editing the ```config.json``` file. Then simply use ```node app.js``` to boot the system. \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..e1cb9da --- /dev/null +++ b/app.js @@ -0,0 +1,85 @@ +var server = require('./lib/server'); +var ping = require('./lib/ping'); +var config = require('./config.json'); + +var networkHistory = []; +var connectedClients = 0; + +// Start our main loop that fires off pings. +setInterval(function() { + var networks = config.networks; + + for (var i = 0; i < networks.length; i++) { + // Make sure we lock our scope. + (function(network) { + ping.ping(network.ip, network.port || 25565, network.type, 2500, function(err, result) { + // Handle our ping results, if it succeeded. + if (err) { + console.log('Failed to ping ' + network.ip + ': ' + err); + } else { + console.log(network.ip + ' reply: ' + result.players.online + '/' + result.players.max); + + server.io.sockets.emit('update', result); + } + + // Log our response. + if (!networkHistory[network.ip]) { + networkHistory[network.ip] = []; + } + + var _networkHistory = networkHistory[network.ip]; + + // Remove our previous entrie's favicons, we don't need them, just the latest one. + for (var i = 0; i < _networkHistory.length; i++) { + delete _networkHistory[i].favicon; + } + + _networkHistory.push({ + err: err, + result: result + }); + + // Make sure we never log too much. + if (_networkHistory.length > 300) { + _networkHistory.shift(); + } + }); + })(networks[i]); + } +}, 2500); + +setInterval(function() { + console.log('Connected clients: %d', connectedClients); +}, 1000); + +// Manually construct our paths. +server.urlMapping['/'] = 'assets/html/index.html'; +server.urlMapping['/compass-icon'] = 'assets/images/compass.png'; + +server.start(config.site.ip, config.site.port, function() { + // Track how many people are currently connected. + server.io.on('connect', function(client) { + console.log('Incoming connection: %s', client.request.connection.remoteAddress); + + // We're good to connect them! + connectedClients += 1; + + // Remap our associative array into just an array. + var networkHistoryList = []; + var networkHistoryKeys = Object.keys(networkHistory); + + for (var i = 0; i < networkHistoryKeys.length; i++) { + networkHistoryList.push(networkHistory[networkHistoryKeys[i]]); + } + + // Send them our previous data, so they have somewhere to start. + client.emit('add', networkHistoryList); + + // Attach our listeners. + client.on('disconnect', function(client) { + console.log('Dropped connection: %s', client.request.connection.remoteAddress); + + connectedClients -= 1; + }); + }); +}); \ No newline at end of file diff --git a/assets/html/index.html b/assets/html/index.html new file mode 100644 index 0000000..c14e783 --- /dev/null +++ b/assets/html/index.html @@ -0,0 +1,36 @@ + + + + + + + Minetrack + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/compass.png b/assets/images/compass.png new file mode 100644 index 0000000..5ff4e53 Binary files /dev/null and b/assets/images/compass.png differ diff --git a/config.json b/config.json new file mode 100644 index 0000000..e1b53d7 --- /dev/null +++ b/config.json @@ -0,0 +1,33 @@ +{ + "networks": [ + { + "name": "Hypixel", + "ip": "mc.hypixel.net", + "type": "PC" + }, + { + "name": "Mineplex: US", + "ip": "us.mineplex.com", + "type": "PC" + }, + { + "name": "Mineplex: EU", + "ip": "eu.mineplex.com", + "type": "PC" + }, + { + "name": "Overcast: US", + "ip": "us.oc.tc", + "type": "PC" + }, + { + "name": "Overcast: EU", + "ip": "eu.oc.tc", + "type": "PC" + } + ], + "site": { + "port": 80, + "ip": "0.0.0.0" + } +} \ No newline at end of file diff --git a/lib/mcpc_buffer.js b/lib/mcpc_buffer.js new file mode 100644 index 0000000..46769ea --- /dev/null +++ b/lib/mcpc_buffer.js @@ -0,0 +1,81 @@ +function CustomBuffer(existingBuffer) { + var buffer = existingBuffer || new Buffer(48); + var offset = 0; + + this.writeVarInt = function(val) { + while (true) { + if ((val & 0xFFFFFF80) == 0) { + this.writeUByte(val); + + return; + } + + this.writeUByte(val & 0x7F | 0x80); + + val = val >>> 7; + } + }; + + this.writeString = function(string) { + this.writeVarInt(string.length); + + if (offset + string.length >= buffer.length) { + Buffer.concat([buffer, new Buffer(string.length)]); + } + + buffer.write(string, offset, string.length, "UTF-8"); + + offset += string.length; + }; + + this.writeUShort = function(val) { + this.writeUByte(val >> 8); + this.writeUByte(val & 0xFF); + }; + + this.writeUByte = function(val) { + if (offset + 1 >= buffer.length) { + Buffer.concat([buffer, new Buffer(50)]); + } + + buffer.writeUInt8(val, offset++); + }; + + this.readVarInt = function() { + var val = 0; + var count = 0; + + while (true) { + var i = buffer.readUInt8(offset++); + + val |= (i & 0x7F) << count++ * 7; + + if ((i & 0x80) != 128) { + break + } + } + + return val; + }; + + this.readString = function() { + var length = this.readVarInt(); + var str = buffer.toString("UTF-8", offset, offset + length); + + offset += length; + + return str; + }; + + this.buffer = function() { + return buffer.slice(0, offset); + }; + + this.offset = function() { + return offset; + }; +} + +exports.createBuffer = function(buffer) { + return new CustomBuffer(buffer); +}; \ No newline at end of file diff --git a/lib/ping.js b/lib/ping.js new file mode 100644 index 0000000..91494ea --- /dev/null +++ b/lib/ping.js @@ -0,0 +1,96 @@ +var mcpc = require('./mcpc_buffer'); +var net = require('net'); + +function pingMinecraftPC(host, port, timeout, callback) { + var client = new net.Socket(); + var milliseconds = (new Date).getTime(); + + client.connect(port, host, function() { + // Write out handshake packet. + var handshakeBuffer = mcpc.createBuffer(); + + handshakeBuffer.writeVarInt(0); + handshakeBuffer.writeVarInt(47); + handshakeBuffer.writeString(host); + handshakeBuffer.writeUShort(port); + handshakeBuffer.writeVarInt(1); + + writePCBuffer(client, handshakeBuffer); + + // Write the set connection state packet, we should get the MOTD after this. + var setModeBuffer = mcpc.createBuffer(); + + setModeBuffer.writeVarInt(0); + + writePCBuffer(client, setModeBuffer); + }); + + var readingBuffer = new Buffer(0); + + client.on('data', function(data) { + readingBuffer = Buffer.concat([readingBuffer, data]); + + var buffer = mcpc.createBuffer(readingBuffer); + var length; + + try { + length = buffer.readVarInt(); + } catch(err) { + // The buffer isn't long enough yet, wait for more data! + return; + } + + // Make sure we have the data we need! + if (readingBuffer.length < length - buffer.offset() ) { + return; + } + + // Read the packet ID, throw it away. + buffer.readVarInt(); + + try { + var json = JSON.parse(buffer.readString()); + + json.latency = (new Date).getTime() - milliseconds; + + // We parsed it, send it along! + callback(null, json); + } catch (err) { + // Our data is corrupt? Fail hard. + callback(err, null); + + return; + } + + // We're done here. + client.destroy(); + }); + + client.on('error', function(err) { + callback(err, null); + }); + + // Make sure we don't go overtime. + setTimeout(function() { + client.end(); + }, timeout); +} + +// Wraps our Buffer into another to fit the Minecraft protocol. +function writePCBuffer(client, buffer) { + var length = mcpc.createBuffer(); + + length.writeVarInt(buffer.buffer().length); + + client.write(Buffer.concat([length.buffer(), buffer.buffer()])); +} + +exports.ping = function(host, port, type, timeout, callback) { + if (type === 'PC') { + pingMinecraftPC(host, port, timeout, callback); + } else if (type === 'PE') { + + } else { + throw new Error('Unsupported type: ' + type); + } +}; \ No newline at end of file diff --git a/lib/server.js b/lib/server.js new file mode 100644 index 0000000..e6ceb2b --- /dev/null +++ b/lib/server.js @@ -0,0 +1,36 @@ +var http = require('http'); +var fs = require('fs'); +var url = require('url'); +var mime = require('mime'); +var io = require('socket.io'); + +var urlMapping = []; + +exports.start = function(ip, port, callback) { + var server = http.createServer(function(req, res) { + var requestUrl = url.parse(req.url).pathname; + + if (requestUrl in urlMapping) { + var file = urlMapping[requestUrl]; + + res.setHeader('Content-Type', mime.lookup(file)); + + fs.createReadStream(file).pipe(res); + } else { + res.statusCode = 404; + res.write('404'); + + res.end(); + } + }); + + server.listen(port, ip); + + // I don't like this. But it works, I think. + exports.io = (io = io.listen(server)); + + // Since everything is loaded, do some final prep work. + callback(); +}; + +exports.urlMapping = urlMapping; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..47b14ab --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "minetrack", + "version": "1.0.0", + "description": "A Minecraft network tracker that lets you focus on the basics.", + "main": "app.js", + "dependencies": { + "socket.io": "^1.3.7", + "mime": "^1.3.4" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Cryptkeeper/Minetrack.git" + }, + "keywords": [ + "minetrack" + ], + "author": "Cryptkeeper ", + "license": "MIT", + "bugs": { + "url": "https://github.com/Cryptkeeper/Minetrack/issues" + }, + "homepage": "https://github.com/Cryptkeeper/Minetrack#README" +}