From 06817d1bccd85b02f1b1ae676b79f23ad7859ee1 Mon Sep 17 00:00:00 2001 From: host Date: Fri, 24 Jul 2026 23:54:18 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/console.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/backend/app/routers/console.py b/backend/app/routers/console.py index 8000bf4..86ee154 100644 --- a/backend/app/routers/console.py +++ b/backend/app/routers/console.py @@ -7,6 +7,12 @@ - API-токен (заголовок `Authorization: PVEAPIToken=...`) — новые PVE ≥ 7.x - Cookie PVEAuthCookie — если заданы PVE_USERNAME + PVE_PASSWORD 3. Проксирует бинарный VNC-поток между браузером клиента и Proxmox. + +Особенности Proxmox: + - PVE может вернуть HTTP redirect (302) на `https://...` вместо `wss://...` + при WebSocket-handshake. Библиотека `websockets` не умеет следовать за + такими redirect (https ≠ ws/wss), поэтому перехватываем redirect + и переписываем scheme. """ import asyncio @@ -15,6 +21,8 @@ from typing import Optional, Tuple import httpx import websockets +from websockets.datastructures import Headers +from websockets.http11 import Response from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect from ..config import settings @@ -24,6 +32,23 @@ logger = logging.getLogger(__name__) router = APIRouter(prefix="/console", tags=["console"]) +def _process_redirect(request, response: Response) -> Optional[str]: + """Перехватывает HTTP-redirect от Proxmox и переписывает https://→wss://. + + Proxmox иногда возвращает Location со схемой https:// (хотя для WebSocket + положено wss://). Библиотека websockets в этом случае падает с + InvalidURI("scheme isn't ws or wss"). Возвращаем исправленный URL. + """ + location = response.headers.get("Location", "") + if not location: + return None + if location.startswith("https://"): + return "wss://" + location[len("https://"):] + if location.startswith("http://"): + return "ws://" + location[len("http://"):] + return location + + async def _get_pve_auth_cookie() -> Optional[Tuple[str, str]]: """Аутентифицируется в Proxmox по логину/паролю и возвращает (cookie_header, csrf_token). @@ -76,14 +101,14 @@ async def console_ws( # Определяем способ аутентификации. # Cookie-вариант приоритетнее, т.к. старые/строгие PVE не принимают API-токен на WS. - headers: dict = {} + headers: list = [] auth = await _get_pve_auth_cookie() if auth: cookie_header, _ = auth - headers["Cookie"] = cookie_header + headers.append(("Cookie", cookie_header)) auth_kind = "cookie" else: - headers["Authorization"] = f"PVEAPIToken={settings.pve_token_name}={settings.pve_token_value}" + headers.append(("Authorization", f"PVEAPIToken={settings.pve_token_name}={settings.pve_token_value}")) auth_kind = "api-token" logger.info( @@ -94,9 +119,10 @@ async def console_ws( try: async with websockets.connect( upstream_url, - extra_headers=headers, + additional_headers=headers, subprotocols=["binary"], ssl=None if settings.pve_verify_ssl else False, + process_redirect=_process_redirect, ) as upstream: logger.info("console_ws: upstream connected")