From 3c48364998e32b95068e2eaf6b037fcf1a5522ac Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 28 Feb 2022 22:40:11 -0500 Subject: [PATCH] BEDROCK SUPPORT. Ping works. Add notification for backups starting and completing. --- app/classes/minecraft/bedrock_ping.py | 109 ++++++++++++++++++++++ app/classes/minecraft/mc_ping.py | 68 +++----------- app/classes/minecraft/stats.py | 125 ++++++++++++++++++++------ app/classes/shared/server.py | 8 ++ 4 files changed, 227 insertions(+), 83 deletions(-) create mode 100644 app/classes/minecraft/bedrock_ping.py diff --git a/app/classes/minecraft/bedrock_ping.py b/app/classes/minecraft/bedrock_ping.py new file mode 100644 index 00000000..e3b64f5e --- /dev/null +++ b/app/classes/minecraft/bedrock_ping.py @@ -0,0 +1,109 @@ +import os +import psutil +import pprint +import socket +import time + +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: + raise IndexError("Ran out of pattern with additional bytes remaining") + 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): + t_start = time.perf_counter() + 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..2b6c5440 100644 --- a/app/classes/minecraft/mc_ping.py +++ b/app/classes/minecraft/mc_ping.py @@ -3,9 +3,11 @@ import socket import base64 import json import os +import sys import logging.config from app.classes.shared.console import console +from app.classes.minecraft.bedrock_ping import BedrockPing logger = logging.getLogger(__name__) @@ -168,63 +170,13 @@ 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) + if len(sys.argv) > 3: + client_guid = sys.argv[3] + else: + client_guid = 0 try: - sock.connect((ip, port)) + brp = BedrockPing(ip, port, client_guid) + return brp.ping() + except socket.timeout: + logger.debug("Unable to get RakNet stats") - except: - print("in first except") - return False - - 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() diff --git a/app/classes/minecraft/stats.py b/app/classes/minecraft/stats.py index d117699a..895ebf37 100644 --- a/app/classes/minecraft/stats.py +++ b/app/classes/minecraft/stats.py @@ -161,6 +161,27 @@ class Stats: return ping_data + @staticmethod + def parse_server_RakNet_ping(ping_obj: object): + online_stats = {} + + 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': online_stats.get('players', 0), + '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): server = servers_helper.get_server_data_by_id(server_id) @@ -177,11 +198,10 @@ 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 @@ -237,7 +257,10 @@ 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) + if servers_helper.get_server_type_by_id(server_id) == 'minecraft-bedrock': + ping_data = self.parse_server_RakNet_ping(int_mc_ping) + else: + ping_data = self.parse_server_ping(int_mc_ping) server_stats = { 'id': server_id, @@ -301,28 +324,80 @@ class Stats: ping_data = {} # 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) + 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) - 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['server_player_count'], + 'max': ping_data['server_player_max'], + 'players': 0, + 'server_description': ping_data['server_edition'], + 'server_version': ping_data['server_version_name'], + 'server_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': 0, + 'max': 0, + 'players': 0, + 'server_description': '', + 'server_version': '', + 'server_icon': '' + } return server_stats diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index 3bcb2d0d..b9ead600 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -575,6 +575,10 @@ class Server: def a_backup_server(self): logger.info(f"Starting server {self.name} (ID {self.server_id}) backup") + server_users = server_permissions.get_server_user_list(self.server_id) + for user in server_users: + websocket_helper.broadcast_user(user, 'notification', "Starting backup for " + self.name) + time.sleep(3) self.is_backingup = True conf = management_helper.get_backup_config(self.server_id) try: @@ -611,6 +615,10 @@ class Server: self.is_backingup = False file_helper.del_dirs(tempDir) logger.info(f"Backup of server: {self.name} completed") + server_users = server_permissions.get_server_user_list(self.server_id) + for user in server_users: + websocket_helper.broadcast_user(user, 'notification', "Backup completed successfully for " + self.name) + time.sleep(3) return except: logger.exception(f"Failed to create backup of server {self.name} (ID {self.server_id})")