diff --git a/app/classes/minecraft/bedrock_ping.py b/app/classes/minecraft/bedrock_ping.py new file mode 100644 index 00000000..1d2a8c99 --- /dev/null +++ b/app/classes/minecraft/bedrock_ping.py @@ -0,0 +1,107 @@ +import os +import socket +import time +import psutil + +class BedrockPing: + magic = b'\x00\xff\xff\x00\xfe\xfe\xfe\xfe\xfd\xfd\xfd\xfd\x12\x34\x56\x78' + fields = { # (len, signed) + "byte": (1, False), + "long": (8, True), + "ulong": (8, False), + "magic": (16, False), + "short": (2, True), + "ushort": (2, False), #unsigned short + "string": (2, False), #strlen is ushort + "bool": (1, False), + "address": (7, False), + "uint24le": (3, False) + } + byte_order = 'big' + + def __init__(self, bedrock_addr, bedrock_port, client_guid=0, timeout=5): + self.addr = bedrock_addr + self.port = bedrock_port + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.settimeout(timeout) + self.proc = psutil.Process(os.getpid()) + self.guid = client_guid + self.guid_bytes = self.guid.to_bytes(8, BedrockPing.byte_order) + + @staticmethod + def __byter(in_val, to_type): + f = BedrockPing.fields[to_type] + return in_val.to_bytes(f[0], BedrockPing.byte_order, signed=f[1]) + + @staticmethod + def __slice(in_bytes, pattern): + ret = [] + bi = 0 # bytes index + pi = 0 # pattern index + while bi < len(in_bytes): + try: + f = BedrockPing.fields[pattern[pi]] + except IndexError as index_error: + raise IndexError("Ran out of pattern with additional bytes remaining") from index_error + if pattern[pi] == "string": + shl = f[0] # string header length + sl = int.from_bytes(in_bytes[bi:bi+shl], BedrockPing.byte_order, signed=f[1]) # string length + l = shl+sl + ret.append(in_bytes[bi+shl:bi+shl+sl].decode('ascii')) + elif pattern[pi] == "magic": + l = f[0] # length of field + ret.append(in_bytes[bi:bi+l]) + else: + l = f[0] # length of field + ret.append(int.from_bytes(in_bytes[bi:bi+l], BedrockPing.byte_order, signed=f[1])) + bi+=l + pi+=1 + return ret + + @staticmethod + def __get_time(): + #return time.time_ns() // 1000000 + return time.perf_counter_ns() // 1000000 + + def __sendping(self): + pack_id = BedrockPing.__byter(0x01, 'byte') + now = BedrockPing.__byter(BedrockPing.__get_time(), 'ulong') + guid = self.guid_bytes + d2s = pack_id+now+BedrockPing.magic+guid + #print("S:", d2s) + self.sock.sendto(d2s, (self.addr, self.port)) + + def __recvpong(self): + data = self.sock.recv(4096) + if data[0] == 0x1c: + ret = {} + sliced = BedrockPing.__slice(data,["byte","ulong","ulong","magic","string"]) + if sliced[3] != BedrockPing.magic: + raise ValueError(f"Incorrect magic received ({sliced[3]})") + ret["server_guid"] = sliced[2] + ret["server_string_raw"] = sliced[4] + server_info = sliced[4].split(';') + ret["server_edition"] = server_info[0] + ret["server_motd"] = (server_info[1], server_info[7]) + ret["server_protocol_version"] = server_info[2] + ret["server_version_name"] = server_info[3] + ret["server_player_count"] = server_info[4] + ret["server_player_max"] = server_info[5] + ret["server_uuid"] = server_info[6] + ret["server_game_mode"] = server_info[8] + ret["server_game_mode_num"] = server_info[9] + ret["server_port_ipv4"] = server_info[10] + ret["server_port_ipv6"] = server_info[11] + return ret + else: + raise ValueError(f"Incorrect packet type ({data[0]} detected") + + def ping(self, retries=3): + rtr = retries + while rtr > 0: + try: + self.__sendping() + return self.__recvpong() + except ValueError as e: + print(f"E: {e}, checking next packet. Retries remaining: {rtr}/{retries}") + rtr -= 1 diff --git a/app/classes/minecraft/mc_ping.py b/app/classes/minecraft/mc_ping.py index 532b28bf..5b7dd201 100644 --- a/app/classes/minecraft/mc_ping.py +++ b/app/classes/minecraft/mc_ping.py @@ -3,9 +3,13 @@ import socket import base64 import json import os +import re import logging.config +import uuid +import random from app.classes.shared.console import console +from app.classes.minecraft.bedrock_ping import BedrockPing logger = logging.getLogger(__name__) @@ -168,63 +172,15 @@ def ping(ip, port): # For the rest of requests see wiki.vg/Protocol def ping_bedrock(ip, port): - def read_var_int(): - i = 0 - j = 0 - while True: - try: - k = sock.recvfrom(1024) - except: - return False - if not k: - return 0 - k = k[0] - i |= (k & 0x7f) << (j * 7) - j += 1 - if j > 5: - raise ValueError('var_int too big') - if not k & 0x80: - return i - - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.settimeout(2) + rd = random.Random() try: - sock.connect((ip, port)) - + #pylint: disable=consider-using-f-string + rd.seed(''.join(re.findall('..', '%012x' % uuid.getnode()))) + client_guid = uuid.UUID(int=rd.getrandbits(32)).int except: - print("in first except") - return False - + client_guid = 0 try: - host = ip.encode('utf-8') - data = b'' # wiki.vg/Server_List_Ping - data += b'\x00' # packet ID - data += b'\x04' # protocol variant - data += struct.pack('>b', len(host)) + host - data += struct.pack('>H', port) - data += b'\x01' # next state - data = struct.pack('>b', len(data)) + data - sock.sendall(data + b'\x01\x00') # handshake + status ping - length = read_var_int() # full packet length - if length < 10: - if length < 0: - return False - else: - return False - try: - sock.recvfrom(1024) # packet type, 0 for pings - except: - return False - length = read_var_int() # string length - data = b'' - while len(data) != length: - print("in while") - chunk = sock.recv(length - len(data)) - if not chunk: - return False - - data += chunk - logger.debug(f"Server reports this data on ping: {data}") - return Server(json.loads(data)) - finally: - sock.close() + brp = BedrockPing(ip, port, client_guid) + return brp.ping() + except socket.timeout: + logger.debug("Unable to get RakNet stats") diff --git a/app/classes/minecraft/stats.py b/app/classes/minecraft/stats.py index d117699a..a775c2aa 100644 --- a/app/classes/minecraft/stats.py +++ b/app/classes/minecraft/stats.py @@ -159,6 +159,26 @@ class Stats: 'server_icon': server_icon } + return ping_data + + @staticmethod + def parse_server_RakNet_ping(ping_obj: object): + + try: + server_icon = base64.encodebytes(ping_obj['icon']) + except Exception as e: + server_icon = False + logger.info(f"Unable to read the server icon : {e}") + ping_data = { + 'online': ping_obj['server_player_count'], + 'max': ping_obj['server_player_max'], + 'players': [], + 'server_description': ping_obj['server_edition'], + 'server_version': ping_obj['server_version_name'], + 'server_icon': server_icon + } + + return ping_data def get_server_players(self, server_id): @@ -177,17 +197,16 @@ class Stats: server_port = server['server_port'] logger.debug("Pinging {internal_ip} on port {server_port}") - if servers_helper.get_server_type_by_id(server_id) == 'minecraft-bedrock': - int_mc_ping = ping_bedrock(internal_ip, int(server_port)) - else: + if servers_helper.get_server_type_by_id(server_id) != 'minecraft-bedrock': int_mc_ping = ping(internal_ip, int(server_port)) - ping_data = {} - # if we got a good ping return, let's parse it - if int_mc_ping: - ping_data = self.parse_server_ping(int_mc_ping) - return ping_data['players'] + ping_data = {} + + # if we got a good ping return, let's parse it + if int_mc_ping: + ping_data = self.parse_server_ping(int_mc_ping) + return ping_data['players'] return [] def get_servers_stats(self): @@ -237,25 +256,47 @@ class Stats: # if we got a good ping return, let's parse it if int_mc_ping: int_data = True - ping_data = self.parse_server_ping(int_mc_ping) - - server_stats = { - 'id': server_id, - 'started': server_obj.get_start_time(), - 'running': server_obj.check_running(), - 'cpu': p_stats.get('cpu_usage', 0), - 'mem': p_stats.get('memory_usage', 0), - "mem_percent": p_stats.get('mem_percentage', 0), - 'world_name': world_name, - 'world_size': self.get_world_size(world_path), - 'server_port': server_port, - 'int_ping_results': int_data, - 'online': ping_data.get("online", False), - "max": ping_data.get("max", False), - 'players': ping_data.get("players", False), - 'desc': ping_data.get("server_description", False), - 'version': ping_data.get("server_version", False) - } + if servers_helper.get_server_type_by_id(s['server_id']) == 'minecraft-bedrock': + ping_data = self.parse_server_RakNet_ping(int_mc_ping) + else: + ping_data = self.parse_server_ping(int_mc_ping) + #Makes sure we only show stats when a server is online otherwise people have gotten confused. + if server_obj.check_running(): + server_stats = { + 'id': server_id, + 'started': server_obj.get_start_time(), + 'running': server_obj.check_running(), + 'cpu': p_stats.get('cpu_usage', 0), + 'mem': p_stats.get('memory_usage', 0), + "mem_percent": p_stats.get('mem_percentage', 0), + 'world_name': world_name, + 'world_size': self.get_world_size(world_path), + 'server_port': server_port, + 'int_ping_results': int_data, + 'online': ping_data.get("online", False), + "max": ping_data.get("max", False), + 'players': ping_data.get("players", False), + 'desc': ping_data.get("server_description", False), + 'version': ping_data.get("server_version", False) + } + else: + server_stats = { + 'id': server_id, + 'started': server_obj.get_start_time(), + 'running': server_obj.check_running(), + 'cpu': p_stats.get('cpu_usage', 0), + 'mem': p_stats.get('memory_usage', 0), + "mem_percent": p_stats.get('mem_percentage', 0), + 'world_name': world_name, + 'world_size': self.get_world_size(world_path), + 'server_port': server_port, + 'int_ping_results': int_data, + 'online': False, + "max": False, + 'players': False, + 'desc': False, + 'version': False + } # add this servers data to the stack server_stats_list.append(server_stats) @@ -264,8 +305,30 @@ class Stats: def get_raw_server_stats(self, server_id): + try: + self.controller.get_server_obj(server_id) + except: + return { 'id': server_id, + 'started': False, + 'running': False, + 'cpu': 0, + 'mem': 0, + "mem_percent": 0, + 'world_name': None, + 'world_size': None, + 'server_port': None, + 'int_ping_results': False, + 'online': False, + 'max': False, + 'players': False, + 'desc': False, + 'version': False, + 'icon': False} + server_stats = {} server = self.controller.get_server_obj(server_id) + if not server: + return {} server_dt = servers_helper.get_server_data_by_id(server_id) @@ -299,30 +362,98 @@ class Stats: int_data = False ping_data = {} + #Makes sure we only show stats when a server is online otherwise people have gotten confused. + if server_obj.check_running(): + # if we got a good ping return, let's parse it + if servers_helper.get_server_type_by_id(server_id) != 'minecraft-bedrock': + if int_mc_ping: + int_data = True + ping_data = self.parse_server_ping(int_mc_ping) - # if we got a good ping return, let's parse it - if int_mc_ping: - int_data = True - ping_data = self.parse_server_ping(int_mc_ping) + server_stats = { + 'id': server_id, + 'started': server_obj.get_start_time(), + 'running': server_obj.check_running(), + 'cpu': p_stats.get('cpu_usage', 0), + 'mem': p_stats.get('memory_usage', 0), + "mem_percent": p_stats.get('mem_percentage', 0), + 'world_name': world_name, + 'world_size': self.get_world_size(world_path), + 'server_port': server_port, + 'int_ping_results': int_data, + 'online': ping_data.get("online", False), + "max": ping_data.get("max", False), + 'players': ping_data.get("players", False), + 'desc': ping_data.get("server_description", False), + 'version': ping_data.get("server_version", False), + 'icon': ping_data.get("server_icon", False) + } - server_stats = { - 'id': server_id, - 'started': server_obj.get_start_time(), - 'running': server_obj.check_running(), - 'cpu': p_stats.get('cpu_usage', 0), - 'mem': p_stats.get('memory_usage', 0), - "mem_percent": p_stats.get('mem_percentage', 0), - 'world_name': world_name, - 'world_size': self.get_world_size(world_path), - 'server_port': server_port, - 'int_ping_results': int_data, - 'online': ping_data.get("online", False), - "max": ping_data.get("max", False), - 'players': ping_data.get("players", False), - 'desc': ping_data.get("server_description", False), - 'version': ping_data.get("server_version", False), - 'icon': ping_data.get("server_icon", False) - } + else: + if int_mc_ping: + int_data = True + ping_data = self.parse_server_RakNet_ping(int_mc_ping) + try: + server_icon = base64.encodebytes(ping_data['icon']) + except Exception as e: + server_icon = False + logger.info(f"Unable to read the server icon : {e}") + + server_stats = { + 'id': server_id, + 'started': server_obj.get_start_time(), + 'running': server_obj.check_running(), + 'cpu': p_stats.get('cpu_usage', 0), + 'mem': p_stats.get('memory_usage', 0), + "mem_percent": p_stats.get('mem_percentage', 0), + 'world_name': world_name, + 'world_size': self.get_world_size(world_path), + 'server_port': server_port, + 'int_ping_results': int_data, + 'online': ping_data['online'], + 'max': ping_data['max'], + 'players': [], + 'desc': ping_data['server_description'], + 'version': ping_data['server_version'], + 'icon': server_icon + } + else: + server_stats = { + 'id': server_id, + 'started': server_obj.get_start_time(), + 'running': server_obj.check_running(), + 'cpu': p_stats.get('cpu_usage', 0), + 'mem': p_stats.get('memory_usage', 0), + "mem_percent": p_stats.get('mem_percentage', 0), + 'world_name': world_name, + 'world_size': self.get_world_size(world_path), + 'server_port': server_port, + 'int_ping_results': int_data, + 'online': False, + 'max': False, + 'players': False, + 'desc': False, + 'version': False, + 'icon': False + } + else: + server_stats = { + 'id': server_id, + 'started': server_obj.get_start_time(), + 'running': server_obj.check_running(), + 'cpu': p_stats.get('cpu_usage', 0), + 'mem': p_stats.get('memory_usage', 0), + "mem_percent": p_stats.get('mem_percentage', 0), + 'world_name': world_name, + 'world_size': self.get_world_size(world_path), + 'server_port': server_port, + 'int_ping_results': int_data, + 'online': False, + "max": False, + 'players': False, + 'desc': False, + 'version': False + } return server_stats diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 7cedc545..d9877f90 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -652,8 +652,15 @@ class Helpers: @staticmethod def generate_tree(folder, output=""): + dir_list = [] + unsorted_files = [] file_list = os.listdir(folder) - file_list = sorted(file_list, key=str.casefold) + for item in file_list: + if os.path.isdir(os.path.join(folder, item)): + dir_list.append(item) + else: + unsorted_files.append(item) + file_list = sorted(dir_list, key=str.casefold) + sorted(unsorted_files, key=str.casefold) for raw_filename in file_list: filename = html.escape(raw_filename) rel = os.path.join(folder, raw_filename) @@ -673,7 +680,7 @@ class Helpers: else: if filename != "crafty_managed.txt": output += f"""