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