Files
Nexus/server/api/rdp.py
T

106 lines
3.2 KiB
Python
Raw Normal View History

"""Browser RDP — Guacamole WebSocket tunnel to guacd (no session audit)."""
from __future__ import annotations
import logging
from typing import Optional
from fastapi import APIRouter, Path, Query, WebSocket, WebSocketDisconnect
from server.config import settings
from server.domain.models import Admin
from server.infrastructure.database.session import AsyncSessionLocal
from server.infrastructure.database.admin_repo import AdminRepositoryImpl
2026-06-10 20:58:46 +08:00
from server.infrastructure.database.rdp_host_repo import RdpHostRepositoryImpl
from server.infrastructure.guacamole.tunnel import bridge_websocket_to_guacd
logger = logging.getLogger("nexus.rdp")
router = APIRouter()
async def _verify_rdp_access_token(token: str) -> Optional[Admin]:
"""Verify normal access JWT for RDP (reject webssh-only tokens)."""
try:
import jwt as pyjwt
payload = pyjwt.decode(
token,
settings.SECRET_KEY,
algorithms=["HS256"],
options={"require": ["exp", "sub"]},
)
except Exception:
return None
if payload.get("purpose") == "webssh":
return None
admin_id = payload.get("sub")
if not admin_id:
return None
try:
admin_id = int(admin_id)
except (TypeError, ValueError):
return None
async with AsyncSessionLocal() as session:
admin_repo = AdminRepositoryImpl(session)
admin = await admin_repo.get_by_id(admin_id)
if not admin or not admin.is_active:
return None
token_tv = payload.get("tv") or 0
if token_tv != (admin.token_version or 0):
return None
return admin
2026-06-10 20:58:46 +08:00
@router.websocket("/ws/rdp/hosts/{host_id}")
async def rdp_guacamole_ws(
websocket: WebSocket,
2026-06-10 20:58:46 +08:00
host_id: int = Path(...),
token: Optional[str] = Query(None),
):
2026-06-10 20:58:46 +08:00
"""Transparent Guacamole tunnel to guacd after JWT + RDP host lookup."""
if not token:
await websocket.close(code=4001, reason="Missing JWT token")
return
admin = await _verify_rdp_access_token(token)
if not admin:
await websocket.close(code=4401, reason="Invalid or expired JWT token")
return
if not settings.GUACD_HOST:
await websocket.close(code=4503, reason="guacd not configured")
return
async with AsyncSessionLocal() as session:
2026-06-10 20:58:46 +08:00
repo = RdpHostRepositoryImpl(session)
row = await repo.get_by_id(host_id)
if not row:
await websocket.close(code=4404, reason="RDP host not found")
return
await websocket.accept(subprotocol="guacamole")
try:
await bridge_websocket_to_guacd(
websocket,
settings.GUACD_HOST,
settings.GUACD_PORT,
)
except WebSocketDisconnect:
pass
except OSError as exc:
2026-06-10 20:58:46 +08:00
logger.warning("RDP guacd bridge failed host_id=%s: %s", host_id, exc)
try:
await websocket.close(code=1011, reason="guacd unreachable")
except Exception:
pass
except Exception:
2026-06-10 20:58:46 +08:00
logger.exception("RDP tunnel error host_id=%s", host_id)
try:
await websocket.close(code=1011, reason="RDP tunnel error")
except Exception:
pass