All checks were successful
Datadog Software Composition Analysis / Datadog SBOM Generation and Upload (push) Successful in 50s
Datadog Secrets Scanning / Datadog Static Analyzer (push) Successful in 56s
Release / build (push) Successful in 1m23s
Release / publish_head (push) Successful in 1m17s
Datadog Static Analysis / Datadog Static Analyzer (push) Successful in 5m41s
178 lines
7.6 KiB
Python
178 lines
7.6 KiB
Python
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)
|