diff --git a/backend/app/routers/console.py b/backend/app/routers/console.py index 0924e36..cbd96d7 100644 --- a/backend/app/routers/console.py +++ b/backend/app/routers/console.py @@ -7,11 +7,12 @@ - Cookie PVEAuthCookie — если заданы PVE_USERNAME + PVE_PASSWORD 3. Проксирует бинарный VNC-поток между браузером клиента и Proxmox. -Используется aiohttp вместо websockets, потому что: - - Proxmox 8.x+ делает 302 Redirect с Location со схемой https:// вместо wss://, - а websockets на uvloop не умеет правильно следовать за такими редиректами - (raise InvalidURI "scheme isn't ws or wss"). - - aiohttp корректно обрабатывает HTTP→WS-редиректы с заменой scheme. +Особенности Proxmox 8.x+: + - При WebSocket-handshake на vncwebsocket Proxmox отвечает 302 Redirect + на тот же URL, но со схемой https:// вместо wss://. aiohttp при + перенаправлении теряет Cookie-заголовок (или не может следовать за + редиректом без TLS-валидации). Поэтому делаем редирект вручную: + перехватываем ответ, переписываем scheme, открываем WS сами. """ import asyncio @@ -76,7 +77,6 @@ async def console_ws( guest_path = "qemu" if guest_type == "vm" else "lxc" pve_host_only = settings.pve_host.replace("http://", "").replace("https://", "") - # Экранируем спецсимволы в тикете/порте (vncticket может содержать # / % :). safe_ticket = quote(ticket, safe="") safe_port = quote(str(port), safe="") upstream_url = ( @@ -84,8 +84,6 @@ async def console_ws( f"?port={safe_port}&vncticket={safe_ticket}" ) - # Определяем способ аутентификации: cookie приоритетнее (некоторые PVE - # не принимают API-токен на WebSocket). headers: dict = {} auth = await _get_pve_auth_cookie() if auth: @@ -106,9 +104,38 @@ async def console_ws( upstream: Optional[aiohttp.ClientWebSocketResponse] = None session: Optional[ClientSession] = None try: - # aiohttp сам следует за HTTP-редиректами и при этом корректно - # меняет схему с https на wss (что websockets на uvloop не делает). session = ClientSession() + # Отключаем автоматический redirect, чтобы самим обработать https→wss + # с сохранением Cookie-заголовка. + resp = await session.get( + upstream_url, + headers=headers, + allow_redirects=False, + ssl=False if not settings.pve_verify_ssl else None, + ) + if resp.status in (301, 302, 303, 307, 308): + location = resp.headers.get("Location", "") + logger.info("console_ws: redirect to %s", location) + if location.startswith("https://"): + upstream_url = "wss://" + location[len("https://"):] + elif location.startswith("http://"): + upstream_url = "ws://" + location[len("http://"):] + else: + upstream_url = location + await resp.release() + elif resp.status == 101: + # Proxmox уже сделал WS-handshake (редкий случай без редиректа). + await resp.release() + else: + await resp.release() + raise aiohttp.ClientResponseError( + request_info=resp.request_info, + history=resp.history, + status=resp.status, + message=f"unexpected status {resp.status}", + headers=resp.headers, + ) + upstream = await session.ws_connect( upstream_url, headers=headers,