First commit, most of the backend system! :)

This commit is contained in:
Cryptkeeper 2015-11-01 22:56:08 -06:00
commit b3f0d8aa18
11 changed files with 419 additions and 0 deletions

1
.gitignore vendored Normal file

@ -0,0 +1 @@
node_modules/

21
LICENSE Normal file

@ -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.

7
README.md Normal file

@ -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.

85
app.js Normal file

@ -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;
});
});
});

36
assets/html/index.html Normal file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<title>Minetrack</title>
</head>
<body>
<img src="/compass-icon">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.7/socket.io.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
var socket = io.connect();
socket.on('update', function(network) {
console.log(network);
});
socket.on('add', function(networks) {
console.log(networks);
});
});
</script>
</body>
</html>

BIN
assets/images/compass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

33
config.json Normal file

@ -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"
}
}

81
lib/mcpc_buffer.js Normal file

@ -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);
};

96
lib/ping.js Normal file

@ -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);
}
};

36
lib/server.js Normal file

@ -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;

23
package.json Normal file

@ -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 <cryptkeeper@hypixel.net>",
"license": "MIT",
"bugs": {
"url": "https://github.com/Cryptkeeper/Minetrack/issues"
},
"homepage": "https://github.com/Cryptkeeper/Minetrack#README"
}