tts-voice-creator-clone-and.../core/docker_client.py
mARTin-B78 c7a1e35539 Security audit, modular refactor, and container-name field
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>
2026-05-29 12:13:07 +02:00

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