from pathlib import Path from src.config.config import Configuration from src.rendering.renderer import render_page, render_error_page from flask import send_file, request from src.rendering.image import generate_thumbnail import os class RouteManager: """ RouteManager is responsible for handling and validating file system paths for serving content, styles, and static files in a web application. It ensures that all requested paths are securely resolved within configured base directories, prevents path traversal attacks, and restricts access to hidden files or folders. Args: config (Configuration): The configuration object containing directory paths for content, templates, and styles. Methods: _validate_and_sanitize_path(base_dir, requested_path_str): Validates and sanitizes a requested path to ensure it is within the specified base directory and not a hidden file/folder. Returns a resolved Path object or None if invalid. _ensure_route(path): Ensures the given path is valid and returns the corresponding Path object. Raises an Exception if the path is illegal. default_route(path): Handles the default route for serving content files. Returns a rendered page or an error page if the path is invalid or not found. get_style(path): Serves style files from the styles directory. Returns the file or an error page if the path is invalid or not found. get_static(path): Serves static files from the content directory. If the file is an image, generates and returns a thumbnail. Returns the file or an error page if the path is invalid or not found. """ def __init__(self, config: Configuration): self.config = config def _validate_and_sanitize_path(self, base_dir, requested_path_str: str): """ Validates and sanitizes a requested file system path to ensure it is safe and allowed. This method resolves the requested path relative to a given base directory, ensuring: - The resolved path exists. - The resolved path is within the base directory (prevents directory traversal attacks). - The path does not access hidden files or directories (those starting with '___'). Args: base_dir (str or Path): The base directory against which the requested path is resolved. requested_path_str (str): The user-supplied path to validate and sanitize. Returns: Path or None: The resolved and validated Path object if the path is safe and allowed; otherwise, None if the path is invalid, does not exist, attempts traversal, or accesses hidden files/directories. """ try: base_dir = Path(base_dir).resolve(strict=True) # a requested path of "" or "." should resolve to the base directory if not requested_path_str: requested_path_str = "." secure_path = (base_dir / requested_path_str).resolve(strict=True) except FileNotFoundError: return None # Path does not exist # The most important check: ensure the resolved path is inside the base directory. if not secure_path.is_relative_to(base_dir): print(f"Illegal path traversal attempt: {requested_path_str}") return None # Check for hidden files/folders (starting with '___') relative_parts = secure_path.relative_to(base_dir).parts # Also check the final component for the case where path is the base_dir itself. if any( part.startswith("___") for part in relative_parts ) or secure_path.name.startswith("___"): print(f"Illegal access to hidden path: {requested_path_str}") return None return secure_path def _ensure_route(self, path: str): file_path = self._validate_and_sanitize_path(self.config.content_dir, path) if not file_path: raise Exception("Illegal path") return file_path def default_route(self, path: str): """ Handles the default route for serving content pages. Attempts to resolve the given path to a file within the content directory. If the path is empty, defaults to "index.md". If the file is not found or an error occurs, renders a 404 error page. Otherwise, renders the requested page using the specified template and style directories. Args: path (str): The requested path to resolve and serve. Returns: Response: The rendered page or an error page if the file is not found. """ try: file_path = self._ensure_route(path if path else "index.md") except Exception as _: return render_error_page( 404, "Not Found", "The requested resource was not found on this server.", self.config.templates_dir, ) return render_page( file_path, base_path=self.config.content_dir, template_path=self.config.templates_dir, style_path=self.config.styles_dir, ) def get_style(self, path: str): """ Retrieves and serves a style file from the configured styles directory. Args: path (str): The relative path to the requested style file. Returns: Response: A Flask response object containing the requested file if found, or an error page with a 404 status code if the file does not exist. """ file_path = self._validate_and_sanitize_path(self.config.styles_dir, path) if not file_path: return render_error_page( 404, "Not Found", "The requested resource was not found on this server.", self.config.templates_dir, ) return send_file(file_path) def get_static(self, path: str): """ Serves static files from the configured content directory. If the requested file is an image (JPEG, PNG, or GIF), generates and returns a thumbnail with a maximum width specified by the 'max_width' query parameter (default: 2048). Otherwise, serves the file as-is. Args: path (str): The relative path to the requested static file. Returns: Response: - If the file is not found or invalid, returns a rendered 404 error page. - If the file is an image, returns the thumbnail bytes with appropriate headers. - Otherwise, returns the file using Flask's send_file. """ file_path = self._validate_and_sanitize_path(self.config.content_dir, path) if not file_path: return render_error_page( 404, "Not Found", "The requested resource was not found on this server.", self.config.templates_dir, ) # Check to see if the file is an image, if it is, render a thumbnail if file_path.suffix.lower() in [".jpg", ".jpeg", ".png", ".gif"]: max_width = request.args.get("max_width", default=2048, type=int) thumbnail_bytes, img_format = generate_thumbnail( str(file_path), 10, 2048, max_width ) return ( thumbnail_bytes, 200, { "Content-Type": f"image/{img_format.lower()}", "cache-control": "public, max-age=31536000", }, ) return send_file(file_path)