This commit is contained in:
2026-09-08 15:49:48 +03:00
parent d4db87186a
commit 02435cb5a6
3 changed files with 149 additions and 24 deletions
+66 -9
View File
@@ -18,25 +18,82 @@ 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. Using a random hex key
# instead of a plain name like "main" just makes the files harder to stumble
# 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):
def translate_path(self, path):
"""
Serves files only under a URL path that starts with a known access key,
e.g. /<hex-key>/manifest.json or /<hex-key>/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 len(parts) >= 1 and parts[0] in CLIENTS:
client_key = parts[0]
base_dir = CLIENTS[client_key]
rel_path = posixpath.join(*parts[1:]) if len(parts) > 1 else ""
return os.path.join(base_dir, rel_path)
return super().translate_path(path)
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__':