From d6442499382ed1c6a185f777ac80c13aefb87fc8 Mon Sep 17 00:00:00 2001 From: host Date: Sat, 25 Jul 2026 00:08:03 +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 | 160 +-------------------------------- 1 file changed, 4 insertions(+), 156 deletions(-) diff --git a/backend/app/routers/console.py b/backend/app/routers/console.py index 86ee154..664d90a 100644 --- a/backend/app/routers/console.py +++ b/backend/app/routers/console.py @@ -1,162 +1,10 @@ -"""WebSocket-прокси для VNC-консоли. - -Поток данных: - 1. Backend получает от Proxmox одноразовый тикет через `vncproxy` (REST API, - работает с API-токеном). - 2. Открывает WebSocket к Proxmox с этим тикетом и портом. Аутентификация: - - 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 -import logging -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 - -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). - - Возвращает 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") -async def console_ws( - websocket: WebSocket, - node: str = Query(...), - vmid: int = Query(...), - guest_type: str = Query(...), - port: int = Query(...), - ticket: str = Query(...), -): - """Проксирует бинарный VNC-поток между браузером и Proxmox.""" - await websocket.accept() - - guest_path = "qemu" if guest_type == "vm" else "lxc" - pve_host_only = settings.pve_host.replace("http://", "").replace("https://", "") - upstream_url = ( - f"wss://{pve_host_only}/api2/json/nodes/{node}/{guest_path}/{vmid}/vncwebsocket" - f"?port={port}&vncticket={ticket}" - ) - - # Определяем способ аутентификации. - # Cookie-вариант приоритетнее, т.к. старые/строгие PVE не принимают API-токен на WS. - headers: list = [] - auth = await _get_pve_auth_cookie() - if auth: - cookie_header, _ = auth - headers.append(("Cookie", cookie_header)) - auth_kind = "cookie" - else: - headers.append(("Authorization", f"PVEAPIToken={settings.pve_token_name}={settings.pve_token_value}")) - auth_kind = "api-token" - - logger.info( - "console_ws: connecting node=%s vmid=%s guest=%s port=%s auth=%s", - node, vmid, guest_type, port, auth_kind, - ) - try: + # extra_headers (а не additional_headers) — последний не поддерживается + # в uvloop, который использует uvicorn[standard]. async with websockets.connect( upstream_url, - additional_headers=headers, + extra_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") - - async def client_to_upstream(): - try: - while True: - data = await websocket.receive_bytes() - await upstream.send(data) - except WebSocketDisconnect: - logger.info("console_ws: client disconnected") - except Exception as exc: - logger.warning("console_ws: client_to_upstream error: %s", exc) - - async def upstream_to_client(): - try: - async for message in upstream: - if isinstance(message, str): - message = message.encode() - await websocket.send_bytes(message) - except websockets.ConnectionClosed as exc: - logger.info("console_ws: upstream closed: code=%s", exc.code) - except Exception as exc: - logger.warning("console_ws: upstream_to_client error: %s", exc) - - await asyncio.gather(client_to_upstream(), upstream_to_client()) - except websockets.exceptions.InvalidStatus as exc: - status = exc.response.status_code if exc.response else "?" - logger.error( - "console_ws: Proxmox отклонил подключение (status=%s, auth=%s). " - "Если 401 — задайте PVE_USERNAME/PVE_PASSWORD в backend/.env " - "для cookie-аутентификации.", - status, auth_kind, - ) - await websocket.close(code=1011, reason=f"Proxmox auth failed: {status}") - except Exception as exc: # noqa: BLE001 - logger.exception("console_ws: ошибка подключения к Proxmox") - await websocket.close(code=1011, reason=str(exc)[:120]) + ) as upstream: \ No newline at end of file