├── 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 | ![](http://i.imgur.com/scyRmnM.png) 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 | --------------------------------------------------------------------------------