feat: implement native subdomain routing (*.hub.yesminedor.tn) for seamless absolute assets resolution across all universes

This commit is contained in:
bolbol
2026-08-20 09:03:30 +01:00
parent b6ce9788f3
commit e074015b72
3 changed files with 86 additions and 20 deletions
+50 -1
View File
@@ -3,13 +3,46 @@ 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
from app.proxy import close_http_client, proxy_request
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()
@@ -22,6 +55,22 @@ app = FastAPI(
lifespan=lifespan
)
# Subdomain Routing Middleware
@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)
+11 -5
View File
@@ -40,7 +40,7 @@ async def close_http_client():
def rewrite_cookie_path(cookie_header: str, universe_id: str) -> str:
"""
Rewrites the Path attribute of a Set-Cookie header to /u/{universe_id}/
to strictly isolate session cookies between Hermes universes.
when using path-based proxying.
"""
target_path = f"/u/{universe_id}/"
if re.search(r'(?i)\bpath=[^;]*', cookie_header):
@@ -83,9 +83,15 @@ async def proxy_request(
for key, value in request.headers.items():
if key.lower() not in HOP_BY_HOP_HEADERS and key.lower() != "host":
req_headers[key] = value
# Forward original host header info
req_headers["x-forwarded-host"] = request.headers.get("host", "")
req_headers["x-forwarded-proto"] = request.url.scheme or "http"
body = await request.body()
is_path_proxied = universe_id and request.url.path.startswith(f"/u/{universe_id}")
try:
upstream_req = client.build_request(
method=request.method,
@@ -107,12 +113,12 @@ async def proxy_request(
if k_str in HOP_BY_HOP_HEADERS:
continue
if k_str == "set-cookie" and universe_id:
# Cloisonnement strict des cookies de session par univers
if k_str == "set-cookie" and is_path_proxied:
# Path-based proxying: rewrite cookie path
v_str = rewrite_cookie_path(v_str, universe_id)
raw_headers.append((b"set-cookie", v_str.encode("latin-1")))
elif k_str == "location" and universe_id:
# Réécriture de la redirection si le backend renvoie vers la racine /
elif k_str == "location" and is_path_proxied:
# Path-based proxying: rewrite location redirect
if v_str.startswith("/") and not v_str.startswith(f"/u/{universe_id}"):
v_str = f"/u/{universe_id}{v_str}"
raw_headers.append((b"location", v_str.encode("latin-1")))