major fixes test
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import os
|
||||
import json
|
||||
import socket
|
||||
import posixpath
|
||||
import urllib.parse
|
||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
CONFIG_FILENAME = "config.json"
|
||||
|
||||
@@ -14,17 +15,31 @@ def load_config():
|
||||
|
||||
|
||||
CONFIG = load_config()
|
||||
HOST = CONFIG.get("host", "0.0.0.0")
|
||||
PORT = CONFIG.get("port", 3232)
|
||||
HOST = CONFIG.get("server", {}).get("host", "0.0.0.0")
|
||||
PORT = CONFIG.get("server", {}).get("port", 3232)
|
||||
|
||||
# Maps a URL access key (long hex string, see generate_key.py) -> physical
|
||||
# folder on the server holding that client's files. A random hex key instead
|
||||
# of a plain name like "main" just makes the update path harder to stumble
|
||||
# upon by guessing; it does not stop someone who deliberately extracts the
|
||||
# key from the updater executable. If a key ever leaks, generate a new one
|
||||
# and update it here plus in the updater's CLIENT_KEYS - no need to touch
|
||||
# folder names or anything else.
|
||||
CLIENTS = CONFIG.get("clients", {})
|
||||
# config.json keeps client folders and access keys under two separate
|
||||
# top-level maps, both keyed by the same friendly client name
|
||||
# ("Main", "Test", ...):
|
||||
#
|
||||
# "clients": {"Main": "game_client", "Test": "game_client_test"}
|
||||
# "access_keys": {"Main": "<hex>", "Test": "<hex>"}
|
||||
#
|
||||
# This mirrors how updater.py's CLIENT_KEYS is keyed (by display name),
|
||||
# so quickstart.py can populate both from the exact same source of truth
|
||||
# without the server and the updater disagreeing about what "Main" means.
|
||||
# The server only cares about the resulting access_key -> folder mapping,
|
||||
# built once here at startup.
|
||||
_CLIENT_FOLDERS = CONFIG.get("clients", {})
|
||||
_ACCESS_KEYS = CONFIG.get("access_keys", {})
|
||||
|
||||
CLIENTS = {}
|
||||
for _name, _folder in _CLIENT_FOLDERS.items():
|
||||
_key = _ACCESS_KEYS.get(_name)
|
||||
if not _key:
|
||||
print(f"WARNING: client '{_name}' has no access_key configured in config.json - skipping it.")
|
||||
continue
|
||||
CLIENTS[_key] = _folder
|
||||
|
||||
# Absolute, resolved base directories for each client key, computed once at
|
||||
# startup. Relative folder paths in config.json are resolved relative to
|
||||
@@ -43,6 +58,14 @@ CLIENT_BASE_DIRS = {
|
||||
# from the script's own working directory.
|
||||
NONEXISTENT_PATH = os.path.join(os.path.abspath(os.sep), "__no_such_path__")
|
||||
|
||||
# Simple per-IP concurrent connection cap. This is not a substitute for a
|
||||
# real reverse proxy (nginx/Caddy) doing proper rate limiting, but it stops
|
||||
# a single naive client (or a basic flood) from tying up every worker
|
||||
# thread the process has. Put a real reverse proxy in front for anything
|
||||
# internet-facing - see README.
|
||||
MAX_CONNECTIONS_PER_IP = CONFIG.get("server", {}).get("max_connections_per_ip", 20)
|
||||
_active_connections = {}
|
||||
|
||||
|
||||
class MultiClientHandler(SimpleHTTPRequestHandler):
|
||||
"""
|
||||
@@ -61,6 +84,11 @@ class MultiClientHandler(SimpleHTTPRequestHandler):
|
||||
working directory.
|
||||
"""
|
||||
|
||||
# Keep noisy default logging off stdout in production; flip to True
|
||||
# (or add real logging) if you need request-level debugging.
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
def _resolve_client_path(self, path):
|
||||
path = urllib.parse.urlparse(path).path
|
||||
path = urllib.parse.unquote(path)
|
||||
@@ -102,10 +130,49 @@ class MultiClientHandler(SimpleHTTPRequestHandler):
|
||||
self.send_error(404, "Not Found")
|
||||
return None
|
||||
|
||||
def finish(self):
|
||||
try:
|
||||
super().finish()
|
||||
finally:
|
||||
ip = self.client_address[0]
|
||||
remaining = _active_connections.get(ip, 1) - 1
|
||||
if remaining <= 0:
|
||||
_active_connections.pop(ip, None)
|
||||
else:
|
||||
_active_connections[ip] = remaining
|
||||
|
||||
def handle_one_request(self):
|
||||
ip = self.client_address[0]
|
||||
current = _active_connections.get(ip, 0)
|
||||
if current >= MAX_CONNECTIONS_PER_IP:
|
||||
self.close_connection = True
|
||||
try:
|
||||
self.send_error(429, "Too Many Requests")
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
return
|
||||
_active_connections[ip] = current + 1
|
||||
super().handle_one_request()
|
||||
|
||||
|
||||
class Server(ThreadingHTTPServer):
|
||||
# Handle each connection in its own thread so one slow/large download
|
||||
# doesn't stall every other player's update. daemon_threads=True keeps
|
||||
# Ctrl+C responsive instead of hanging on stray worker threads.
|
||||
daemon_threads = True
|
||||
allow_reuse_address = True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
server_address = (HOST, PORT)
|
||||
httpd = HTTPServer(server_address, MultiClientHandler)
|
||||
httpd = Server(server_address, MultiClientHandler)
|
||||
print(f"Server running on {HOST}:{PORT}...")
|
||||
print(f"Available client keys: {', '.join(CLIENTS.keys()) if CLIENTS else '(none configured)'}")
|
||||
httpd.serve_forever()
|
||||
if CLIENTS:
|
||||
active_names = [name for name in _CLIENT_FOLDERS if _ACCESS_KEYS.get(name)]
|
||||
print(f"Configured clients: {', '.join(active_names)}")
|
||||
else:
|
||||
print("WARNING: no clients configured (check config.json 'clients' and 'access_keys').")
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user