105 lines
3.8 KiB
Python
105 lines
3.8 KiB
Python
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. /<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 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()
|