fixes
This commit is contained in:
@@ -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__':
|
||||
|
||||
Reference in New Issue
Block a user