feat: implement native subdomain routing (*.hub.yesminedor.tn) for seamless absolute assets resolution across all universes
This commit is contained in:
+50
-1
@@ -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
@@ -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")))
|
||||
|
||||
Reference in New Issue
Block a user