Обновление файла

This commit is contained in:
2026-07-25 01:46:29 +03:00
parent c8950e19e6
commit 9e1d06ba3d
+26 -14
View File
@@ -4,15 +4,15 @@
1. Backend получает от Proxmox одноразовый тикет через `vncproxy` (REST API). 1. Backend получает от Proxmox одноразовый тикет через `vncproxy` (REST API).
2. Открывает WebSocket к Proxmox с этим тикетом и портом. Аутентификация: 2. Открывает WebSocket к Proxmox с этим тикетом и портом. Аутентификация:
- API-токен (заголовок `Authorization: PVEAPIToken=...`) — новые PVE ≥ 7.x - API-токен (заголовок `Authorization: PVEAPIToken=...`) — новые PVE ≥ 7.x
- Cookie PVEAuthCookie — если заданы PVE_USERNAME + PVE_PASSWORD - Cookie PVEAuthCookie + CSRFPreventionToken — если заданы PVE_USERNAME + PVE_PASSWORD
3. Проксирует бинарный VNC-поток между браузером клиента и Proxmox. 3. Проксирует бинарный VNC-поток между браузером клиента и Proxmox.
Особенности Proxmox 8.x+: Особенности Proxmox 8.x+:
- При WebSocket-handshake на vncwebsocket Proxmox отвечает 302 Redirect - WebSocket-эндпоинт vncwebsocket возвращает 302 Redirect с Location со
на тот же URL, но со схемой https:// вместо wss://. aiohttp при схемой https:// вместо wss://. Обрабатываем редирект вручную,
перенаправлении теряет Cookie-заголовок (или не может следовать за чтобы сохранить Cookie и переписать scheme.
редиректом без TLS-валидации). Поэтому делаем редирект вручную: - На WebSocket Proxmox требует одновременно Cookie и CSRF-токен
перехватываем ответ, переписываем scheme, открываем WS сами. (только для POST/PUT/DELETE, но Proxmox иногда проверяет и для WS-handshake).
""" """
import asyncio import asyncio
@@ -55,7 +55,10 @@ async def _get_pve_auth_cookie() -> Optional[Tuple[str, str]]:
if not cookie: if not cookie:
logger.error("auth: Proxmox не вернул PVEAuthCookie") logger.error("auth: Proxmox не вернул PVEAuthCookie")
return None return None
logger.info("auth: PVEAuthCookie получен успешно") logger.info(
"auth: PVEAuthCookie получен (len=%d, csrf_prefix=%s)",
len(cookie), (csrf or "")[:8],
)
return f"PVEAuthCookie={cookie}", csrf or "" return f"PVEAuthCookie={cookie}", csrf or ""
except Exception as exc: except Exception as exc:
logger.exception("auth: ошибка аутентификации в Proxmox") logger.exception("auth: ошибка аутентификации в Proxmox")
@@ -85,10 +88,15 @@ async def console_ws(
) )
headers: dict = {} headers: dict = {}
csrf_token: Optional[str] = None
auth = await _get_pve_auth_cookie() auth = await _get_pve_auth_cookie()
if auth: if auth:
cookie_header, _ = auth cookie_header, csrf_token = auth
headers["Cookie"] = cookie_header headers["Cookie"] = cookie_header
# Proxmox ожидает CSRF-токен в одноимённом заголовке для всех
# не-GET запросов; для WebSocket-handshake передаём на всякий случай.
if csrf_token:
headers["CSRFPreventionToken"] = csrf_token
auth_kind = "cookie" auth_kind = "cookie"
else: else:
headers["Authorization"] = ( headers["Authorization"] = (
@@ -97,34 +105,38 @@ async def console_ws(
auth_kind = "api-token" auth_kind = "api-token"
logger.info( logger.info(
"console_ws: connecting node=%s vmid=%s guest=%s port=%s auth=%s", "console_ws: connecting node=%s vmid=%s guest=%s port=%s auth=%s ticket_prefix=%s",
node, vmid, guest_type, port, auth_kind, node, vmid, guest_type, port, auth_kind, safe_ticket[:24],
) )
upstream: Optional[aiohttp.ClientWebSocketResponse] = None upstream: Optional[aiohttp.ClientWebSocketResponse] = None
session: Optional[ClientSession] = None session: Optional[ClientSession] = None
try: try:
session = ClientSession() session = ClientSession()
# Отключаем автоматический redirect, чтобы самим обработать https→wss # Отключаем автоматический redirect, чтобы самим обработать https→wss.
# с сохранением Cookie-заголовка.
resp = await session.get( resp = await session.get(
upstream_url, upstream_url,
headers=headers, headers=headers,
allow_redirects=False, allow_redirects=False,
ssl=False if not settings.pve_verify_ssl else None, ssl=False if not settings.pve_verify_ssl else None,
) )
logger.info(
"console_ws: initial GET status=%s, location=%s, body=%s",
resp.status,
resp.headers.get("Location", "<none>"),
(await resp.text())[:200],
)
if resp.status in (301, 302, 303, 307, 308): if resp.status in (301, 302, 303, 307, 308):
location = resp.headers.get("Location", "") location = resp.headers.get("Location", "")
logger.info("console_ws: redirect to %s", location)
if location.startswith("https://"): if location.startswith("https://"):
upstream_url = "wss://" + location[len("https://"):] upstream_url = "wss://" + location[len("https://"):]
elif location.startswith("http://"): elif location.startswith("http://"):
upstream_url = "ws://" + location[len("http://"):] upstream_url = "ws://" + location[len("http://"):]
else: else:
upstream_url = location upstream_url = location
logger.info("console_ws: rewritten URL to %s", upstream_url)
await resp.release() await resp.release()
elif resp.status == 101: elif resp.status == 101:
# Proxmox уже сделал WS-handshake (редкий случай без редиректа).
await resp.release() await resp.release()
else: else:
await resp.release() await resp.release()