From 354c9aea4c89fcd4aae604601220ab95fb31c467 Mon Sep 17 00:00:00 2001 From: Lexipherous Date: Wed, 30 Aug 2023 22:54:37 +0100 Subject: [PATCH 1/5] Added ability to define custom port ranges the WebHost will use for game servers, instead of pure random. --- WebHostLib/__init__.py | 1 + WebHostLib/autolauncher.py | 3 +- WebHostLib/customserver.py | 42 ++++++++++++++++++++++---- docs/webhost configuration sample.yaml | 4 +++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index a59e3aa553..16f62f69ae 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -29,6 +29,7 @@ app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encr app.config["SELFGEN"] = True # application process is in charge of scheduling Generations. app.config["DEBUG"] = False app.config["PORT"] = 80 +app.config["GAME_PORTS"] = "49152-65535" app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit # if you want to deploy, make sure you have a non-guessable secret key diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 0475a63297..8821272db5 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -180,6 +180,7 @@ class MultiworldInstance(): self.cert = config["SELFLAUNCHCERT"] self.key = config["SELFLAUNCHKEY"] self.host = config["HOST_ADDRESS"] + self.game_ports = config["GAME_PORTS"] def start(self): if self.process and self.process.is_alive(): @@ -188,7 +189,7 @@ class MultiworldInstance(): logging.info(f"Spinning up {self.room_id}") process = multiprocessing.Process(group=None, target=run_server_process, args=(self.room_id, self.ponyconfig, get_static_server_data(), - self.cert, self.key, self.host), + self.cert, self.key, self.host, self.game_ports), name="MultiHost") process.start() # bind after start to prevent thread sync issues with guardian. diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 8fbf692dec..cf153069cc 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -19,6 +19,8 @@ import Utils from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert from Utils import restricted_loads, cache_argsless + +from . import app from .models import Command, GameDataPackage, Room, db @@ -84,13 +86,13 @@ class WebHostContext(Context): time.sleep(5) @db_session - def load(self, room_id: int): + def load(self, room_id: int, game_ports: str): self.room_id = room_id room = Room.get(id=room_id) if room.last_port: self.port = room.last_port else: - self.port = get_random_port() + self.port = get_random_port(game_ports) multidata = self.decompress(room.seed.multidata) game_data_packages = {} @@ -133,8 +135,36 @@ class WebHostContext(Context): return d -def get_random_port(): - return random.randint(49152, 65535) +def get_random_port(game_ports): + config_range_list = game_ports.split(",") + available_ports = [] + for item in config_range_list: + if '-' in item: + start, end = map(int, item.split('-')) + available_ports.extend(range(start, end+1)) + else: + available_ports.append(int(item)) + + port = get_port_from_list(available_ports) + + return port + + +def get_port_from_list(available_ports: list) -> int: + while available_ports: + port = random.choice(available_ports) + available_ports.remove(port) + + if not is_port_in_use(port): + break + else: + port = 0 + return port + + +def is_port_in_use(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(('localhost', port)) == 0 @cache_argsless @@ -157,7 +187,7 @@ def get_static_server_data() -> dict: def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, cert_file: typing.Optional[str], cert_key_file: typing.Optional[str], - host: str): + host: str, game_ports: str): # establish DB connection for multidata and multisave db.bind(**ponyconfig) db.generate_mapping(check_tables=False) @@ -165,7 +195,7 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, async def main(): Utils.init_logging(str(room_id), write_mode="a") ctx = WebHostContext(static_server_data) - ctx.load(room_id) + ctx.load(room_id, game_ports) ctx.init_save() ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None try: diff --git a/docs/webhost configuration sample.yaml b/docs/webhost configuration sample.yaml index 70050b0590..70e03ec112 100644 --- a/docs/webhost configuration sample.yaml +++ b/docs/webhost configuration sample.yaml @@ -17,6 +17,10 @@ # Web hosting port #PORT: 80 +# Ports used for game hosting. Values can be specific ports, port ranges or both. Default is: "49152-65535" +# Examples of valid values: "40000-41000,49152-65535" +#GAME_PORTS: "49152-65535" + # Place where uploads go. #UPLOAD_FOLDER: uploads From 392a45ec89c8cfef0e4b263203f49f8bb21a318c Mon Sep 17 00:00:00 2001 From: Lexipherous Date: Sat, 9 Sep 2023 14:40:52 +0100 Subject: [PATCH 2/5] - Added better fallback to default port range when a custom range fails - Updated config to be clearer --- WebHostLib/customserver.py | 9 +++++++++ docs/webhost configuration sample.yaml | 1 + 2 files changed, 10 insertions(+) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index cf153069cc..1fe0fc4bf6 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -146,6 +146,15 @@ def get_random_port(game_ports): available_ports.append(int(item)) port = get_port_from_list(available_ports) + if port == 0: + logging.info("Unable to find port. Expanding search to the default ports. (49152-65535)") + checked_ports = [] + while len(set(checked_ports)) < (65535-49152)+1: + port = random.randint(49152, 65535) + if not is_port_in_use(port): + break + else: + checked_ports.append(port) return port diff --git a/docs/webhost configuration sample.yaml b/docs/webhost configuration sample.yaml index 70e03ec112..2a78e18997 100644 --- a/docs/webhost configuration sample.yaml +++ b/docs/webhost configuration sample.yaml @@ -19,6 +19,7 @@ # Ports used for game hosting. Values can be specific ports, port ranges or both. Default is: "49152-65535" # Examples of valid values: "40000-41000,49152-65535" +# If ports within the range(s) are already in use, the WebHost will fallback to the default "49152-65535" range. #GAME_PORTS: "49152-65535" # Place where uploads go. From b326045cb72b1b0ccd6fb199ea66761dc99bacf4 Mon Sep 17 00:00:00 2001 From: Lexipherous Date: Wed, 30 Aug 2023 22:54:37 +0100 Subject: [PATCH 3/5] Added ability to define custom port ranges the WebHost will use for game servers, instead of pure random. --- WebHostLib/__init__.py | 1 + WebHostLib/autolauncher.py | 3 +- WebHostLib/customserver.py | 42 ++++++++++++++++++++++---- docs/webhost configuration sample.yaml | 4 +++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 441f3272fd..49d57c9d9d 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -29,6 +29,7 @@ app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encr app.config["SELFGEN"] = True # application process is in charge of scheduling Generations. app.config["DEBUG"] = False app.config["PORT"] = 80 +app.config["GAME_PORTS"] = "49152-65535" app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit # if you want to deploy, make sure you have a non-guessable secret key diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 0475a63297..8821272db5 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -180,6 +180,7 @@ class MultiworldInstance(): self.cert = config["SELFLAUNCHCERT"] self.key = config["SELFLAUNCHKEY"] self.host = config["HOST_ADDRESS"] + self.game_ports = config["GAME_PORTS"] def start(self): if self.process and self.process.is_alive(): @@ -188,7 +189,7 @@ class MultiworldInstance(): logging.info(f"Spinning up {self.room_id}") process = multiprocessing.Process(group=None, target=run_server_process, args=(self.room_id, self.ponyconfig, get_static_server_data(), - self.cert, self.key, self.host), + self.cert, self.key, self.host, self.game_ports), name="MultiHost") process.start() # bind after start to prevent thread sync issues with guardian. diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 8fbf692dec..cf153069cc 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -19,6 +19,8 @@ import Utils from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert from Utils import restricted_loads, cache_argsless + +from . import app from .models import Command, GameDataPackage, Room, db @@ -84,13 +86,13 @@ class WebHostContext(Context): time.sleep(5) @db_session - def load(self, room_id: int): + def load(self, room_id: int, game_ports: str): self.room_id = room_id room = Room.get(id=room_id) if room.last_port: self.port = room.last_port else: - self.port = get_random_port() + self.port = get_random_port(game_ports) multidata = self.decompress(room.seed.multidata) game_data_packages = {} @@ -133,8 +135,36 @@ class WebHostContext(Context): return d -def get_random_port(): - return random.randint(49152, 65535) +def get_random_port(game_ports): + config_range_list = game_ports.split(",") + available_ports = [] + for item in config_range_list: + if '-' in item: + start, end = map(int, item.split('-')) + available_ports.extend(range(start, end+1)) + else: + available_ports.append(int(item)) + + port = get_port_from_list(available_ports) + + return port + + +def get_port_from_list(available_ports: list) -> int: + while available_ports: + port = random.choice(available_ports) + available_ports.remove(port) + + if not is_port_in_use(port): + break + else: + port = 0 + return port + + +def is_port_in_use(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(('localhost', port)) == 0 @cache_argsless @@ -157,7 +187,7 @@ def get_static_server_data() -> dict: def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, cert_file: typing.Optional[str], cert_key_file: typing.Optional[str], - host: str): + host: str, game_ports: str): # establish DB connection for multidata and multisave db.bind(**ponyconfig) db.generate_mapping(check_tables=False) @@ -165,7 +195,7 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, async def main(): Utils.init_logging(str(room_id), write_mode="a") ctx = WebHostContext(static_server_data) - ctx.load(room_id) + ctx.load(room_id, game_ports) ctx.init_save() ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None try: diff --git a/docs/webhost configuration sample.yaml b/docs/webhost configuration sample.yaml index 70050b0590..70e03ec112 100644 --- a/docs/webhost configuration sample.yaml +++ b/docs/webhost configuration sample.yaml @@ -17,6 +17,10 @@ # Web hosting port #PORT: 80 +# Ports used for game hosting. Values can be specific ports, port ranges or both. Default is: "49152-65535" +# Examples of valid values: "40000-41000,49152-65535" +#GAME_PORTS: "49152-65535" + # Place where uploads go. #UPLOAD_FOLDER: uploads From 1815645994ca353234a2c86655f8a8cbd7a8f8a5 Mon Sep 17 00:00:00 2001 From: Lexipherous Date: Sat, 9 Sep 2023 14:40:52 +0100 Subject: [PATCH 4/5] - Added better fallback to default port range when a custom range fails - Updated config to be clearer --- WebHostLib/customserver.py | 9 +++++++++ docs/webhost configuration sample.yaml | 1 + 2 files changed, 10 insertions(+) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index cf153069cc..1fe0fc4bf6 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -146,6 +146,15 @@ def get_random_port(game_ports): available_ports.append(int(item)) port = get_port_from_list(available_ports) + if port == 0: + logging.info("Unable to find port. Expanding search to the default ports. (49152-65535)") + checked_ports = [] + while len(set(checked_ports)) < (65535-49152)+1: + port = random.randint(49152, 65535) + if not is_port_in_use(port): + break + else: + checked_ports.append(port) return port diff --git a/docs/webhost configuration sample.yaml b/docs/webhost configuration sample.yaml index 70e03ec112..2a78e18997 100644 --- a/docs/webhost configuration sample.yaml +++ b/docs/webhost configuration sample.yaml @@ -19,6 +19,7 @@ # Ports used for game hosting. Values can be specific ports, port ranges or both. Default is: "49152-65535" # Examples of valid values: "40000-41000,49152-65535" +# If ports within the range(s) are already in use, the WebHost will fallback to the default "49152-65535" range. #GAME_PORTS: "49152-65535" # Place where uploads go. From 8f4e4cf6b2cc672beae7321d1c202365d33f6e61 Mon Sep 17 00:00:00 2001 From: Lexipherous Date: Sun, 4 Feb 2024 18:47:08 +0000 Subject: [PATCH 5/5] Updated soft-fail message --- WebHostLib/customserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 9048d51ff7..ba971b1683 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -151,7 +151,6 @@ def get_random_port(game_ports): port = get_port_from_list(available_ports) if port == 0: - logging.info("Unable to find port. Expanding search to the default ports. (49152-65535)") checked_ports = [] while len(set(checked_ports)) < (65535-49152)+1: port = random.randint(49152, 65535) @@ -159,6 +158,7 @@ def get_random_port(game_ports): break else: checked_ports.append(port) + logging.info(f"Unable to find an available port in custom range. Expanded search to the default ports. Hosting on port {port}.") return port