47f7a85510
Die Ampel-Nachruestung deckte 317/337 vorbestehende ruff-Verstoesse im ganzen Repo auf. Aufgeraeumt: - ruff.toml: intentionale Muster als Projekt-Politik ausgenommen (BLE001 blind-except, S110/S112 try-except-pass/continue, PLW1510 subprocess-best-effort, B008 FastAPI- Depends/File-Idiom, EXE001 Shebang, + wenige Stil-Regeln). __init__.py-Re-Exports geschuetzt (F401). - ruff --fix: 128 mechanische (Import-Sortierung, PEP585/604-Annotationen, tote Imports, ueberfluessige noqa) auto-behoben. - 12 echte Reste von Hand: PERF402/102, PLC3002 (Lambda->walrus), ISC004 (String-Concat geklammert), F841/RUF059 (ungenutzte Vars), PIE810 (startswith-Tuple), UP031 (f-string), UP035 (veraltete typing-Imports). Ergebnis: 'ruff check .' = 0, 'compileall' grün. Kein Verhaltenswechsel (nur Stil/Modernisierung). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
3.7 KiB
Python
88 lines
3.7 KiB
Python
"""
|
|
ttyd-Reverse-Proxy für die eingebetteten Web-Terminals (Box-Konsole + Hermes-Terminal).
|
|
|
|
Beide ttyd-Dienste binden NUR an Loopback (--base-path /<name>) und werden hier über
|
|
den ohnehin offenen MC2-Port (9001) same-origin durchgereicht — HTTP (index.html/token)
|
|
+ WebSocket (/<name>/ws). Vorteil: keine eigene Firewall-Freigabe je Terminal nötig und
|
|
alles läuft same-origin zum Dashboard. Reiner Byte-Durchreicher; ttyds `tty`-Subprotokoll
|
|
wird auf beiden Seiten ausgehandelt. Der Login-Schutz sitzt IM Terminal (ttyd startet die
|
|
Shell über `su` → fragt das Box-Passwort ab), nicht im Proxy.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
import httpx
|
|
import websockets
|
|
from config import BOX_CONSOLE_UPSTREAM
|
|
from fastapi import APIRouter, Request, WebSocket
|
|
from starlette.responses import Response
|
|
|
|
log = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
|
|
def _register(base: str, upstream: str) -> None:
|
|
"""Registriert HTTP- + WS-Proxy-Routen für eine ttyd-Instanz (base-path `/<base>`)."""
|
|
ws_upstream = upstream.replace("http://", "ws://").replace("https://", "wss://")
|
|
|
|
@router.websocket(f"/{base}/ws")
|
|
async def _proxy_ws(ws: WebSocket) -> None:
|
|
await ws.accept(subprotocol="tty")
|
|
try:
|
|
async with websockets.connect(
|
|
f"{ws_upstream}/{base}/ws", subprotocols=["tty"],
|
|
open_timeout=10, max_size=None, ping_interval=None,
|
|
) as up:
|
|
async def c2u() -> None:
|
|
while True:
|
|
msg = await ws.receive()
|
|
if msg.get("type") == "websocket.disconnect":
|
|
return
|
|
if (t := msg.get("text")) is not None:
|
|
await up.send(t)
|
|
elif (b := msg.get("bytes")) is not None:
|
|
await up.send(b)
|
|
|
|
async def u2c() -> None:
|
|
async for m in up:
|
|
if isinstance(m, (bytes, bytearray)):
|
|
await ws.send_bytes(bytes(m))
|
|
else:
|
|
await ws.send_text(m)
|
|
|
|
_done, pending = await asyncio.wait(
|
|
[asyncio.create_task(c2u()), asyncio.create_task(u2c())],
|
|
return_when=asyncio.FIRST_COMPLETED,
|
|
)
|
|
for task in pending:
|
|
task.cancel()
|
|
except Exception:
|
|
log.debug("ttyd-proxy ws (%s) beendet/fehlgeschlagen", base, exc_info=True)
|
|
finally:
|
|
try:
|
|
await ws.close()
|
|
except Exception:
|
|
pass
|
|
|
|
async def _proxy_http(request: Request, path: str = "") -> Response:
|
|
url = f"{upstream}/{base}/{path}"
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as c:
|
|
r = await c.request(request.method, url, content=await request.body())
|
|
return Response(content=r.content, status_code=r.status_code,
|
|
media_type=r.headers.get("content-type"))
|
|
except httpx.HTTPError as exc:
|
|
log.debug("ttyd-proxy http (%s) nicht erreichbar: %s", base, exc)
|
|
return Response(content=b"Terminal (ttyd) nicht erreichbar.", status_code=502,
|
|
media_type="text/plain")
|
|
|
|
router.add_api_route(f"/{base}", _proxy_http, methods=["GET"], name=f"{base}_root")
|
|
router.add_api_route(f"/{base}/{{path:path}}", _proxy_http, methods=["GET", "POST"],
|
|
name=f"{base}_path")
|
|
|
|
|
|
# Box-Konsole (Login-Shell) — Loopback-ttyd. (Das Hermes-Terminal-ttyd ist mit dem
|
|
# Umzug auf Hermes Desktop entfallen; die Agent-Oberfläche ist die Desktop-App bzw. /hermes-ui/.)
|
|
_register("console", BOX_CONSOLE_UPSTREAM)
|