diff --git a/pkgs/clan-cli/clan_cli/webui/app.py b/pkgs/clan-cli/clan_cli/webui/app.py index daf415861..fbc662be5 100644 --- a/pkgs/clan-cli/clan_cli/webui/app.py +++ b/pkgs/clan-cli/clan_cli/webui/app.py @@ -5,6 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.routing import APIRoute from fastapi.staticfiles import StaticFiles + from .assets import asset_path from .routers import flake, health, machines, root, utils, vms @@ -35,7 +36,7 @@ def setup_app() -> FastAPI: app.add_exception_handler( utils.NixBuildException, utils.nix_build_exception_handler ) - + app.mount("/static", StaticFiles(directory=asset_path()), name="static") for route in app.routes: diff --git a/pkgs/clan-cli/clan_cli/webui/assets.py b/pkgs/clan-cli/clan_cli/webui/assets.py index b6a027c4b..4c7e1fa1e 100644 --- a/pkgs/clan-cli/clan_cli/webui/assets.py +++ b/pkgs/clan-cli/clan_cli/webui/assets.py @@ -1,7 +1,39 @@ import functools from pathlib import Path +import logging + +log = logging.getLogger(__name__) + +def get_hash(string: str) -> str: + """ + This function takes a string like '/nix/store/kkvk20b8zh8aafdnfjp6dnf062x19732-source' + and returns the hash part 'kkvk20b8zh8aafdnfjp6dnf062x19732' after '/nix/store/' and before '-source'. + """ + # Split the string by '/' and get the last element + last_element = string.split('/')[-1] + # Split the last element by '-' and get the first element + hash_part = last_element.split('-')[0] + # Return the hash part + return hash_part + + +def check_divergence(path: Path) -> None: + p = path.resolve() + + log.info("Absolute web asset path: %s", p) + if not p.is_dir(): + raise FileNotFoundError(p) + + # Get the hash part of the path + gh = get_hash(str(p)) + + log.debug(f"Serving webui asset with hash {gh}") + @functools.cache def asset_path() -> Path: - return Path(__file__).parent / "assets" + path = Path(__file__).parent / "assets" + log.debug("Serving assets from: %s", path) + check_divergence(path) + return path \ No newline at end of file diff --git a/pkgs/clan-cli/clan_cli/webui/routers/root.py b/pkgs/clan-cli/clan_cli/webui/routers/root.py index e8121d07c..67bc05cbb 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/root.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/root.py @@ -1,13 +1,15 @@ import os from mimetypes import guess_type from pathlib import Path - +import logging from fastapi import APIRouter, Response from ..assets import asset_path router = APIRouter() +log = logging.getLogger(__name__) + @router.get("/{path_name:path}") async def root(path_name: str) -> Response: @@ -16,6 +18,7 @@ async def root(path_name: str) -> Response: filename = Path(os.path.normpath(asset_path() / path_name)) if not filename.is_relative_to(asset_path()): + log.error("Prevented directory traversal: %s", filename) # prevent directory traversal return Response(status_code=403) @@ -23,6 +26,7 @@ async def root(path_name: str) -> Response: if filename.suffix == "": filename = filename.with_suffix(".html") if not filename.is_file(): + log.error("File not found: %s", filename) return Response(status_code=404) content_type, _ = guess_type(filename)