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>
96 lines
2.9 KiB
Python
96 lines
2.9 KiB
Python
"""Low-level Docker socket client."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import socket
|
|
from pathlib import Path
|
|
|
|
|
|
def _decode_chunked_bytes(data: bytes) -> bytes:
|
|
result = bytearray()
|
|
pos = 0
|
|
while pos < len(data):
|
|
end = data.find(b"\r\n", pos)
|
|
if end < 0:
|
|
break
|
|
try:
|
|
size = int(data[pos:end].split(b";")[0].strip(), 16)
|
|
except ValueError:
|
|
break
|
|
if size == 0:
|
|
break
|
|
pos = end + 2
|
|
result.extend(data[pos:pos + size])
|
|
pos += size + 2
|
|
return bytes(result)
|
|
|
|
|
|
def _docker_get_json(path: str) -> tuple[int, dict | list | None]:
|
|
sock_path = os.environ.get("DOCKER_SOCKET", "/var/run/docker.sock")
|
|
if not Path(sock_path).exists():
|
|
raise RuntimeError(f"Docker socket not found: {sock_path}")
|
|
request = (
|
|
f"GET {path} HTTP/1.1\r\n"
|
|
"Host: docker\r\n"
|
|
"Connection: close\r\n\r\n"
|
|
).encode("utf-8")
|
|
chunks: list[bytes] = []
|
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
|
sock.settimeout(10)
|
|
sock.connect(sock_path)
|
|
sock.sendall(request)
|
|
while True:
|
|
data = sock.recv(65536)
|
|
if not data:
|
|
break
|
|
chunks.append(data)
|
|
raw = b"".join(chunks)
|
|
header_end = raw.find(b"\r\n\r\n")
|
|
if header_end < 0:
|
|
raise RuntimeError("Invalid Docker HTTP response")
|
|
header_str = raw[:header_end].decode("utf-8", errors="replace")
|
|
body_bytes = raw[header_end + 4:]
|
|
status_line = header_str.splitlines()[0]
|
|
m = re.match(r"HTTP/\S+\s+(\d+)", status_line)
|
|
if not m:
|
|
raise RuntimeError(f"Invalid Docker status line: {status_line!r}")
|
|
code = int(m.group(1))
|
|
if "transfer-encoding: chunked" in header_str.lower():
|
|
body_bytes = _decode_chunked_bytes(body_bytes)
|
|
if not body_bytes.strip():
|
|
return code, None
|
|
return code, json.loads(body_bytes)
|
|
|
|
|
|
def _docker_post(path: str) -> tuple[int, str]:
|
|
sock_path = os.environ.get("DOCKER_SOCKET", "/var/run/docker.sock")
|
|
if not Path(sock_path).exists():
|
|
raise RuntimeError(f"Docker socket not found: {sock_path}")
|
|
|
|
request = (
|
|
f"POST {path} HTTP/1.1\r\n"
|
|
"Host: docker\r\n"
|
|
"Content-Length: 0\r\n"
|
|
"Connection: close\r\n\r\n"
|
|
).encode("utf-8")
|
|
|
|
chunks = []
|
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
|
sock.settimeout(20)
|
|
sock.connect(sock_path)
|
|
sock.sendall(request)
|
|
while True:
|
|
data = sock.recv(8192)
|
|
if not data:
|
|
break
|
|
chunks.append(data)
|
|
|
|
raw = b"".join(chunks).decode("utf-8", errors="replace")
|
|
status_line = raw.splitlines()[0] if raw else ""
|
|
match = re.match(r"HTTP/\S+\s+(\d+)", status_line)
|
|
if not match:
|
|
raise RuntimeError("Invalid Docker API response")
|
|
return int(match.group(1)), raw
|