153 lines
4.9 KiB
Python
153 lines
4.9 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, chat
|
|
from app.proxy import close_http_client, proxy_request, proxy_websocket
|
|
from app.personas import ensure_dirs
|
|
from app.db import init_db
|
|
|
|
def get_subdomain_target(host: str) -> Optional[Tuple[str, str]]:
|
|
"""
|
|
Inspects Host header and returns (target_backend_url, universe_id) if it matches a universe hostname.
|
|
|
|
Hostnames (Dash-based 1st level):
|
|
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 -> (HERMES_DSH_URL, 'dsh')
|
|
files-hub.yesminedor.tn -> (DSH_FILEBROWSER_URL, 'nabil')
|
|
|
|
Apex / Hub UI:
|
|
hub.yesminedor.tn -> None (Serves index.html Workspace Switcher)
|
|
"""
|
|
if not host:
|
|
return None
|
|
|
|
hostname = host.split(":")[0].lower()
|
|
|
|
# The main hub switcher UI should NOT be proxied
|
|
if hostname in ("hub.yesminedor.tn", "localhost", "127.0.0.1"):
|
|
return None
|
|
|
|
sub = None
|
|
# 1. New 1st level dash-based production hostnames (*-hub.yesminedor.tn)
|
|
if hostname.endswith("-hub.yesminedor.tn"):
|
|
sub = hostname.rsplit("-hub.yesminedor.tn", 1)[0]
|
|
# 2. Backward compatibility (*.hub.yesminedor.tn)
|
|
elif hostname.endswith(".hub.yesminedor.tn"):
|
|
sub = hostname.rsplit(".hub.yesminedor.tn", 1)[0]
|
|
# 3. Localhost dash development (*-hub.localhost)
|
|
elif hostname.endswith("-hub.localhost"):
|
|
sub = hostname.rsplit("-hub.localhost", 1)[0]
|
|
# 4. Localhost dot development (*.localhost)
|
|
elif hostname.endswith(".localhost"):
|
|
sub = hostname.rsplit(".localhost", 1)[0]
|
|
|
|
if not sub or sub in ("hub", "www"):
|
|
return None
|
|
|
|
if sub in ("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()
|
|
init_db()
|
|
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.include_router(chat.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)
|