First commit, most of the backend system! :)
This commit is contained in:
commit
b3f0d8aa18
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
21
LICENSE
Normal file
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
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
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
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
BIN
assets/images/compass.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 585 B |
33
config.json
Normal file
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
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
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
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
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"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user