serve.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. """KVM Frontend - Static files + API proxy + WebSocket VNC proxy"""
  2. import os
  3. import re
  4. from starlette.applications import Starlette
  5. from starlette.routing import Route, WebSocketRoute, Mount
  6. from starlette.staticfiles import StaticFiles
  7. from starlette.responses import FileResponse, RedirectResponse
  8. from starlette.websockets import WebSocket
  9. import uvicorn
  10. HOST = "0.0.0.0"
  11. PORT = 8006
  12. DIST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "dist")
  13. API_BASE = "http://127.0.0.1:8004"
  14. WS_BASE = "ws://127.0.0.1:8004"
  15. # ── SPA fallback ──────────────────────────────────────────────
  16. async def serve_spa(request):
  17. path = request.url.path
  18. file_path = os.path.join(DIST_DIR, path.lstrip("/"))
  19. if os.path.isfile(file_path):
  20. return FileResponse(file_path)
  21. return FileResponse(os.path.join(DIST_DIR, "index.html"))
  22. # ── API proxy ─────────────────────────────────────────────────
  23. async def proxy_api(request):
  24. from starlette.requests import Request
  25. body = await request.body()
  26. headers = {}
  27. for key in ["content-type", "authorization"]:
  28. val = request.headers.get(key)
  29. if val:
  30. headers[key.replace("_", "-")] = val
  31. url = f"{API_BASE}{request.url.path}"
  32. if request.url.query:
  33. url += f"?{request.url.query}"
  34. async with __import__("httpx").AsyncClient(timeout=30) as client:
  35. try:
  36. resp = await client.request(
  37. method=request.method,
  38. url=url,
  39. content=body,
  40. headers=headers,
  41. )
  42. return Response(
  43. resp.content,
  44. status_code=resp.status_code,
  45. headers={"access-control-allow-origin": "*"},
  46. )
  47. except Exception as e:
  48. from starlette.responses import JSONResponse
  49. return JSONResponse({"error": str(e)}, status_code=502)
  50. from starlette.responses import Response
  51. return Response("", status_code=500)
  52. # ── WebSocket VNC proxy ────────────────────────────────────────
  53. async def vnc_websocket(websocket: WebSocket):
  54. await websocket.accept()
  55. # Build backend WebSocket URL
  56. path = websocket.url.path # e.g. /ws/vnc/vm-name?host_id=local
  57. backend_url = f"{WS_BASE}{path}"
  58. try:
  59. async with __import__("websockets").connect(backend_url) as backend:
  60. async def forward_to_backend():
  61. async for msg in websocket.iter_text():
  62. await backend.send(msg)
  63. async for msg in websocket.iter_bytes():
  64. await backend.send(msg)
  65. async def forward_to_frontend():
  66. async for msg in backend:
  67. if isinstance(msg, bytes):
  68. await websocket.send_bytes(msg)
  69. else:
  70. await websocket.send_text(msg)
  71. await __import__("asyncio").gather(
  72. forward_to_backend(),
  73. forward_to_frontend(),
  74. )
  75. except Exception as e:
  76. print(f"WebSocket proxy error: {e}")
  77. finally:
  78. try:
  79. await websocket.close()
  80. except Exception:
  81. pass
  82. from starlette.responses import Response
  83. routes = [
  84. WebSocketRoute("/ws/{path:path}", vnc_websocket, name="vnc"),
  85. Route("/api/{path:path}", proxy_api),
  86. Route("/api", proxy_api),
  87. Mount("/", StaticFiles(directory=DIST_DIR, html=True), name="static"),
  88. ]
  89. app = Starlette(routes=routes)
  90. if __name__ == "__main__":
  91. uvicorn.run(
  92. app,
  93. host=HOST,
  94. port=PORT,
  95. log_level="info",
  96. )