"""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 json import math import logging import os import re import threading from pathlib import Path from typing import Any, Dict, List, Optional, Set, Tuple from urllib.parse import unquote, urlsplit import hashlib import io import time import zipfile from urllib.request import HTTPRedirectHandler, build_opener, urlopen 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"] ] runtime_config = { "extensions": config.get("extensions", []), } runtime_json = json.dumps(runtime_config, ensure_ascii=False, separators=(",", ":")).replace("<", "\\u003c") runtime_tag = ( "" ).format(runtime_json) 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 runtime_config["extensions"] or script_tags: body_marker = "" block = runtime_tag + "\n" if script_tags: 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