From aaa6de38eb23a89a0422a7ae10dc2a79453b793b Mon Sep 17 00:00:00 2001 From: Janis Date: Thu, 4 Jan 2024 18:54:39 +0100 Subject: [PATCH] Update code: Fix linting issues and optimize code --- .vscode/settings.json | 3 +- app/app.py | 30 ++++---- app/requestHandler.py | 162 +++++++++++++++++++++++++++++++++-------- app/socket_server.py | 163 ++++++++++++++++++++++++++++++++++++++++++ app/utils.py | 72 ++++++++++++------- 5 files changed, 357 insertions(+), 73 deletions(-) create mode 100644 app/socket_server.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 18b0b06..dbc995f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "python.analysis.autoImportCompletions": true, "python.analysis.diagnosticSeverityOverrides": { "reportMissingTypeStubs": "none" - } + }, + "cSpell.words": ["REUSEADDR", "varint"] } diff --git a/app/app.py b/app/app.py index 02a5968..5d601f9 100644 --- a/app/app.py +++ b/app/app.py @@ -4,25 +4,23 @@ from nginxHandler import NginxHandler from minecraftServerHandler import MinecraftServerHandler from requestHandler import RequestHandler import logging -from FakeMCServer.fake_mc_server import FakeMCServer -import threading logging.basicConfig(level=logging.INFO) -def init_placeholder_servers(): - sleeping = FakeMCServer(port=20000, motd={ - "1": "sleeping!", "2": "§aCheck example.com for more information!"}) - starting = FakeMCServer(port=20001, motd={ - "1": "starting!", "2": "§aCheck example.com for more information!"}) +# def init_placeholder_servers(): +# sleeping = FakeMCServer(port=20000, motd={ +# "1": "sleeping!", "2": "§aCheck example.com for more information!"}) +# starting = FakeMCServer(port=20001, motd={ +# "1": "starting!", "2": "§aCheck example.com for more information!"}) - # Create threads for each server initialization - sleeping_thread = threading.Thread(target=sleeping.start_server) - starting_thread = threading.Thread(target=starting.start_server) +# # Create threads for each server initialization +# sleeping_thread = threading.Thread(target=sleeping.start_server) +# starting_thread = threading.Thread(target=starting.start_server) - # Start the threads - sleeping_thread.start() - starting_thread.start() +# # Start the threads +# sleeping_thread.start() +# starting_thread.start() def initialize_docker_handler() -> DockerHandler: @@ -64,9 +62,9 @@ def initialize_request_handlers(docker_handler, minecraft_server_handler): def main() -> None: try: - logging.info('[INIT] initializing placeholder servers...') - init_placeholder_servers() - logging.info('[INIT] placeholder servers initialized') + # logging.info('[INIT] initializing placeholder servers...') + # init_placeholder_servers() + # logging.info('[INIT] placeholder servers initialized') docker_handler = initialize_docker_handler() nginx_handler = initialize_nginx_handler() diff --git a/app/requestHandler.py b/app/requestHandler.py index 57c1d68..fb9f72e 100644 --- a/app/requestHandler.py +++ b/app/requestHandler.py @@ -1,11 +1,15 @@ +import json import os import socket import logging import threading from typing import Literal +import uuid +from app import byte_utils from dockerHandler import DockerHandler from minecraftServerHandler import MinecraftServerHandler from objects.minecraftServer import MinecraftServer +import byte_utils class RequestHandler(threading.Thread): @@ -95,8 +99,9 @@ class RequestHandler(threading.Thread): if minecraft_server.is_starting() == True: logging.info( f'[RequestHandler:{self.port}] container {service_name} is already starting...') - self.forward_request_to_placeholder( - request, minecraft_server) + self.send_response(self.client_address) + # self.forward_request_to_placeholder( + # request, minecraft_server) else: logging.info( f'[RequestHandler:{self.port}] starting container {service_name}') @@ -105,43 +110,142 @@ class RequestHandler(threading.Thread): elif b'\x01' in request: logging.info( f'[RequestHandler:{self.port}] detected ping request for {service_name}') - self.forward_request_to_placeholder( - request, minecraft_server) + self.send_response(self.client_address) + + # self.forward_request_to_placeholder( + # request, minecraft_server) elif request[0] == 0xFE: logging.info( f'[RequestHandler:{self.port}] detected legacy ping request for {service_name}') - self.forward_request_to_placeholder(request, minecraft_server) + self.send_response(self.client_address) + + # self.forward_request_to_placeholder(request, minecraft_server) else: logging.info( f'[RequestHandler:{self.port}] detected unknown request for {service_name}') - self.forward_request_to_placeholder(request, minecraft_server) + self.send_response(self.client_address) + + # self.forward_request_to_placeholder(request, minecraft_server) else: logging.info( f'[RequestHandler:{self.port}] no container mapped to port {self.port}') - def forward_request_to_placeholder(self, request, minecraft_server: MinecraftServer) -> None: - logging.info( - '[RequestHandler:{self.port}] forwarding request to placeholder server') - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket: - ip = "127.0.0.1" - logging.info( - f'[RequestHandler:{self.port}] placeholder server ip: {ip}') - try: - if minecraft_server.is_starting() == True: - logging.info( - '[RequestHandler:{self.port}] container is starting. Using placeholder port 20001') - server_socket.connect((ip, 20001)) - else: - logging.info( - '[RequestHandler:{self.port}] container is not starting. Using placeholder port 20000') - server_socket.connect((ip, 20000)) + def send_response(self, addr): + client_socket = self.connection - server_socket.sendall(request) - response = server_socket.recv(1024) - self.connection.sendall(response) - except Exception as e: - logging.info( - f'[RequestHandler:{self.port}] error while handling request on port {self.port}: {e}') - self.restart() + data = client_socket.recv(1024) + client_ip = addr[0] + + fqdn = socket.getfqdn(client_ip) + if self.show_hostname and client_ip != fqdn: + client_ip = fqdn + "/" + client_ip + + try: + (length, i) = byte_utils.read_varint(data, 0) + (packetID, i) = byte_utils.read_varint(data, i) + + if packetID == 0: + (version, i) = byte_utils.read_varint(data, i) + (ip, i) = byte_utils.read_utf(data, i) + + ip = ip.replace('\x00', '').replace("\r", "\\r").replace( + "\t", "\\t").replace("\n", "\\n") + is_using_fml = False + + if ip.endswith("FML"): + is_using_fml = True + ip = ip[:-3] + + (port, i) = byte_utils.read_ushort(data, i) + (state, i) = byte_utils.read_varint(data, i) + + if state == 1: + self.logger.info(("[%s:%s] Received client " + ("(using ForgeModLoader) " if is_using_fml else "") + + "ping packet (%s:%s).") % (client_ip, addr[1], ip, port)) + motd = {} + motd["version"] = {} + motd["version"]["name"] = "testing" + motd["version"]["protocol"] = 2 + motd["players"] = {} + motd["players"]["max"] = 0 + motd["players"]["online"] = 0 + motd["players"]["sample"] = [] + + for sample in self.samples: + motd["players"]["sample"].append( + {"name": sample, "id": str(uuid.uuid4())}) + + motd["description"] = {"text": { + "1": "§4Maintensadfance!", "2": "§aCheck example.caom for more information!"}} + + if self.server_icon and len(self.server_icon) > 0: + motd["favicon"] = self.server_icon + + self.write_response(client_socket, json.dumps(motd)) + elif state == 2: + name = "" + if len(data) != i: + (some_int, i) = byte_utils.read_varint(data, i) + (some_int, i) = byte_utils.read_varint(data, i) + (name, i) = byte_utils.read_utf(data, i) + self.logger.info( + ("[%s:%s] " + (name + " t" if len(name) > 0 else "T") + "ries to connect to the server " + + ("(using ForgeModLoader) " if is_using_fml else "") + "(%s:%s).") + % (client_ip, addr[1], ip, port)) + self.write_response(client_socket, json.dumps( + {"text": ["§bSorry", "", "§aThis servzzzer is offline!"]})) + else: + self.logger.info( + "[%s:%d] Tried to request a login/ping with an unknown state: %d" % (client_ip, addr[1], state)) + elif packetID == 1: + (long, i) = byte_utils.read_long(data, i) + response = bytearray() + byte_utils.write_varint(response, 9) + byte_utils.write_varint(response, 1) + bytearray.append(long) + client_socket.sendall(bytearray) + self.logger.info( + "[%s:%d] Responded with pong packet." % (client_ip, addr[1])) + else: + self.logger.warning("[%s:%d] Sent an unexpected packet: %d" % ( + client_ip, addr[1], packetID)) + except (TypeError, IndexError): + self.logger.warning( + "[%s:%s] Received invalid data (%s)" % (client_ip, addr[1], data)) + return + + def write_response(self, client_socket, response): + response_array = bytearray() + byte_utils.write_varint(response_array, 0) + byte_utils.write_utf(response_array, response) + length = bytearray() + byte_utils.write_varint(length, len(response_array)) + client_socket.sendall(length) + client_socket.sendall(response_array) + + # def forward_request_to_placeholder(self, request, minecraft_server: MinecraftServer) -> None: + # logging.info( + # '[RequestHandler:{self.port}] forwarding request to placeholder server') + # with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket: + # ip = "127.0.0.1" + # logging.info( + # f'[RequestHandler:{self.port}] placeholder server ip: {ip}') + # try: + # if minecraft_server.is_starting() == True: + # logging.info( + # '[RequestHandler:{self.port}] container is starting. Using placeholder port 20001') + # server_socket.connect((ip, 20001)) + # else: + # logging.info( + # '[RequestHandler:{self.port}] container is not starting. Using placeholder port 20000') + # server_socket.connect((ip, 20000)) + + # server_socket.sendall(request) + # response = server_socket.recv(1024) + # self.connection.sendall(response) + # except Exception as e: + # logging.info( + # f'[RequestHandler:{self.port}] error while handling request on port {self.port}: {e}') + # self.restart() diff --git a/app/socket_server.py b/app/socket_server.py new file mode 100644 index 0000000..8c259fe --- /dev/null +++ b/app/socket_server.py @@ -0,0 +1,163 @@ +import json +import socket +import uuid +import logging +from threading import Thread +import utils as utils + + +class SocketServer: + def __init__(self, port: int, ip: str = "0.0.0.0", motd: dict = {"1": "§4Maintenance!", "2": "§aCheck example.com for more information!"}, + version_text: str = "§4Maintenance", kick_message: list = ["§bSorry", "", "§aThis server is offline!"], + server_icon: str = "server_icon.png", samples: list = ["§bexample.com", "", "§4Maintenance"], show_hostname_if_available: bool = True, + player_max: int = 0, player_online: int = 0, protocol: int = 2): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.ip = ip + self.port = port + self.motd = motd + self.version_text = version_text + self.kick_message = kick_message + self.samples = samples + self.server_icon = server_icon + self.show_hostname = show_hostname_if_available + self.player_max = player_max + self.player_online = player_online + self.protocol = protocol + + def is_server_list_ping(self, request, service_name): + if request[0] == 0xFE: + logging.info( + f'[RequestHandler:{self.port}] detected legacy ping request for {service_name}') + self.send_response(self.client_address) + elif b'\x01' in request: + logging.info( + f'[RequestHandler:{self.port}] detected ping request for {service_name}') + self.send_response(self.client_address) + else: + logging.info( + f'[RequestHandler:{self.port}] detected unknown request for {service_name}') + self.send_response(self.client_address) + + def is_join_attempt(self, request, service_name, minecraft_server): + if request[0] == 0x10 or request[0] == 0x15 or request[0] == 0x1b: + if b'\x02' in request: + logging.info( + f'[RequestHandler:{self.port}] detected join/login request for {service_name}') + if minecraft_server.is_starting() == True: + logging.info( + f'[RequestHandler:{self.port}] container {service_name} is already starting...') + self.send_response(self.client_address) + else: + logging.info( + f'[RequestHandler:{self.port}] starting container {service_name}') + self.minecraft_server_handler.start_server(service_name) + + def on_new_client(self, client_socket, addr): + data = client_socket.recv(1024) + client_ip = addr[0] + + if self.is_server_list_ping(data): + fqdn = socket.getfqdn(client_ip) + if self.show_hostname and client_ip != fqdn: + client_ip = fqdn + "/" + client_ip + + try: + (length, i) = utils.read_varint(data, 0) + (packetID, i) = utils.read_varint(data, i) + + if packetID == 0: + (version, i) = utils.read_varint(data, i) + (ip, i) = utils.read_utf(data, i) + + ip = ip.replace('\x00', '').replace("\r", "\\r").replace( + "\t", "\\t").replace("\n", "\\n") + is_using_fml = False + + if ip.endswith("FML"): + is_using_fml = True + ip = ip[:-3] + + (port, i) = utils.read_ushort(data, i) + (state, i) = utils.read_varint(data, i) + + if state == 1: + logging.info(("[%s:%s] Received client " + ("(using ForgeModLoader) " if is_using_fml else "") + + "ping packet (%s:%s).") % (client_ip, addr[1], ip, port)) + motd = {} + motd["version"] = {} + motd["version"]["name"] = self.version_text + motd["version"]["protocol"] = self.protocol + motd["players"] = {} + motd["players"]["max"] = self.player_max + motd["players"]["online"] = self.player_online + motd["players"]["sample"] = [] + + for sample in ["§bexamplaaaaaaaae.com", "", "§4Maintaaaaaaaaaaaaaaenance"]: + motd["players"]["sample"].append( + {"name": sample, "id": str(uuid.uuid4())}) + + motd["description"] = {"text": self.motd} + + if self.server_icon and len(self.server_icon) > 0: + motd["favicon"] = self.server_icon + + self.write_response(client_socket, json.dumps(motd)) + elif state == 2: + name = "" + if len(data) != i: + (some_int, i) = utils.read_varint(data, i) + (some_int, i) = utils.read_varint(data, i) + (name, i) = utils.read_utf(data, i) + logging.info( + ("[%s:%s] " + (name + " t" if len(name) > 0 else "T") + "ries to connect to the server " + + ("(using ForgeModLoader) " if is_using_fml else "") + "(%s:%s).") + % (client_ip, addr[1], ip, port)) + self.write_response(client_socket, json.dumps( + {"text": "kicked"})) + else: + logging.info( + "[%s:%d] Tried to request a login/ping with an unknown state: %d" % (client_ip, addr[1], state)) + elif packetID == 1: + (long, i) = utils.read_long(data, i) + response = bytearray() + utils.write_varint(response, 9) + utils.write_varint(response, 1) + bytearray.append(long) + client_socket.sendall(bytearray) + logging.info( + "[%s:%d] Responded with pong packet." % (client_ip, addr[1])) + else: + logging.warning("[%s:%d] Sent an unexpected packet: %d" % ( + client_ip, addr[1], packetID)) + except (TypeError, IndexError): + logging.warning( + "[%s:%s] Received invalid data (%s)" % (client_ip, addr[1], data)) + return + + def write_response(self, client_socket, response): + response_array = bytearray() + utils.write_varint(response_array, 0) + utils.write_utf(response_array, response) + length = bytearray() + utils.write_varint(length, len(response_array)) + client_socket.sendall(length) + client_socket.sendall(response_array) + + def start(self): + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.sock.bind((self.ip, self.port)) + self.sock.settimeout(5) + self.sock.listen(30) + logging.info("Server started on %s:%s! Waiting for incoming connections..." % ( + self.ip, self.port)) + while 1: + try: + (client, address) = self.sock.accept() + except socket.timeout: + continue # timeouts may occur but shouldn't worry uns server-side + + Thread(target=self.on_new_client, daemon=True, + args=(client, address,)).start() + + def close(self): + self.sock.close() diff --git a/app/utils.py b/app/utils.py index 5209bef..c9b38e8 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,8 +1,7 @@ import logging import os -import json -import json -from typing import List, Dict +import struct +from typing import Dict def docker_container_mapping() -> Dict[str, str]: @@ -22,29 +21,48 @@ def docker_container_mapping() -> Dict[str, str]: return port_map -# motd = { -# "1": "§4Maintenance!", -# "2": "§aCheck example.com for more information!" -# } -# version_text = "§4Maintenance" -# samples = ["§bexample.com", "", "§4Maintenance"] -# kick_message = ["§bSorry", "", "§aThis server is offline!"] +def read_varint(byte, i): + result = 0 + bytes = 0 + while True: + byte_in = byte[i] + i += 1 + result |= (byte_in & 0x7F) << (bytes * 7) + if bytes > 32: + raise IOError("Packet is too long!") + if (byte_in & 0x80) != 0x80: + return result, i -def generate_placeholder_server_config_file(path: str, ip: str, port: int, motd: Dict[str, str], version_text: str, samples: List[str], kick_message: List[str]) -> None: - config = { - "ip": ip, - "kick_message": kick_message, - "motd": motd, - "player_max": 0, - "player_online": 0, - "port": port, - "protocol": 2, - "samples": samples, - "server_icon": "server_icon.png", - "show_hostname_if_available": True, - "show_ip_if_hostname_available": True, - "version_text": version_text - } - with open(path, 'w') as f: - json.dump(config, f, indent=4) +def read_utf(byte, i): + (length, i) = read_varint(byte, i) + ip = byte[i:(i + length)].decode('utf-8') + i += length + return ip, i + + +def read_ushort(byte, i): + new_i = i + 2 + return struct.unpack(">H", byte[i:new_i])[0], new_i + + +def read_long(byte, i): + new_i = i + 8 + return struct.unpack(">q", byte[i:new_i]), new_i + + +def write_varint(byte, value): + while True: + part = value & 0x7F + value >>= 7 + if value != 0: + part |= 0x80 + byte.append(part) + if value == 0: + break + + +def write_utf(byte, value): + write_varint(byte, len(value)) + for b in value.encode(): + byte.append(b)