diff --git a/backend/app/routers/console.py b/backend/app/routers/console.py index 7e31858..c68ca87 100644 --- a/backend/app/routers/console.py +++ b/backend/app/routers/console.py @@ -15,12 +15,16 @@ - vncticket привязан к origin (схема+хост+порт), в котором был запрошен. Чтобы проксировать через внешний DNS, нужно передать host= в vncproxy.post() (см. proxmox_client.py). + - Proxmox в Location редиректа часто возвращает свой self-reported + origin (например, pve1.input.netcraze.pro — тот, что в сертификате). + Если backend работает внутри сети и должен стучаться на внутренний + IP (192.168.31.4), нужно заменить hostname в Location обратно. """ import asyncio import logging from typing import Optional, Tuple -from urllib.parse import quote +from urllib.parse import quote, urlparse, urlunparse import aiohttp import httpx @@ -34,6 +38,51 @@ logger = logging.getLogger(__name__) router = APIRouter(prefix="/console", tags=["console"]) +def _rewrite_redirect(location: str, fallback_host: str) -> str: + """Переписывает URL редиректа: заменяет https→wss и нежелательные хосты. + + Args: + location: значение заголовка Location из Proxmox. + fallback_host: хост, на который нужно стучаться по сети + (например, 192.168.31.4:8006). Если в location хост + отличается — он будет заменён. + """ + if not location: + return location + + # 1. ws/wss вместо http/https. + if location.startswith("https://"): + location = "wss://" + location[len("https://"):] + elif location.startswith("http://"): + location = "ws://" + location[len("http://"):] + + # 2. Если хост в редиректе — не тот, через который мы работаем + # (например, Proxmox отдаёт свой публичный DNS, а backend сидит + # внутри сети), заменяем host:port на fallback_host. + parsed = urlparse(location) + if parsed.hostname: + # Берём host:port из fallback_host (вида "192.168.31.4:8006" + # или "pve1.input.netcraze.pro:8006"). + fb_host = fallback_host + if ":" in fb_host and not fb_host.startswith("["): + # IPv6 — оборачиваем в скобки. + pass + if ":" in fb_host: + fb_hostname, _, fb_port = fb_host.partition(":") + else: + fb_hostname, fb_port = fb_host, "" + + # Определяем, что Proxmox вернул «чужой» адрес (например, + # свой публичный DNS), а не тот, через который мы работаем. + # Сравниваем по hostname. + if parsed.hostname != fb_hostname: + new_netloc = fb_hostname + if fb_port: + new_netloc = f"{fb_hostname}:{fb_port}" + location = urlunparse(parsed._replace(netloc=new_netloc)) + return location + + async def _get_pve_auth_cookie() -> Optional[Tuple[str, str]]: """Аутентифицируется в Proxmox по логину/паролю через REST API. @@ -80,16 +129,18 @@ async def console_ws( await websocket.accept() guest_path = "qemu" if guest_type == "vm" else "lxc" - # По умолчанию WebSocket идёт на тот же хост, что и PVE_HOST. Если - # vncticket был выписан для внешнего origin (PVE_PUBLIC_HOST), то и - # WebSocket нужно открывать к нему — иначе Proxmox отвергнет тикет. - pve_origin = settings.pve_public_host or settings.pve_host - pve_host_only = pve_origin.replace("http://", "").replace("https://", "") + # Хост, через который backend реально ходит к Proxmox внутри сети. + # Используем PVE_HOST без схемы и порта. + pve_backend_host = settings.pve_host.replace("http://", "").replace("https://", "").split(":")[0] + pve_backend_port = settings.pve_host.replace("http://", "").replace("https://", "").split(":")[-1] + if not pve_backend_port.isdigit(): + pve_backend_port = "8006" + fallback_host = f"{pve_backend_host}:{pve_backend_port}" safe_ticket = quote(ticket, safe="") safe_port = quote(str(port), safe="") upstream_url = ( - f"wss://{pve_host_only}/api2/json/nodes/{node}/{guest_path}/{vmid}/vncwebsocket" + f"wss://{fallback_host}/api2/json/nodes/{node}/{guest_path}/{vmid}/vncwebsocket" f"?port={safe_port}&vncticket={safe_ticket}" ) @@ -131,12 +182,7 @@ async def console_ws( ) if resp.status in (301, 302, 303, 307, 308): location = resp.headers.get("Location", "") - if location.startswith("https://"): - upstream_url = "wss://" + location[len("https://"):] - elif location.startswith("http://"): - upstream_url = "ws://" + location[len("http://"):] - else: - upstream_url = location + upstream_url = _rewrite_redirect(location, fallback_host) logger.info("console_ws: rewritten URL to %s", upstream_url) await resp.release() elif resp.status == 101: