From 0eb86721ff2c22f378f8432affb69232ce2bbe83 Mon Sep 17 00:00:00 2001 From: dertyp7 Date: Wed, 27 Dec 2023 16:21:45 +0100 Subject: [PATCH] Update Dockerfile and utils.py --- Dockerfile | 6 +- app/FakeMCServer | 1 + app/app.py | 132 +++++++++++-------- app/dockerHandler.py | 196 ++++++++++++++-------------- app/nginxHandler.py | 184 +++++++++++++------------- app/requestHandler.py | 297 +++++++++++++++++++++--------------------- app/utils.py | 70 +++++++--- docker-compose.yml | 145 +++++++-------------- nginx-example.conf | 54 ++++---- readme.md | 34 ++--- 10 files changed, 556 insertions(+), 563 deletions(-) create mode 160000 app/FakeMCServer diff --git a/Dockerfile b/Dockerfile index 9742fe4..c3451e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,11 +3,9 @@ FROM nginx:1.25.3 ENV PORT_MAP "" ENV PYTHONUNBUFFERED=1 -ENV PLACEHOLDER_SERVER_SLEEPING_IP "" -ENV PLACEHOLDER_SERVER_STARTING_IP "" -# Install Python and pip -RUN apt-get update && apt-get install -y python3 python3-pip python3-venv +# Install Python, pip and git +RUN apt-get update && apt-get install -y python3 python3-pip python3-venv # Create a virtual environment and activate it RUN python3 -m venv /app/venv diff --git a/app/FakeMCServer b/app/FakeMCServer new file mode 160000 index 0000000..d7d7bed --- /dev/null +++ b/app/FakeMCServer @@ -0,0 +1 @@ +Subproject commit d7d7bedb2b9b4242c97e55954317a06e0ff807cd diff --git a/app/app.py b/app/app.py index 8d74968..af3c91a 100644 --- a/app/app.py +++ b/app/app.py @@ -1,56 +1,76 @@ -from math import log -import time -from dockerHandler import DockerHandler -from nginxHandler import NginxHandler -from minecraftServerHandler import MinecraftServerHandler -from requestHandler import RequestHandler -import logging - - -logging.basicConfig(level=logging.INFO) - - -def main() -> None: - try: - logging.info('[INIT] initializing auto starter...') - logging.info('[INIT] initializing docker handler...') - docker_handler: DockerHandler = DockerHandler( - 'unix://var/run/docker.sock') - logging.info('[INIT] docker handler initialized') - - logging.info('[INIT] initializing nginx handler...') - nginx_handler: NginxHandler = NginxHandler('/etc/nginx/nginx.conf') - - nginx_handler.update_config_file( - docker_handler) - - logging.info('[INIT] nginx handler initialized') - - logging.info('[INIT] initializing minecraft server handler...') - minecraft_server_handler: MinecraftServerHandler = MinecraftServerHandler( - docker_handler, nginx_handler) - - # Find all Minecraft servers and add them to the MinecraftServerHandler instance - for service_name in docker_handler.get_port_map().values(): - minecraft_server_handler.add_server(service_name) - - logging.info('[INIT] wait 20 seconds before stopping all servers...') - time.sleep(20) - minecraft_server_handler.stop_all_servers() - logging.info('[INIT] minecraft server handler initialized') - - logging.info('[INIT] initializing request handlers...') - # Create a RequestHandler instance for each port - for port in docker_handler.get_port_map().keys(): - logging.info(f'[INIT] creating request handler for port {port}') - request_handler: RequestHandler = RequestHandler( - int(port), docker_handler, minecraft_server_handler) - request_handler.start() - logging.info('[INIT] request handlers initialized') - - except Exception as e: - logging.error(f'An error occurred: {e}') - - -if __name__ == "__main__": - main() +import os +import time +from dockerHandler import DockerHandler +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!"}) + + # 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() + + +def main() -> None: + try: + logging.info('[INIT] initializing placeholder servers...') + init_placeholder_servers() + logging.info('[INIT] placeholder servers initialized') + + logging.info('[INIT] initializing auto starter...') + logging.info('[INIT] initializing docker handler...') + docker_handler: DockerHandler = DockerHandler( + 'unix://var/run/docker.sock') + logging.info('[INIT] docker handler initialized') + + logging.info('[INIT] initializing nginx handler...') + nginx_handler: NginxHandler = NginxHandler('/etc/nginx/nginx.conf') + + nginx_handler.update_config_file( + docker_handler) + + logging.info('[INIT] nginx handler initialized') + + logging.info('[INIT] initializing minecraft server handler...') + minecraft_server_handler: MinecraftServerHandler = MinecraftServerHandler( + docker_handler, nginx_handler) + + # Find all Minecraft servers and add them to the MinecraftServerHandler instance + for service_name in docker_handler.get_port_map().values(): + minecraft_server_handler.add_server(service_name) + + logging.info('[INIT] wait 20 seconds before stopping all servers...') + time.sleep(20) + minecraft_server_handler.stop_all_servers() + logging.info('[INIT] minecraft server handler initialized') + + logging.info('[INIT] initializing request handlers...') + # Create a RequestHandler instance for each port + for port in docker_handler.get_port_map().keys(): + logging.info(f'[INIT] creating request handler for port {port}') + request_handler: RequestHandler = RequestHandler( + int(port), docker_handler, minecraft_server_handler) + request_handler.start() + logging.info('[INIT] request handlers initialized') + + except Exception as e: + logging.error(f'An error occurred: {e}') + + +if __name__ == "__main__": + main() diff --git a/app/dockerHandler.py b/app/dockerHandler.py index 4dd096c..5baa27f 100644 --- a/app/dockerHandler.py +++ b/app/dockerHandler.py @@ -1,98 +1,98 @@ -from typing import Dict -import docker -from docker import DockerClient -from docker.models.networks import Network -import os -import logging - -from utils import docker_container_mapping - - -class DockerHandler: - def __init__(self, base_url: str): - logging.info( - f'[DockerHandler] initializing docker handler with base url {base_url} and port ip map: {self.get_port_map()}...') - self.base_url: str = base_url - self.client: DockerClient = DockerClient(base_url=base_url) - self.current_network: Network = self.get_current_network() - logging.info('[DockerHandler] docker handler initialized') - logging.info( - f'[DockerHandler] current container name: {self.get_auto_starter_container_name()}') - logging.info( - f'[DockerHandler] current network: {str(self.current_network)}') - - def get_port_map(self) -> Dict[str, str]: - return docker_container_mapping() - - def stop_container(self, container) -> None: - if container: - logging.info( - f'[DockerHandler] stopping container {str(container.name)}') - container.stop() - logging.info(f'[DockerHandler] container {container.name} stopped') - else: - logging.info('[DockerHandler] no container to stop') - - def start_container(self, container) -> None: - if container: - logging.info( - f'[DockerHandler] starting container {container.name}') - container.start() - logging.info(f'[DockerHandler] container {container.name} started') - else: - logging.info('[DockerHandler] no container to start') - - def get_container_by_service_name(self, service_name): - logging.info( - f'[DockerHandler] getting container by service name {service_name}...') - try: - containers = self.client.containers.list( - all=True, filters={"network": self.current_network}) - - if containers is None: - logging.info('[DockerHandler] no containers found in network') - return None - - for container in containers: - networks = container.attrs['NetworkSettings']['Networks'] - if self.current_network in networks and service_name in networks[self.current_network]['Aliases']: - logging.info( - f'[DockerHandler] found container {container.name} with service name {service_name} in network {self.current_network}') - return container - logging.info( - f'[DockerHandler] no docker container found with service name {service_name} in network {self.current_network}') - return None - - except docker.errors.APIError as e: - logging.error(f'Error getting container list: {e}') - return None - - def get_auto_starter_container_name(self) -> str | None: - return os.environ.get('HOSTNAME') - - def get_auto_starter_container(self): - hostname = os.environ.get('HOSTNAME') - if hostname: - return self.client.containers.get(hostname) - return None - - def get_current_network(self) -> Network: - current_container = self.get_auto_starter_container() - if current_container: - networks = current_container.attrs['NetworkSettings']['Networks'] - return list(networks.keys())[0] - return None - - def get_ip_by_service_name(self, service_name: str) -> str: - container = self.get_container_by_service_name(service_name) - if container: - networks = container.attrs['NetworkSettings']['Networks'] - return networks[self.current_network]['IPAddress'] - return "" - - def get_auto_starter_container_ip(self) -> str: - container = self.get_auto_starter_container() - if container: - networks = container.attrs['NetworkSettings']['Networks'] - return networks[self.current_network]['IPAddress'] - return "" +from typing import Dict +import docker +from docker import DockerClient +from docker.models.networks import Network +import os +import logging + +from utils import docker_container_mapping + + +class DockerHandler: + def __init__(self, base_url: str): + logging.info( + f'[DockerHandler] initializing docker handler with base url {base_url} and port ip map: {self.get_port_map()}...') + self.base_url: str = base_url + self.client: DockerClient = DockerClient(base_url=base_url) + self.current_network: Network = self.get_current_network() + logging.info('[DockerHandler] docker handler initialized') + logging.info( + f'[DockerHandler] current container name: {self.get_auto_starter_container_name()}') + logging.info( + f'[DockerHandler] current network: {str(self.current_network)}') + + def get_port_map(self) -> Dict[str, str]: + return docker_container_mapping() + + def stop_container(self, container) -> None: + if container: + logging.info( + f'[DockerHandler] stopping container {str(container.name)}') + container.stop() + logging.info(f'[DockerHandler] container {container.name} stopped') + else: + logging.info('[DockerHandler] no container to stop') + + def start_container(self, container) -> None: + if container: + logging.info( + f'[DockerHandler] starting container {container.name}') + container.start() + logging.info(f'[DockerHandler] container {container.name} started') + else: + logging.info('[DockerHandler] no container to start') + + def get_container_by_service_name(self, service_name): + logging.info( + f'[DockerHandler] getting container by service name {service_name}...') + try: + containers = self.client.containers.list( + all=True, filters={"network": self.current_network}) + + if containers is None: + logging.info('[DockerHandler] no containers found in network') + return None + + for container in containers: + networks = container.attrs['NetworkSettings']['Networks'] + if self.current_network in networks and service_name in networks[self.current_network]['Aliases']: + logging.info( + f'[DockerHandler] found container {container.name} with service name {service_name} in network {self.current_network}') + return container + logging.info( + f'[DockerHandler] no docker container found with service name {service_name} in network {self.current_network}') + return None + + except docker.errors.APIError as e: + logging.error(f'Error getting container list: {e}') + return None + + def get_auto_starter_container_name(self) -> str | None: + return os.environ.get('HOSTNAME') + + def get_auto_starter_container(self): + hostname = os.environ.get('HOSTNAME') + if hostname: + return self.client.containers.get(hostname) + return None + + def get_current_network(self) -> Network: + current_container = self.get_auto_starter_container() + if current_container: + networks = current_container.attrs['NetworkSettings']['Networks'] + return list(networks.keys())[0] + return None + + def get_ip_by_service_name(self, service_name: str) -> str: + container = self.get_container_by_service_name(service_name) + if container: + networks = container.attrs['NetworkSettings']['Networks'] + return networks[self.current_network]['IPAddress'] + return "" + + def get_auto_starter_container_ip(self) -> str: + container = self.get_auto_starter_container() + if container: + networks = container.attrs['NetworkSettings']['Networks'] + return networks[self.current_network]['IPAddress'] + return "" diff --git a/app/nginxHandler.py b/app/nginxHandler.py index e2be859..b82fb2c 100644 --- a/app/nginxHandler.py +++ b/app/nginxHandler.py @@ -1,92 +1,92 @@ -import os -import logging -from typing import TextIO, Dict - -from dockerHandler import DockerHandler - - -class NginxHandler: - def __init__(self, config_path: str): - logging.info('[NginxHandler] initializing nginx handler...') - self.config_path: str = config_path - - def start(self) -> None: - logging.info('[NginxHandler] starting nginx...') - os.system('nginx > /dev/null 2>&1 &') - logging.info('[NginxHandler] nginx started') - - def stop(self) -> None: - logging.info('[NginxHandler] stopping nginx...') - os.system('nginx -s stop') - logging.info('[NginxHandler] nginx stopped') - - def restart(self) -> None: - self.stop() - self.start() - - def print_config(self) -> None: - logging.info('[NginxHandler] printing nginx config file...') - logging.info('========================================') - with open(self.config_path, 'r') as f: - logging.info(f.read()) - logging.info('========================================') - logging.info('[NginxHandler] nginx config file printed') - - def update_config_file(self, docker_handler: DockerHandler) -> None: - logging.info('[NginxHandler] updating nginx config file...') - self.stop() - port_map: Dict[str, str] = docker_handler.get_port_map() - if port_map is None: - logging.error('[NginxHandler] port_map is None') - return - - proxy_timeout: str = "5s" - logging.info('[NginxHandler] setting up NGINX config file...') - logging.info('[NginxHandler] port_map: {}'.format(port_map)) - nginx_conf: TextIO = open(self.config_path, 'w+') - nginx_conf.truncate() - nginx_conf.write('worker_processes 5;\n') - nginx_conf.write('events { \n') - nginx_conf.write(' worker_connections 1024;\n') - nginx_conf.write(' multi_accept on;\n') - nginx_conf.write('}\n') - nginx_conf.write('stream {\n') - - # This looks confusing, but the nginx.conf looks good when it's done - # Example for the nginx-example.conf file is in the repo root directory - if isinstance(port_map, dict): - for port in port_map: - ip = docker_handler.get_ip_by_service_name(port_map[port]) - - nginx_conf.write( - f' # docker service {port_map[port]} on port {port}\n') - nginx_conf.write(f' upstream upstream_{port} {{\n') - - if ip == "": - nginx_conf.write(f' server 127.0.0.1:{port};\n') - else: - nginx_conf.write(f' server {ip}:25565;\n') - nginx_conf.write( - f' server 127.0.0.1:{port} backup;\n') - nginx_conf.write(' }\n') - - nginx_conf.write(' server {\n') - nginx_conf.write( - f' listen {docker_handler.get_auto_starter_container_ip()}:{port};\n') - - nginx_conf.write( - f' proxy_connect_timeout {proxy_timeout};\n') - nginx_conf.write(f' proxy_timeout {proxy_timeout};\n') - - nginx_conf.write(f' proxy_pass upstream_{port};\n') - nginx_conf.write(' }\n') - else: - logging.error('port_map is not a dictionary') - - nginx_conf.write('}\n') - nginx_conf.close() - logging.info('[NginxHandler] nginx config file setup complete') - self.start() - - # Restart for good measure. Add inconsistency issues with nginx - self.restart() +import os +import logging +from typing import TextIO, Dict + +from dockerHandler import DockerHandler + + +class NginxHandler: + def __init__(self, config_path: str): + logging.info('[NginxHandler] initializing nginx handler...') + self.config_path: str = config_path + + def start(self) -> None: + logging.info('[NginxHandler] starting nginx...') + os.system('nginx > /dev/null 2>&1 &') + logging.info('[NginxHandler] nginx started') + + def stop(self) -> None: + logging.info('[NginxHandler] stopping nginx...') + os.system('nginx -s stop') + logging.info('[NginxHandler] nginx stopped') + + def restart(self) -> None: + self.stop() + self.start() + + def print_config(self) -> None: + logging.info('[NginxHandler] printing nginx config file...') + logging.info('========================================') + with open(self.config_path, 'r') as f: + logging.info(f.read()) + logging.info('========================================') + logging.info('[NginxHandler] nginx config file printed') + + def update_config_file(self, docker_handler: DockerHandler) -> None: + logging.info('[NginxHandler] updating nginx config file...') + self.stop() + port_map: Dict[str, str] = docker_handler.get_port_map() + if port_map is None: + logging.error('[NginxHandler] port_map is None') + return + + proxy_timeout: str = "5s" + logging.info('[NginxHandler] setting up NGINX config file...') + logging.info('[NginxHandler] port_map: {}'.format(port_map)) + nginx_conf: TextIO = open(self.config_path, 'w+') + nginx_conf.truncate() + nginx_conf.write('worker_processes 5;\n') + nginx_conf.write('events { \n') + nginx_conf.write(' worker_connections 1024;\n') + nginx_conf.write(' multi_accept on;\n') + nginx_conf.write('}\n') + nginx_conf.write('stream {\n') + + # This looks confusing, but the nginx.conf looks good when it's done + # Example for the nginx-example.conf file is in the repo root directory + if isinstance(port_map, dict): + for port in port_map: + ip = docker_handler.get_ip_by_service_name(port_map[port]) + + nginx_conf.write( + f' # docker service {port_map[port]} on port {port}\n') + nginx_conf.write(f' upstream upstream_{port} {{\n') + + if ip == "": + nginx_conf.write(f' server 127.0.0.1:{port};\n') + else: + nginx_conf.write(f' server {ip}:25565;\n') + nginx_conf.write( + f' server 127.0.0.1:{port} backup;\n') + nginx_conf.write(' }\n') + + nginx_conf.write(' server {\n') + nginx_conf.write( + f' listen {docker_handler.get_auto_starter_container_ip()}:{port};\n') + + nginx_conf.write( + f' proxy_connect_timeout {proxy_timeout};\n') + nginx_conf.write(f' proxy_timeout {proxy_timeout};\n') + + nginx_conf.write(f' proxy_pass upstream_{port};\n') + nginx_conf.write(' }\n') + else: + logging.error('port_map is not a dictionary') + + nginx_conf.write('}\n') + nginx_conf.close() + logging.info('[NginxHandler] nginx config file setup complete') + self.start() + + # Restart for good measure. Add inconsistency issues with nginx + self.restart() diff --git a/app/requestHandler.py b/app/requestHandler.py index cdf0af0..57c1d68 100644 --- a/app/requestHandler.py +++ b/app/requestHandler.py @@ -1,150 +1,147 @@ -import os -import socket -import logging -import threading -from typing import Literal -from dockerHandler import DockerHandler -from minecraftServerHandler import MinecraftServerHandler -from objects.minecraftServer import MinecraftServer - - -class RequestHandler(threading.Thread): - def __init__(self, port: str, docker_handler: DockerHandler, minecraft_server_handler: MinecraftServerHandler): - logging.info( - f'[RequestHandler:{port}] initializing request handler...') - super().__init__() - self.port: str = port - - if not self.port: - logging.info( - f'[RequestHandler:{self.port}] no port specified') - return - - self.docker_handler: DockerHandler = docker_handler - self.minecraft_server_handler: MinecraftServerHandler = minecraft_server_handler - - self.sock: socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server_address: tuple[Literal['localhost'], str] = ( - 'localhost', self.port) - logging.info( - f'[RequestHandler:{self.port}] starting up on {server_address[0]} port {server_address[1]}') - self.sock.bind(server_address) - self.sock.listen(1) - logging.info( - f'[RequestHandler:{self.port}] request handler initialized') - - def restart(self): - logging.info( - f'[RequestHandler:{self.port}] restarting request handler for port {self.port}') - self.sock.close() - self.__init__(self.port, self.docker_handler, - self.minecraft_server_handler) - - def run(self) -> None: - while True: - try: - logging.info( - f'[RequestHandler:{self.port}] waiting for a connection on port {self.port}') - self.connection, self.client_address = self.sock.accept() - try: - logging.info( - f'[RequestHandler:{self.port}] connection from {self.client_address}') - self.handle_request() - except Exception as e: - logging.info( - f'[RequestHandler:{self.port}] error in request handler for port {self.port}: {e}') - logging.info( - '[RequestHandler:{self.port}] restarting request handler...') - self.restart() - finally: - self.connection.close() - self.restart() - except Exception as e: - logging.info( - f'[RequestHandler:{self.port}] error in request handler for port {self.port}: {e}') - logging.info( - '[RequestHandler:{self.port}] restarting request handler...') - self.restart() - - def handle_request(self) -> None: - logging.info( - f'[RequestHandler:{self.port}] handling request on port {self.port}') - - service_name = self.docker_handler.get_port_map().get(str(self.port)) - logging.info( - f'[RequestHandler:{self.port}] service name: {service_name}') - - if service_name: - minecraft_server: MinecraftServer = self.minecraft_server_handler.get_server( - service_name) - - if not minecraft_server: - logging.info( - f'[RequestHandler:{self.port}] no minecraft server found for service name {service_name}') - return - request = self.connection.recv(1024) - logging.info( - f'[RequestHandler:{self.port}] received request: {request}') - # b'\x1b\x00\xfb\x05\x14mc.tealfire.de\x00FML3\x00c\xa0\x02\x1a\x00\x07DerTyp7\x01\xf2]\x9a\x18*\xeaJ\xed\xbe0g\x9c\x8aT\xa9t' - 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.forward_request_to_placeholder( - request, minecraft_server) - else: - logging.info( - f'[RequestHandler:{self.port}] starting container {service_name}') - self.minecraft_server_handler.start_server( - service_name) - 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) - - 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) - else: - logging.info( - f'[RequestHandler:{self.port}] detected unknown request for {service_name}') - 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 = self.docker_handler.get_ip_by_service_name( - os.environ.get('PLACEHOLDER_SERVER_SLEEPING_SERVICE')) - if minecraft_server.is_starting() == True: - logging.info( - '[RequestHandler:{self.port}] container is starting. Using starting placeholder ip') - ip = self.docker_handler.get_ip_by_service_name( - os.environ.get('PLACEHOLDER_SERVER_STARTING_SERVICE')) - - if not ip: - logging.info( - '[RequestHandler:{self.port}] no placeholder server ip found') - return - - logging.info( - f'[RequestHandler:{self.port}] placeholder server ip: {ip}') - try: - server_socket.connect((ip, 25565)) - 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() +import os +import socket +import logging +import threading +from typing import Literal +from dockerHandler import DockerHandler +from minecraftServerHandler import MinecraftServerHandler +from objects.minecraftServer import MinecraftServer + + +class RequestHandler(threading.Thread): + def __init__(self, port: str, docker_handler: DockerHandler, minecraft_server_handler: MinecraftServerHandler): + logging.info( + f'[RequestHandler:{port}] initializing request handler...') + super().__init__() + self.port: str = port + + if not self.port: + logging.info( + f'[RequestHandler:{self.port}] no port specified') + return + + self.docker_handler: DockerHandler = docker_handler + self.minecraft_server_handler: MinecraftServerHandler = minecraft_server_handler + + self.sock: socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_address: tuple[Literal['localhost'], str] = ( + 'localhost', self.port) + logging.info( + f'[RequestHandler:{self.port}] starting up on {server_address[0]} port {server_address[1]}') + self.sock.bind(server_address) + self.sock.settimeout(5) + self.sock.listen(30) + logging.info( + f'[RequestHandler:{self.port}] request handler initialized') + + def restart(self): + logging.info( + f'[RequestHandler:{self.port}] restarting request handler for port {self.port}') + self.sock.close() + self.__init__(self.port, self.docker_handler, + self.minecraft_server_handler) + + def run(self) -> None: + while True: + try: + logging.info( + f'[RequestHandler:{self.port}] waiting for a connection on port {self.port}') + self.connection, self.client_address = self.sock.accept() + try: + logging.info( + f'[RequestHandler:{self.port}] connection from {self.client_address}') + self.handle_request() + except Exception as e: + logging.info( + f'[RequestHandler:{self.port}] error in request handler for port {self.port}: {e}') + logging.info( + '[RequestHandler:{self.port}] restarting request handler...') + self.restart() + finally: + self.connection.close() + self.restart() + except Exception as e: + logging.info( + f'[RequestHandler:{self.port}] error in request handler for port {self.port}: {e}') + logging.info( + '[RequestHandler:{self.port}] restarting request handler...') + self.restart() + + def handle_request(self) -> None: + logging.info( + f'[RequestHandler:{self.port}] handling request on port {self.port}') + + service_name = self.docker_handler.get_port_map().get(str(self.port)) + logging.info( + f'[RequestHandler:{self.port}] service name: {service_name}') + + if service_name: + minecraft_server: MinecraftServer = self.minecraft_server_handler.get_server( + service_name) + + if not minecraft_server: + logging.info( + f'[RequestHandler:{self.port}] no minecraft server found for service name {service_name}') + return + request = self.connection.recv(1024) + logging.info( + f'[RequestHandler:{self.port}] received request: {request}') + # b'\x1b\x00\xfb\x05\x14mc.tealfire.de\x00FML3\x00c\xa0\x02\x1a\x00\x07DerTyp7\x01\xf2]\x9a\x18*\xeaJ\xed\xbe0g\x9c\x8aT\xa9t' + 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.forward_request_to_placeholder( + request, minecraft_server) + else: + logging.info( + f'[RequestHandler:{self.port}] starting container {service_name}') + self.minecraft_server_handler.start_server( + service_name) + 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) + + 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) + else: + logging.info( + f'[RequestHandler:{self.port}] detected unknown request for {service_name}') + 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)) + + 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/utils.py b/app/utils.py index cadff3a..5209bef 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,20 +1,50 @@ -import logging -import os -import socket - - -def docker_container_mapping(): - port_map_str = os.environ.get('PORT_MAP') - - port_map = {} - for line in port_map_str.split('\n'): - if line: - port, name = line.split(':') - port_map[port.strip()] = name.strip().replace( - "'", "").replace('"', "").strip() - - # print port map for debugging - logging.info('PORT_MAP:') - for port in port_map: - logging.info(f'{port} -> {port_map[port]}') - return port_map +import logging +import os +import json +import json +from typing import List, Dict + + +def docker_container_mapping() -> Dict[str, str]: + port_map_str = os.environ.get('PORT_MAP') + + port_map = {} + for line in port_map_str.split('\n'): + if line: + port, name = line.split(':') + port_map[port.strip()] = name.strip().replace( + "'", "").replace('"', "").strip() + + # print port map for debugging + logging.info('PORT_MAP:') + for port in port_map: + logging.info(f'{port} -> {port_map[port]}') + 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 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) diff --git a/docker-compose.yml b/docker-compose.yml index 37f9086..f7bf50e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,99 +1,46 @@ -version: "3.9" - -services: - auto_starter: - container_name: mc_auto_starter - restart: no - image: dertyp7/minecraft_server_auto_starter:latest - ports: - - 25565:25565 - - 25566:25566 - environment: - # The ip of the placeholder servers below - PLACEHOLDER_SERVER_SLEEPING_SERVICE: "mc_placeholder_server_sleeping" - PLACEHOLDER_SERVER_STARTING_SERVICE: "mc_placeholder_server_starting" - - # Port mapping for the servers - # The key is the external port of the placeholder server - # The value is the internal ip of the actual server - # Don't change the server port in the actual server. Use this instead - PORT_MAP: | - 25565: "mc" - 25566: "mc2" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - # These are the placeholder servers. They are used to show the player a message - # They are not needed, but they are nice to have - # Keep in mind these servers are consuming some resources - mc_placeholder_server_sleeping: - container_name: mc_placeholder_server_sleeping - restart: always - image: itzg/minecraft-server:java8 - environment: - VERSION: "1.12.2" - EULA: "TRUE" - MOTD: "Sleeping | Join to wake up" - - # The placeholder servers should be as lightweight as possible - MAX_PLAYERS: "0" - MAX_MEMORY: "512M" - INIT_MEMORY: "512M" - LEVEL_TYPE: "FLAT" - JVM_XX_OPTS: "-XX:+UseConcMarkSweepGC -XX:+DisableExplicitGC -XX:+UseCompressedOops" - VIEW_DISTANCE: "1" - SPAWN_ANIMALS: "false" - SPAWN_MONSTERS: "false" - SNOOPER_ENABLED: "false" - GENERATE_STRUCTURES: "false" - ALLOW_NETHER: "false" - ALLOW_END: "false" - - mc_placeholder_server_starting: - container_name: mc_placeholder_server_starting - restart: always - image: itzg/minecraft-server:java8 - environment: - VERSION: "1.12.2" - EULA: "TRUE" - MOTD: "Starting, please wait..." - - # The placeholder servers should be as lightweight as possible - MAX_PLAYERS: "0" - MAX_MEMORY: "512M" - INIT_MEMORY: "512M" - LEVEL_TYPE: "FLAT" - JVM_XX_OPTS: "-XX:+UseConcMarkSweepGC -XX:+DisableExplicitGC -XX:+UseCompressedOops" - VIEW_DISTANCE: "1" - SPAWN_ANIMALS: "false" - SPAWN_MONSTERS: "false" - SNOOPER_ENABLED: "false" - GENERATE_STRUCTURES: "false" - ALLOW_NETHER: "false" - ALLOW_END: "false" - - # These are the actual servers - # For itzg/minecraft-server you can find the documentation here: https://docker-minecraft-server.readthedocs.io/en/latest/variables/ - mc: - container_name: example_mc_server_1 - image: itzg/minecraft-server - restart: unless-stopped #! This is important. If you restart the server always automatically, the auto_starter will not work - environment: - type: "PAPER" - EULA: "TRUE" - MOTD: "Example Server 1" - MAX_PLAYERS: "1" - #! Dont change SERVER_PORT. Use PORT_IP_MAP in auto_starter instead. - # SERVER_PORT default is "25565" - - mc2: - container_name: example_mc_server_2 - image: itzg/minecraft-server - restart: unless-stopped #! This is important. If you restart the server always automatically, the auto_starter will not work - environment: - type: "PAPER" - EULA: "TRUE" - MOTD: "Example Server 2" - MAX_PLAYERS: "1" - #! Dont change SERVER_PORT. Use PORT_IP_MAP in auto_starter instead. - # SERVER_PORT default is "25565" +version: "3.9" + +services: + auto_starter: + container_name: mc_auto_starter + restart: no + image: dertyp7/minecraft_server_auto_starter:latest + ports: + - 25565:25565 + - 25566:25566 + environment: + # Port mapping for the servers + # The key is the external port of the placeholder server + # The value is the internal ip of the actual server + # Don't change the server port in the actual server. Use this instead + PORT_MAP: | + 25565: "mc" + 25566: "mc2" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # These are the actual servers + # For itzg/minecraft-server you can find the documentation here: https://docker-minecraft-server.readthedocs.io/en/latest/variables/ + mc: + container_name: example_mc_server_1 + image: itzg/minecraft-server + restart: unless-stopped #! This is important. If you restart the server always automatically, the auto_starter will not work + environment: + type: "PAPER" + EULA: "TRUE" + MOTD: "Example Server 1" + MAX_PLAYERS: "1" + #! Dont change SERVER_PORT. Use PORT_IP_MAP in auto_starter instead. + # SERVER_PORT default is "25565" + + mc2: + container_name: example_mc_server_2 + image: itzg/minecraft-server + restart: unless-stopped #! This is important. If you restart the server always automatically, the auto_starter will not work + environment: + type: "PAPER" + EULA: "TRUE" + MOTD: "Example Server 2" + MAX_PLAYERS: "1" + #! Dont change SERVER_PORT. Use PORT_IP_MAP in auto_starter instead. + # SERVER_PORT default is "25565" diff --git a/nginx-example.conf b/nginx-example.conf index 9633146..3e6690b 100644 --- a/nginx-example.conf +++ b/nginx-example.conf @@ -1,27 +1,27 @@ -worker_processes 5; -events { - worker_connections 1024; - multi_accept on; -} -stream { - upstream upstream_25565 { - server 192.168.144.5:25565; - server 127.0.0.1:25565 backup; - } - server { - listen 192.168.144.6:25565; - proxy_connect_timeout 5s; - proxy_timeout 5s; - proxy_pass upstream_25565; - } - upstream upstream_25566 { - server 192.168.144.3:25565; - server 127.0.0.1:25566 backup; - } - server { - listen 192.168.144.6:25566; - proxy_connect_timeout 5s; - proxy_timeout 5s; - proxy_pass upstream_25566; - } -} +worker_processes 5; +events { + worker_connections 1024; + multi_accept on; +} +stream { + upstream upstream_25565 { + server 192.168.144.5:25565; + server 127.0.0.1:25565 backup; + } + server { + listen 192.168.144.6:25565; + proxy_connect_timeout 5s; + proxy_timeout 5s; + proxy_pass upstream_25565; + } + upstream upstream_25566 { + server 192.168.144.3:25565; + server 127.0.0.1:25566 backup; + } + server { + listen 192.168.144.6:25566; + proxy_connect_timeout 5s; + proxy_timeout 5s; + proxy_pass upstream_25566; + } +} diff --git a/readme.md b/readme.md index 44add89..0edb0d2 100644 --- a/readme.md +++ b/readme.md @@ -1,17 +1,17 @@ -# Minecraft Server Auto Starter for Docker Compose - -This container will manage the access to your Minecraft server. It will start the Minecraft server when a player tries to connect. -This container is designed to work with the [itzg/minecraft-server](https://hub.docker.com/r/itzg/minecraft-server) container. -It uses the AutoStop feature of the [itzg/minecraft-server](https://hub.docker.com/r/itzg/minecraft-server) container to stop the Minecraft server when no player is connected. - -## Usage - -See the [docker-compose.yml](https://github.com/DerTyp7/docker_minecraft_server_auto_starter/blob/main/docker-compose.yml) file for an example. - -## Environment Variables - -| Variable | Description | Default | Example | -| -------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------- | -------------- | -| `PLACEHOLDER_SERVER_SLEEPING_IP` | (optional) The internal docker-compose IP for the placeholder server when a server is sleeping | `""` | `"172.20.0.3"` | -| `PLACEHOLDER_SERVER_STARTING_IP` | (optional) The internal docker-compose IP for the placeholder server when a server is starting | `""` | `"172.20.0.4"` | -| `PORT_IP_MAP` | Map which matches the external Minecraft ports to the internal docker-compose IPs for the Minecraft-Servers | | ![image](https://github.com/DerTyp7/docker_minecraft_server_auto_starter/assets/76851529/4319a42c-7fc4-4be6-8e9d-710475dfde9a)| +# Minecraft Server Auto Starter for Docker Compose + +This container will manage the access to your Minecraft server. It will start the Minecraft server when a player tries to connect. +This container is designed to work with the [itzg/minecraft-server](https://hub.docker.com/r/itzg/minecraft-server) container. +It uses the AutoStop feature of the [itzg/minecraft-server](https://hub.docker.com/r/itzg/minecraft-server) container to stop the Minecraft server when no player is connected. + +## Usage + +See the [docker-compose.yml](https://github.com/DerTyp7/docker_minecraft_server_auto_starter/blob/main/docker-compose.yml) file for an example. + +## Environment Variables + +| Variable | Description | Default | Example | +| -------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------- | -------------- | +| `PLACEHOLDER_SERVER_SLEEPING_IP` | (optional) The internal docker-compose IP for the placeholder server when a server is sleeping | `""` | `"172.20.0.3"` | +| `PLACEHOLDER_SERVER_STARTING_IP` | (optional) The internal docker-compose IP for the placeholder server when a server is starting | `""` | `"172.20.0.4"` | +| `PORT_IP_MAP` | Map which matches the external Minecraft ports to the internal docker-compose IPs for the Minecraft-Servers | | ![image](https://github.com/DerTyp7/docker_minecraft_server_auto_starter/assets/76851529/4319a42c-7fc4-4be6-8e9d-710475dfde9a)|