fix(rdp): 服务端 guacd 握手替代透明 TCP 桥接
guacamole-common-js 不会向 guacd 发送 select/connect;改为 Nexus 用 rdp_hosts 凭据完成握手后再桥接 WebSocket,消除 protocol violation。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+57
-10
@@ -4,15 +4,17 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from urllib.parse import parse_qs, unquote
|
||||
|
||||
from fastapi import APIRouter, Path, WebSocket, WebSocketDisconnect
|
||||
|
||||
from server.config import settings
|
||||
from server.domain.models import Admin
|
||||
from server.infrastructure.database.crypto import decrypt_value
|
||||
from server.infrastructure.database.session import AsyncSessionLocal
|
||||
from server.infrastructure.database.admin_repo import AdminRepositoryImpl
|
||||
from server.infrastructure.database.rdp_host_repo import RdpHostRepositoryImpl
|
||||
from server.infrastructure.guacamole.tunnel import bridge_websocket_to_guacd
|
||||
from server.infrastructure.guacamole.ws_tunnel import run_guacamole_rdp_tunnel
|
||||
|
||||
logger = logging.getLogger("nexus.rdp")
|
||||
|
||||
@@ -55,19 +57,35 @@ async def _verify_rdp_access_token(token: str) -> Optional[Admin]:
|
||||
return admin
|
||||
|
||||
|
||||
def _rdp_password_plain(enc: str) -> str:
|
||||
plain = str(enc or "").strip()
|
||||
if not plain:
|
||||
return ""
|
||||
try:
|
||||
return decrypt_value(plain)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _parse_display_size(query_string: bytes) -> tuple[int, int]:
|
||||
qs = parse_qs(query_string.decode("utf-8", errors="replace"))
|
||||
try:
|
||||
w = int(qs.get("width", ["1024"])[0])
|
||||
h = int(qs.get("height", ["768"])[0])
|
||||
if w > 0 and h > 0:
|
||||
return w, h
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return 1024, 768
|
||||
|
||||
|
||||
@router.websocket("/ws/rdp/hosts/{host_id}/tunnel/{access_token}")
|
||||
async def rdp_guacamole_ws(
|
||||
websocket: WebSocket,
|
||||
host_id: int = Path(...),
|
||||
access_token: str = Path(..., description="URL-encoded JWT (not query — Guacamole appends ?connect)"),
|
||||
):
|
||||
"""Transparent Guacamole tunnel to guacd after JWT + RDP host lookup.
|
||||
|
||||
Token is in the path because guacamole-common-js ``Client.connect()`` always
|
||||
appends ``?hostname=...`` to the tunnel URL; a ``?token=`` query would break JWT parsing.
|
||||
"""
|
||||
from urllib.parse import unquote
|
||||
|
||||
"""Guacamole WS tunnel: JWT auth, server-side guacd handshake, then relay."""
|
||||
token = unquote(access_token).strip()
|
||||
if not token:
|
||||
await websocket.close(code=4001, reason="Missing JWT token")
|
||||
@@ -89,17 +107,46 @@ async def rdp_guacamole_ws(
|
||||
await websocket.close(code=4404, reason="RDP host not found")
|
||||
return
|
||||
|
||||
password = _rdp_password_plain(row.password)
|
||||
if not password:
|
||||
await websocket.close(code=4400, reason="RDP password not configured")
|
||||
return
|
||||
|
||||
connect_params = {
|
||||
k: v
|
||||
for k, v in (
|
||||
("hostname", row.host.strip()),
|
||||
("port", str(row.port)),
|
||||
("username", row.username.strip()),
|
||||
("password", password),
|
||||
("security", "any"),
|
||||
("ignore-cert", "true"),
|
||||
("disable-wallpaper", "true"),
|
||||
("disable-theming", "true"),
|
||||
("enable-font-smoothing", "true"),
|
||||
("enable-full-window-drag", "false"),
|
||||
("enable-desktop-composition", "false"),
|
||||
("enable-menu-animations", "false"),
|
||||
)
|
||||
if v
|
||||
}
|
||||
|
||||
width, height = _parse_display_size(websocket.scope.get("query_string", b""))
|
||||
|
||||
await websocket.accept(subprotocol="guacamole")
|
||||
try:
|
||||
await bridge_websocket_to_guacd(
|
||||
await run_guacamole_rdp_tunnel(
|
||||
websocket,
|
||||
settings.GUACD_HOST,
|
||||
settings.GUACD_PORT,
|
||||
connect_params,
|
||||
width=width,
|
||||
height=height,
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except OSError as exc:
|
||||
logger.warning("RDP guacd bridge failed host_id=%s: %s", host_id, exc)
|
||||
logger.warning("RDP guacd tunnel failed host_id=%s: %s", host_id, exc)
|
||||
try:
|
||||
await websocket.close(code=1011, reason="guacd unreachable")
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user