Files
hermes-hub/app/main.py
T

133 lines
4.1 KiB
Python

from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import Optional, Tuple
from contextlib import asynccontextmanager
from app.config import settings, UNIVERSES, BASE_DIR
from app.routers import api, proxy
from app.proxy import close_http_client, proxy_request, proxy_websocket
from app.personas import ensure_dirs
def get_subdomain_target(host: str) -> Optional[Tuple[str, str]]:
"""
Inspects Host header and returns (target_backend_url, universe_id) if it matches a subdomain.
Examples:
tt.hub.yesminedor.tn -> (HERMES_TT_URL, 'tt')
nyora.hub.yesminedor.tn -> (HERMES_NYORA_URL, 'nyora')
perso.hub.yesminedor.tn -> (HERMES_PERSO_URL, 'perso')
nabil.hub.yesminedor.tn -> (HERMES_NABIL_URL, 'nabil')
dsh.hub.yesminedor.tn -> (DSH_FILEBROWSER_URL, 'nabil')
"""
if not host:
return None
hostname = host.split(":")[0].lower()
sub = None
if hostname.endswith(".hub.yesminedor.tn"):
sub = hostname.rsplit(".hub.yesminedor.tn", 1)[0]
elif hostname.endswith(".localhost"):
sub = hostname.rsplit(".localhost", 1)[0]
if not sub or sub in ("hub", "www"):
return None
if sub in ("dsh", "files", "dsh-files"):
return (settings.dsh_filebrowser_url, "nabil")
if sub in UNIVERSES:
return (UNIVERSES[sub].backend_url, sub)
return None
@asynccontextmanager
async def lifespan(app: FastAPI):
ensure_dirs()
yield
await close_http_client()
app = FastAPI(
title=settings.app_name,
version="1.0.0",
lifespan=lifespan
)
# Subdomain Routing Middleware for HTTP
@app.middleware("http")
async def subdomain_routing_middleware(request: Request, call_next):
host = request.headers.get("host", "")
target = get_subdomain_target(host)
if target:
backend_url, universe_id = target
path = request.url.path
return await proxy_request(
request=request,
backend_url=backend_url,
path=path,
universe_id=universe_id
)
return await call_next(request)
# Mount static files
static_dir = BASE_DIR / "static"
static_dir.mkdir(parents=True, exist_ok=True)
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
# Templates
templates_dir = BASE_DIR / "app" / "templates"
templates = Jinja2Templates(directory=str(templates_dir))
# Include Routers
app.include_router(api.router)
app.include_router(proxy.router)
@app.get("/", response_class=HTMLResponse)
@app.head("/", response_class=HTMLResponse)
async def index_view(request: Request):
return templates.TemplateResponse(
request=request,
name="index.html",
context={
"universes": UNIVERSES,
"app_name": settings.app_name
}
)
# WebSocket Proxy Route (intercepts any WebSocket connection across subdomains)
@app.websocket("/{path:path}")
@app.websocket("")
async def websocket_proxy_endpoint(websocket: WebSocket, path: str = ""):
host = websocket.headers.get("host", "")
target = get_subdomain_target(host)
if target:
backend_url, universe_id = target
await proxy_websocket(
client_ws=websocket,
backend_url=backend_url,
path=path,
universe_id=universe_id
)
elif path.startswith("u/"):
parts = path.split("/", 2)
if len(parts) >= 2 and parts[1] in UNIVERSES:
u_id = parts[1]
subpath = parts[2] if len(parts) > 2 else ""
await proxy_websocket(
client_ws=websocket,
backend_url=UNIVERSES[u_id].backend_url,
path=subpath,
universe_id=u_id
)
else:
await websocket.close(code=1008)
else:
await websocket.close(code=1008)
if __name__ == "__main__":
import uvicorn
uvicorn.run("app.main:app", host=settings.host, port=settings.port, reload=settings.debug)