"""Opt-in WebUI extension hooks. This module intentionally provides a small, self-hosted extension surface: configured same-origin script/style injection plus sandboxed static file serving. It is disabled by default and never executes or fetches third-party URLs. """ import html import logging import os from pathlib import Path from typing import Dict, List, Optional from urllib.parse import unquote, urlsplit from api.helpers import _security_headers, j _log = logging.getLogger(__name__) # Sane bound on configured URLs — real extensions ship 1-3 files. Higher values # typically indicate a misconfiguration (one giant unsplit string, or a runaway # generator script that wrote an env-var template without filtering). Capping # avoids rendering tens of thousands of '.format(html.escape(url, quote=True)) for url in config["script_urls"] ] if stylesheet_tags: head_marker = "" block = "\n".join(stylesheet_tags) + "\n" if head_marker in result: result = result.replace(head_marker, block + head_marker, 1) else: result = block + result if script_tags: body_marker = "" block = "\n".join(script_tags) + "\n" if body_marker in result: result = result.replace(body_marker, block + body_marker, 1) else: result = result + "\n" + block return result def _is_safe_relative_path(rel: str) -> bool: if not rel or "\x00" in rel or "\\" in rel: return False for segment in rel.split("/"): if not segment or segment in (".", "..") or segment.startswith("."): return False return True def _not_found(handler) -> bool: j(handler, {"error": "not found"}, status=404) return True def serve_extension_static(handler, parsed) -> bool: """Serve a file from the configured extension directory. The function always returns True for /extensions/* requests: either a file response or a 404. It never reveals why a request failed, which avoids leaking local paths or extension configuration details. """ root = _extension_root() if root is None: return _not_found(handler) rel = unquote(parsed.path[len(EXTENSION_ROUTE_PREFIX) :]) if not _is_safe_relative_path(rel): return _not_found(handler) static_file = (root / rel).resolve() try: static_file.relative_to(root) except ValueError: return _not_found(handler) if not static_file.exists() or not static_file.is_file(): return _not_found(handler) ct = _EXTENSION_MIME.get(static_file.suffix.lower().lstrip("."), "text/plain") ct_header = "{}; charset=utf-8".format(ct) if ct in _TEXT_MIME_TYPES else ct try: raw = static_file.read_bytes() except OSError: return _not_found(handler) handler.send_response(200) handler.send_header("Content-Type", ct_header) handler.send_header("Cache-Control", "no-store") handler.send_header("Content-Length", str(len(raw))) _security_headers(handler) handler.end_headers() handler.wfile.write(raw) return True