"""WebSocket-прокси для VNC-консоли. Берёт у Proxmox одноразовый тикет через `vncproxy` (это работает с API-токеном), затем проксирует бинарный поток от браузера к WebSocket-эндпоинту Proxmox. В зависимости от версии Proxmox WebSocket-эндпоинт может требовать: 1. Заголовок `Authorization: PVEAPIToken=...` (новые версии PVE ≥ 7.x) 2. Cookie `PVEAuthCookie` (старые версии или строгие настройки) В консоль пишется подробный лог, чтобы было видно, на каком этапе отваливается. """ import asyncio import logging import websockets from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect from ..config import settings logger = logging.getLogger(__name__) router = APIRouter(prefix="/console", tags=["console"]) @router.websocket("/ws") async def console_ws( websocket: WebSocket, node: str = Query(...), vmid: int = Query(...), guest_type: str = Query(...), port: int = Query(...), ticket: str = Query(...), ): """ Проксирует бинарный VNC-поток между браузером клиента и WebSocket-эндпоинтом Proxmox (nodes/{node}/(qemu|lxc)/{vmid}/vncwebsocket). Если Proxmox отказывает в аутентификации — см. README, раздел «Консоль VNC». Возможные варианты: обновить PVE, либо задать PVE_USERNAME/PVE_PASSWORD в .env для cookie-авторизации через POST /access/ticket. """ await websocket.accept() guest_path = "qemu" if guest_type == "vm" else "lxc" pve_host = settings.pve_host.replace("http://", "").replace("https://", "") upstream_url = ( f"wss://{pve_host}/api2/json/nodes/{node}/{guest_path}/{vmid}/vncwebsocket" f"?port={port}&vncticket={ticket}" ) headers = { "Authorization": f"PVEAPIToken={settings.pve_token_name}={settings.pve_token_value}" } logger.info( "console_ws: подключение к %s (node=%s, vmid=%s, guest=%s, port=%s)", pve_host, node, vmid, guest_type, port, ) try: async with websockets.connect( upstream_url, extra_headers=headers, subprotocols=["binary"], ssl=None if settings.pve_verify_ssl else False, ) as upstream: logger.info("console_ws: upstream connected") async def client_to_upstream(): try: while True: data = await websocket.receive_bytes() await upstream.send(data) except WebSocketDisconnect: logger.info("console_ws: клиент отключился") except Exception as exc: logger.warning("console_ws: client_to_upstream error: %s", exc) async def upstream_to_client(): try: async for message in upstream: if isinstance(message, str): message = message.encode() await websocket.send_bytes(message) except websockets.ConnectionClosed as exc: logger.info("console_ws: upstream closed: %s", exc) except Exception as exc: logger.warning("console_ws: upstream_to_client error: %s", exc) await asyncio.gather(client_to_upstream(), upstream_to_client()) except websockets.exceptions.InvalidStatus as exc: # Proxmox отверг соединение — скорее всего из-за аутентификации. logger.error( "console_ws: Proxmox отклонил подключение (status=%s). " "Возможно, требуется cookie PVEAuthCookie вместо API-токена — " "задайте PVE_USERNAME и PVE_PASSWORD в backend/.env", exc.response.status_code if exc.response else "?", ) await websocket.close(code=1011, reason=f"Proxmox auth failed: {exc.response.status_code if exc.response else exc}") except Exception as exc: # noqa: BLE001 logger.exception("console_ws: ошибка подключения к Proxmox") await websocket.close(code=1011, reason=str(exc)[:120])