Обновление файла
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user