import os import json import posixpath import urllib.parse from http.server import SimpleHTTPRequestHandler, HTTPServer CONFIG_FILENAME = "config.json" def load_config(): config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), CONFIG_FILENAME) with open(config_path, "r", encoding="utf-8") as f: return json.load(f) CONFIG = load_config() HOST = CONFIG.get("host", "0.0.0.0") PORT = CONFIG.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", {}) # Absolute, resolved base directories for each client key, computed once at # startup. Every request is checked against these to make sure the final # path can never land outside the intended client folder. CLIENT_BASE_DIRS = {key: os.path.realpath(folder) for key, folder in CLIENTS.items()} # A path that is guaranteed not to exist, used to force a 404 through the # normal http.server flow instead of ever falling back to serving files # from the script's own working directory. NONEXISTENT_PATH = os.path.join(os.path.abspath(os.sep), "__no_such_path__") class MultiClientHandler(SimpleHTTPRequestHandler): """ Serves files only under a URL path that starts with a known access key, e.g. //manifest.json or //some/file. Everything else gets a 404: - a bare "/" (the directory listing that exposes server.py, config.json, the raw client folders, etc.) - an unknown or missing key - any path that tries to escape the client folder (e.g. via "..") - directory listings even *inside* a valid client folder This is enforced by never letting execution fall through to the default SimpleHTTPRequestHandler behavior, which serves the process's current working directory. """ def _resolve_client_path(self, path): path = urllib.parse.urlparse(path).path path = urllib.parse.unquote(path) parts = [p for p in path.split('/') if p] if not parts: return None key = parts[0] if key not in CLIENT_BASE_DIRS: return None base_dir = CLIENT_BASE_DIRS[key] rel_parts = parts[1:] rel_path = posixpath.join(*rel_parts) if rel_parts else "" full_path = os.path.realpath(os.path.join(base_dir, rel_path)) # Confirm the resolved path is still inside base_dir (blocks "..", # symlink tricks, etc.). commonpath needs both paths to exist on the # same drive/root, which realpath + os.path.join guarantees here. try: if os.path.commonpath([full_path, base_dir]) != base_dir: return None except ValueError: return None return full_path def translate_path(self, path): resolved = self._resolve_client_path(path) if resolved is None: return NONEXISTENT_PATH return resolved def list_directory(self, path): # Never render a directory listing, even for a path inside a valid # client folder - only exact file paths are servable. self.send_error(404, "Not Found") return None if __name__ == '__main__': server_address = (HOST, PORT) httpd = HTTPServer(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()