├── 404.html
├── README.md
├── app
├── app.py
├── config.example.yml
├── mcstatus.py
└── requirements.txt
├── index.html
├── override.css
└── populate.js
/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Status - 404
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Error: 404 Not Found
15 |
The requested file does not exist.
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Official Thread:** [http://www.spigotmc.org/resources/mc-status-viewer.518/](http://www.spigotmc.org/resources/mc-status-viewer.518/)
2 |
3 | **Installation:**
4 |
5 | * `git clone [https://github.com/vemacs/mc-status-viewer.git](https://github.com/vemacs/mc-status-viewer.git)` (I prefer in `/srv`)
6 | * Edit `index.html` to have the title you want
7 | * Edit `app/config.yml` to set up your categories and servers, see `[config.example.yml](https://github.com/vemacs/mc-status-viewer/blob/master/app/config.example.yml)`
8 | * `pip install bottle pyyaml`, depending on your distro, you may need to install `python-pip` and `python-dev` first
9 | * `cd /app; python app.py`
10 | * You can test it by changing `app/app.py `to bind to `0.0.0.0 `and then connecting to `:8080`, `config.yml` is not accessible to the public, so your backends will stay hidden
11 | * Run it in a `tmux `or `screen` session
12 | * Set up a reverse proxy, here's a [sample nginx config](http://paste.ubuntu.com/7301975/), [Apache instructions](http://paste.ubuntu.com/7401472/)
13 | * `git pull` to update if there are any updates
14 | * You can **change the width** by editing `override.css` in the` .btn` class if you have longer server names
15 |
16 | Post feedback or suggestions here! Keep in mind that I'm a noob at Python and Javascript (I literally learned JS today), so any code quality feedback would be awesome.
17 |
18 | Here's the CPU/mem usage you'll be looking at (pinging way too many servers, running for an hour):
19 |
20 | 
21 |
22 | It's not 100% accurate due to the nature of server list ping, but it works well enough (99.99999% accurate). If you're having issues, ask your host if this is triggering their anti-DDoS mechanism. Delaying the pings does not seem to help in this situation, so your best bet may be to ask your host, or host it on a box that can ping. Updates are every 5 seconds on the server, and then 3 seconds to pull on the client. Obviously, you should allow the box you're putting this on through your backend firewall.
23 |
24 | Technically, you can host the frontend anywhere, just that origin policy complicates things.
25 |
26 | If Ctrl+C isn't stopping it, try Ctrl+Z. A restart needs to be issued for config changes to apply.
27 |
28 | **IF YOU ARE USING WINDOWS TO RUN A MINECRAFT SERVER, YOU ARE DOING IT WRONG. QUIT ASKING HOW TO RUN THIS ON WINDOWS. INSTRUCTIONS SHOULD BE VERY SIMILAR, BUT NO GUARANTEES THAT THIS WILL WORK.**
29 |
--------------------------------------------------------------------------------
/app/app.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 | import mcstatus, yaml, time, threading
3 | from bottle import route, run, template, static_file, error
4 |
5 | data = {}
6 | json_response = None
7 |
8 | with open('config.yml', 'r') as cfg_file:
9 | servers_config = yaml.load(cfg_file)
10 |
11 | # c = 0.0
12 |
13 | for category in servers_config:
14 | print category
15 | data[category] = {}
16 | for server in servers_config[category]:
17 | print "- " + server + ": " + servers_config[category][server]
18 | ip = servers_config[category][server]
19 | if "/" not in ip:
20 | ip += "/25565"
21 | status = mcstatus.McServer(ip.split("/")[0], ip.split("/")[1])
22 | # c += 1
23 | data[category][server] = status
24 |
25 | def update_all():
26 | # i = 0.0
27 | for category in data:
28 | # d = 5.0 / c
29 | for server in data[category]:
30 | # i += 1.0
31 | status = data[category][server]
32 | threading.Thread(target=lambda: status.Update()).start()
33 |
34 | def sort_dict_by_key(to_sort):
35 | return OrderedDict(sorted(to_sort.items(), key=lambda t: t[0]))
36 |
37 | def generate_json():
38 | alive = "alive"
39 | dead = "dead"
40 | response = {}
41 | response[alive] = {}
42 | response[dead] = {}
43 | for category in data:
44 | response[alive][category] = {}
45 | response[dead][category] = []
46 | for server in data[category]:
47 | status = data[category][server]
48 | if status.available:
49 | response[alive][category][server] = str(status.num_players_online) + "/" + str(status.max_players_online)
50 | else:
51 | response[dead][category].append(server)
52 | response[alive][category] = sort_dict_by_key(response[alive][category])
53 | response[dead][category].sort()
54 | if len(response[alive][category]) == 0:
55 | del response[alive][category]
56 | if len(response[dead][category]) == 0:
57 | del response[dead][category]
58 | response[alive] = sort_dict_by_key(response[alive])
59 | response[dead] = sort_dict_by_key(response[dead])
60 | return response
61 |
62 | def schedule_update():
63 | threading.Timer(5, schedule_update).start()
64 | update_all()
65 |
66 | def schedule_json():
67 | threading.Timer(1.5, schedule_json).start()
68 | global json_response
69 | json_response = generate_json()
70 |
71 | @route('/status')
72 | def index():
73 | return json_response
74 |
75 | @route('/')
76 | def server_static():
77 | return static_file('index.html', '..')
78 |
79 | @error(404)
80 | def error404(error):
81 | return static_file('404.html', '..')
82 |
83 | @route('/')
84 | def server_static(filename):
85 | return static_file(filename, root = '..')
86 |
87 | schedule_update()
88 | schedule_json()
89 |
90 | try:
91 | run(host='localhost', port=8080)
92 | except KeyboardInterrupt:
93 | sys.exit(0)
94 |
--------------------------------------------------------------------------------
/app/config.example.yml:
--------------------------------------------------------------------------------
1 | Bungee:
2 | bungee1: 192.95.39.40/25565
3 | bungee2: 192.95.38.142/25565
4 |
--------------------------------------------------------------------------------
/app/mcstatus.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Checks the status (availability, logged-in players) on a Minecraft server.
3 |
4 | Example:
5 | $ %(prog)s host [port]
6 | available, 3/5 online: mf, dignity, viking
7 |
8 | or
9 |
10 | >>> McServer('my.mcserver.com').Update().player_names_sample
11 | frozenset(['mf', 'dignity', 'viking'])
12 |
13 | Based on:
14 | https://gist.github.com/barneygale/1209061
15 | Protocol reference:
16 | http://wiki.vg/Server_List_Ping
17 | """
18 |
19 | import argparse
20 | import json
21 | import logging
22 | import socket
23 | import struct
24 |
25 | DEFAULT_PORT = 25565
26 | TIMEOUT_SEC = 5.0
27 |
28 |
29 | class McServer:
30 |
31 | def __init__(self, host, port=DEFAULT_PORT):
32 | self._host = host
33 | self._port = int(port)
34 | self._Reinit()
35 |
36 | def _Reinit(self):
37 | self._available = False
38 | self._num_players_online = 0
39 | self._max_players_online = 0
40 | self._player_names_sample = frozenset()
41 |
42 | def Update(self):
43 | # print "Updating "+ self._host + "/" + str(self._port)
44 | try:
45 | json_dict = GetJson(self._host, port=self._port)
46 | except (socket.error, ValueError) as e:
47 | self._Reinit()
48 | logging.debug(e)
49 | return self
50 | self._num_players_online = json_dict['players']['online']
51 | self._max_players_online = json_dict['players']['max']
52 | self._available = True
53 | return self
54 |
55 | @property
56 | def available(self):
57 | return self._available
58 |
59 | @property
60 | def num_players_online(self):
61 | return self._num_players_online
62 |
63 | @property
64 | def max_players_online(self):
65 | return self._max_players_online
66 |
67 | @property
68 | def player_names_sample(self):
69 | return self._player_names_sample
70 |
71 |
72 | def GetJson(host, port=DEFAULT_PORT):
73 | """
74 | Example response:
75 |
76 | json_dict = {
77 | u'players': {
78 | u'sample': [
79 | {u'id': u'6a0c2570-274f-36b8-97b0-898868ba6827', u'name': u'mf'}
80 | ],
81 | u'max': 20,
82 | u'online': 1,
83 | },
84 | u'version': {u'protocol': 5, u'name': u'1.7.8'},
85 | u'description': u'1.7.8',
86 | u'favicon': u'data:image/png;base64,iVBORw0K...YII=',
87 | }
88 | """
89 | # Open the socket and connect.
90 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
91 | s.settimeout(TIMEOUT_SEC)
92 | s.connect((host, port))
93 |
94 | # Send the handshake + status request.
95 | s.send(_PackData('\x00\x00' + _PackData(host.encode('utf8'))
96 | + _PackPort(port) + '\x01'))
97 | s.send(_PackData('\x00'))
98 |
99 | # Read the response.
100 | unused_packet_len = _UnpackVarint(s)
101 | unused_packet_id = _UnpackVarint(s)
102 | expected_response_len = _UnpackVarint(s)
103 |
104 | data = ''
105 | while len(data) < expected_response_len:
106 | data += s.recv(1024)
107 |
108 | s.close()
109 |
110 | return json.loads(data.decode('utf8'))
111 |
112 |
113 | def _UnpackVarint(s):
114 | num = 0
115 | for i in range(5):
116 | next_byte = ord(s.recv(1))
117 | num |= (next_byte & 0x7F) << 7*i
118 | if not next_byte & 0x80:
119 | break
120 | return num
121 |
122 |
123 | def _PackVarint(num):
124 | remainder = num
125 | packed = ''
126 | while True:
127 | next_byte = remainder & 0x7F
128 | remainder >>= 7
129 | packed += struct.pack('B', next_byte | (0x80 if remainder > 0 else 0))
130 | if remainder == 0:
131 | break
132 | return packed
133 |
134 |
135 | def _PackData(data_str):
136 | return _PackVarint(len(data_str)) + data_str
137 |
138 |
139 | def _PackPort(port_num):
140 | return struct.pack('>H', port_num)
141 |
142 |
143 | if __name__ == '__main__':
144 | logging.basicConfig(
145 | format='%(levelname)s %(asctime)s %(filename)s:%(lineno)s: %(message)s',
146 | level=logging.DEBUG)
147 |
148 | summary_line, _, main_doc = __doc__.partition('\n\n')
149 | parser = argparse.ArgumentParser(
150 | description=summary_line,
151 | epilog=main_doc,
152 | formatter_class=argparse.RawDescriptionHelpFormatter)
153 | parser.add_argument(
154 | '--port', type=int, default=DEFAULT_PORT,
155 | help='defaults to %d' % DEFAULT_PORT)
156 | parser.add_argument('host')
157 | args = parser.parse_args()
158 |
159 | logging.info('querying %s:%d', args.host, args.port)
160 |
161 | server = McServer(args.host, port=args.port)
162 | server.Update()
163 | if server.available:
164 | logging.info(
165 | 'available, %d/%d online: %s',
166 | server.num_players_online, server.max_players_online,
167 | ', '.join(server.player_names_sample))
168 | else:
169 | logging.info('unavailable')
170 |
--------------------------------------------------------------------------------
/app/requirements.txt:
--------------------------------------------------------------------------------
1 | PyYAML==3.11
2 | bottle==0.12.5
3 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Status Test
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
Alive
18 |
19 |
20 |
21 |
22 |
23 |
Dead
Dead instances are instances which refused the
24 | connection or timed out.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/override.css:
--------------------------------------------------------------------------------
1 | .btn {
2 | margin-bottom: 2px;
3 | margin-right: 2px;
4 | width: 100px;
5 | }
6 |
7 | .btn-override:hover, .btn-override:visited, .btn-override:active {
8 | cursor: inherit;
9 | box-shadow: none;
10 | }
11 |
12 | .align-row {
13 | margin-left: 0px;
14 | margin-right: 0px;
15 | }
16 |
17 | .btn-override em {
18 | display: block;
19 | font-size: 12px;
20 | font-style: normal;
21 | color: rgba(255,255,255,0.6);
22 | }
23 |
24 | body {
25 | margin-bottom: 16px;
26 | }
--------------------------------------------------------------------------------
/populate.js:
--------------------------------------------------------------------------------
1 | function populate() {
2 | $.getJSON('status?' + Math.floor(Math.random()*30000), function (data) {
3 | var x = "";
4 |
5 | for (var category in data.alive) {
6 | x += "";
7 |
8 | x += "
" + category + "
";
9 | for (var entry in data.alive[category]) {
10 | x += "" +
11 | entry + "" + data['alive'][category][entry] + "";
12 | }
13 | x += ""
14 | }
15 |
16 | $('#alive-data').html(x);
17 |
18 | var x = "";
19 |
20 | for (var category in data.dead) {
21 | x += "";
22 |
23 | x += "
" + category + "
";
24 | for (var entry in data.dead[category]) {
25 | x += "" +
26 | data.dead[category][entry] + "";
27 | }
28 | x += ""
29 | }
30 |
31 | $('#dead-data').html(x);
32 | });
33 | }
34 |
35 | $(document).ready(function () {
36 | window.setInterval(populate, 3000);
37 | populate();
38 | });
39 |
--------------------------------------------------------------------------------