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

This commit is contained in:
2026-07-24 23:09:16 +03:00
parent d99dc5b5ac
commit f1c1833eca
+64 -31
View File
@@ -1,18 +1,19 @@
"""WebSocket-прокси для VNC-консоли. """WebSocket-прокси для VNC-консоли.
Берёт у Proxmox одноразовый тикет через `vncproxy` (это работает с API-токеном), Поток данных:
затем проксирует бинарный поток от браузера к WebSocket-эндпоинту Proxmox. 1. Backend получает от Proxmox одноразовый тикет через `vncproxy` (REST API,
работает с API-токеном).
В зависимости от версии Proxmox WebSocket-эндпоинт может требовать: 2. Открывает WebSocket к Proxmox с этим тикетом и портом. Аутентификация:
1. Заголовок `Authorization: PVEAPIToken=...` (новые версии PVE ≥ 7.x) - API-токен (заголовок `Authorization: PVEAPIToken=...`) — новые PVE ≥ 7.x
2. Cookie `PVEAuthCookie` (старые версии или строгие настройки) - Cookie PVEAuthCookie — если заданы PVE_USERNAME + PVE_PASSWORD
3. Проксирует бинарный VNC-поток между браузером клиента и Proxmox.
В консоль пишется подробный лог, чтобы было видно, на каком этапе отваливается.
""" """
import asyncio import asyncio
import logging import logging
from typing import Optional, Tuple
import httpx
import websockets import websockets
from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect
@@ -23,6 +24,37 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/console", tags=["console"]) router = APIRouter(prefix="/console", tags=["console"])
async def _get_pve_auth_cookie() -> Optional[Tuple[str, str]]:
"""Аутентифицируется в Proxmox по логину/паролю и возвращает (cookie_header, csrf_token).
Возвращает None, если PVE_USERNAME/PVE_PASSWORD не заданы в настройках.
"""
if not settings.pve_username or not settings.pve_password:
return None
url = f"{settings.pve_host}/api2/json/access/ticket"
payload = {"username": settings.pve_username, "password": settings.pve_password}
verify = settings.pve_verify_ssl
try:
async with httpx.AsyncClient(verify=verify, timeout=10.0) as client:
resp = await client.post(url, data=payload)
resp.raise_for_status()
data = resp.json().get("data", {})
cookie = data.get("ticket") # в Proxmox поле называется "ticket"
csrf = data.get("CSRFPreventionToken")
if not cookie:
logger.error("auth: Proxmox не вернул PVEAuthCookie (пустой ответ)")
return None
# Для WebSocket-эндпоинта vncwebsocket достаточно PVEAuthCookie.
cookie_header = f"PVEAuthCookie={cookie}"
logger.info("auth: PVEAuthCookie получен успешно")
return cookie_header, csrf or ""
except Exception as exc:
logger.exception("auth: ошибка аутентификации в Proxmox")
return None
@router.websocket("/ws") @router.websocket("/ws")
async def console_ws( async def console_ws(
websocket: WebSocket, websocket: WebSocket,
@@ -32,30 +64,31 @@ async def console_ws(
port: int = Query(...), port: int = Query(...),
ticket: str = Query(...), ticket: str = Query(...),
): ):
""" """Проксирует бинарный VNC-поток между браузером и Proxmox."""
Проксирует бинарный 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() await websocket.accept()
guest_path = "qemu" if guest_type == "vm" else "lxc" guest_path = "qemu" if guest_type == "vm" else "lxc"
pve_host = settings.pve_host.replace("http://", "").replace("https://", "") pve_host_only = settings.pve_host.replace("http://", "").replace("https://", "")
upstream_url = ( upstream_url = (
f"wss://{pve_host}/api2/json/nodes/{node}/{guest_path}/{vmid}/vncwebsocket" f"wss://{pve_host_only}/api2/json/nodes/{node}/{guest_path}/{vmid}/vncwebsocket"
f"?port={port}&vncticket={ticket}" f"?port={port}&vncticket={ticket}"
) )
headers = { # Определяем способ аутентификации.
"Authorization": f"PVEAPIToken={settings.pve_token_name}={settings.pve_token_value}" # Cookie-вариант приоритетнее, т.к. старые/строгие PVE не принимают API-токен на WS.
} headers: dict = {}
auth = await _get_pve_auth_cookie()
if auth:
cookie_header, _ = auth
headers["Cookie"] = cookie_header
auth_kind = "cookie"
else:
headers["Authorization"] = f"PVEAPIToken={settings.pve_token_name}={settings.pve_token_value}"
auth_kind = "api-token"
logger.info( logger.info(
"console_ws: подключение к %s (node=%s, vmid=%s, guest=%s, port=%s)", "console_ws: connecting node=%s vmid=%s guest=%s port=%s auth=%s",
pve_host, node, vmid, guest_type, port, node, vmid, guest_type, port, auth_kind,
) )
try: try:
@@ -73,7 +106,7 @@ async def console_ws(
data = await websocket.receive_bytes() data = await websocket.receive_bytes()
await upstream.send(data) await upstream.send(data)
except WebSocketDisconnect: except WebSocketDisconnect:
logger.info("console_ws: клиент отключился") logger.info("console_ws: client disconnected")
except Exception as exc: except Exception as exc:
logger.warning("console_ws: client_to_upstream error: %s", exc) logger.warning("console_ws: client_to_upstream error: %s", exc)
@@ -84,20 +117,20 @@ async def console_ws(
message = message.encode() message = message.encode()
await websocket.send_bytes(message) await websocket.send_bytes(message)
except websockets.ConnectionClosed as exc: except websockets.ConnectionClosed as exc:
logger.info("console_ws: upstream closed: %s", exc) logger.info("console_ws: upstream closed: code=%s", exc.code)
except Exception as exc: except Exception as exc:
logger.warning("console_ws: upstream_to_client error: %s", exc) logger.warning("console_ws: upstream_to_client error: %s", exc)
await asyncio.gather(client_to_upstream(), upstream_to_client()) await asyncio.gather(client_to_upstream(), upstream_to_client())
except websockets.exceptions.InvalidStatus as exc: except websockets.exceptions.InvalidStatus as exc:
# Proxmox отверг соединение — скорее всего из-за аутентификации. status = exc.response.status_code if exc.response else "?"
logger.error( logger.error(
"console_ws: Proxmox отклонил подключение (status=%s). " "console_ws: Proxmox отклонил подключение (status=%s, auth=%s). "
"Возможно, требуется cookie PVEAuthCookie вместо API-токена — " "Если 401 — задайте PVE_USERNAME/PVE_PASSWORD в backend/.env "
"задайте PVE_USERNAME и PVE_PASSWORD в backend/.env", "для cookie-аутентификации.",
exc.response.status_code if exc.response else "?", status, auth_kind,
) )
await websocket.close(code=1011, reason=f"Proxmox auth failed: {exc.response.status_code if exc.response else exc}") await websocket.close(code=1011, reason=f"Proxmox auth failed: {status}")
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
logger.exception("console_ws: ошибка подключения к Proxmox") logger.exception("console_ws: ошибка подключения к Proxmox")
await websocket.close(code=1011, reason=str(exc)[:120]) await websocket.close(code=1011, reason=str(exc)[:120])