Security fixes: - Block /proc /sys /dev /run /boot in /api/browse-dirs (path traversal) - Verify yt-dlp output stays inside TEMP_DIR before registration - Remove Access-Control-Allow-Origin: * from /api/proxy-audio - TTL-based temp file registry (default 2h) to prevent disk fill Performance: - Cache settings + routing rules in memory (mtime-checked); eliminates per-request disk reads on every TTS call UI: - Add container name (optional) field to Docker stack TTS/STT engine cards (Qwen3 Voice Clone, Voice Design, Custom Voice, Streaming, NVIDIA Magpie, Parakeet) — enables Stop/Start/Restart buttons on all engine cards, matching the existing Other Local TTS/STT cards Refactor — backend: - server.py: 5560 lines → 43-line entry point - core/ package: constants, registry, validation, docker_client, config, routing, audio, voice, presets, tts_helpers - routes/ package: admin, settings, library, stt, sources, docker, tts, conversation (FastAPI APIRouter modules) - Dockerfile + docker-compose.yml updated to include core/ and routes/ Refactor — frontend: - static/app.js: 8744 lines → 16 modules in static/js/ utils, voice-inspector, voice-sources, integrations, routing, settings, voice-clone, voice-library, tts-preview, benchmark, stt, init, engines, ai-backends, generation, conversation - static/loader.js updated to load modules sequentially Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
53 lines
1.4 KiB
Python
53 lines
1.4 KiB
Python
"""Admin routes: index, favicon, browse-dirs, robots."""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.responses import FileResponse, PlainTextResponse, Response
|
|
|
|
from core.constants import STATIC_DIR
|
|
|
|
router = APIRouter()
|
|
|
|
_BROWSE_BLOCKED: frozenset[str] = frozenset({
|
|
"/proc", "/sys", "/dev", "/run", "/boot",
|
|
})
|
|
|
|
|
|
@router.get("/")
|
|
async def index():
|
|
return FileResponse(
|
|
STATIC_DIR / "index.html",
|
|
headers={"Cache-Control": "no-store, max-age=0"},
|
|
)
|
|
|
|
|
|
@router.get("/favicon.ico")
|
|
async def favicon():
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.get("/api/browse-dirs")
|
|
async def browse_dirs(path: str = "/"):
|
|
p = Path(path).resolve()
|
|
p_str = str(p)
|
|
if any(p_str == b or p_str.startswith(b + "/") for b in _BROWSE_BLOCKED):
|
|
raise HTTPException(403, "Access to this path is not permitted")
|
|
if not p.is_dir():
|
|
raise HTTPException(404, "Not a directory")
|
|
try:
|
|
entries = sorted(
|
|
[d.name for d in p.iterdir() if d.is_dir() and not d.name.startswith(".")],
|
|
key=str.lower,
|
|
)
|
|
except PermissionError:
|
|
raise HTTPException(403, "Permission denied")
|
|
parent = str(p.parent) if p.parent != p else None
|
|
return {"path": str(p), "parent": parent, "dirs": entries}
|
|
|
|
|
|
@router.get("/robots.txt", response_class=PlainTextResponse)
|
|
async def robots_txt():
|
|
return "User-agent: *\nDisallow: /"
|