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

This commit is contained in:
2026-07-25 02:17:13 +03:00
parent 11133a116d
commit 2e88f5797c
+59 -13
View File
@@ -15,12 +15,16 @@
- vncticket привязан к origin (схема+хост+порт), в котором был - vncticket привязан к origin (схема+хост+порт), в котором был
запрошен. Чтобы проксировать через внешний DNS, нужно передать запрошен. Чтобы проксировать через внешний DNS, нужно передать
host=<public_host> в vncproxy.post() (см. proxmox_client.py). host=<public_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 asyncio
import logging import logging
from typing import Optional, Tuple from typing import Optional, Tuple
from urllib.parse import quote from urllib.parse import quote, urlparse, urlunparse
import aiohttp import aiohttp
import httpx import httpx
@@ -34,6 +38,51 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/console", tags=["console"]) 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]]: async def _get_pve_auth_cookie() -> Optional[Tuple[str, str]]:
"""Аутентифицируется в Proxmox по логину/паролю через REST API. """Аутентифицируется в Proxmox по логину/паролю через REST API.
@@ -80,16 +129,18 @@ async def console_ws(
await websocket.accept() await websocket.accept()
guest_path = "qemu" if guest_type == "vm" else "lxc" guest_path = "qemu" if guest_type == "vm" else "lxc"
# По умолчанию WebSocket идёт на тот же хост, что и PVE_HOST. Если # Хост, через который backend реально ходит к Proxmox внутри сети.
# vncticket был выписан для внешнего origin (PVE_PUBLIC_HOST), то и # Используем PVE_HOST без схемы и порта.
# WebSocket нужно открывать к нему — иначе Proxmox отвергнет тикет. pve_backend_host = settings.pve_host.replace("http://", "").replace("https://", "").split(":")[0]
pve_origin = settings.pve_public_host or settings.pve_host pve_backend_port = settings.pve_host.replace("http://", "").replace("https://", "").split(":")[-1]
pve_host_only = pve_origin.replace("http://", "").replace("https://", "") 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_ticket = quote(ticket, safe="")
safe_port = quote(str(port), safe="") safe_port = quote(str(port), safe="")
upstream_url = ( 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}" f"?port={safe_port}&vncticket={safe_ticket}"
) )
@@ -131,12 +182,7 @@ async def console_ws(
) )
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", "")
if location.startswith("https://"): upstream_url = _rewrite_redirect(location, fallback_host)
upstream_url = "wss://" + location[len("https://"):]
elif location.startswith("http://"):
upstream_url = "ws://" + location[len("http://"):]
else:
upstream_url = location
logger.info("console_ws: rewritten URL to %s", upstream_url) logger.info("console_ws: rewritten URL to %s", upstream_url)
await resp.release() await resp.release()
elif resp.status == 101: elif resp.status == 101: