From 39750851b0b42fede3bf3b192b251bc8a3788674 Mon Sep 17 00:00:00 2001 From: bruno Date: Wed, 28 Oct 2026 12:13:03 -0400 Subject: [PATCH] =?UTF-8?q?Recherche=20partout=20(#68)=20:=20CORS=20r?= =?UTF-8?q?=C3=A9solue=20avec=20l'h=C3=B4te=20de=20la=20requ=C3=AAte=20(or?= =?UTF-8?q?dre=20middlewares)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/recherche.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/app/recherche.py b/app/recherche.py index e3ff103..381c520 100644 --- a/app/recherche.py +++ b/app/recherche.py @@ -66,18 +66,24 @@ def origin_of(url: str | None) -> str | None: return f"{parsed.scheme}://{host}{port}" -def sister_origins(resolved: dict | None = None) -> frozenset: +def sister_origins( + resolved: dict | None = None, request_host: str | None = None +) -> frozenset: """Origines autorisées pour la lecture cross-origin de /api/search. ``resolved`` : dictionnaire ``draco_urls`` rendu par ``render()``. - Sans argument, le résolveur est appelé (import paresseux) ; en cas - d'échec, repli sur prod + dev officiels (premier parti uniquement). + Sans argument, le résolveur est appelé (import paresseux) avec + l'hôte de la requête quand il est connu — ce middleware s'exécute + avant tout poseur de contexte d'hôte, et sans lui la résolution + retomberait sur la configuration seule et raterait l'environnement + ``-dev`` ; en cas d'échec, repli sur prod + dev officiels + (premier parti uniquement). """ if resolved is None: try: from app.config import get_settings, resoudre_urls_draco - urls = resoudre_urls_draco(base=get_settings()) + urls = resoudre_urls_draco(hote_requete=request_host, base=get_settings()) resolved = { "obsigate": urls.sso_url, "flowdeck": urls.flowdeck_url, @@ -94,11 +100,13 @@ def sister_origins(resolved: dict | None = None) -> frozenset: return frozenset(origins) -def cors_headers_for(origin: str | None, resolved: dict | None = None) -> dict | None: +def cors_headers_for( + origin: str | None, resolved: dict | None = None, request_host: str | None = None +) -> dict | None: """En-têtes CORS d'une réponse de recherche, ou None si refusée.""" if not origin: return None - if origin.strip() not in sister_origins(resolved): + if origin.strip() not in sister_origins(resolved, request_host): return None return { "Access-Control-Allow-Origin": origin.strip(), @@ -107,9 +115,11 @@ def cors_headers_for(origin: str | None, resolved: dict | None = None) -> dict | } -def preflight_headers(origin: str | None, resolved: dict | None = None) -> dict | None: +def preflight_headers( + origin: str | None, resolved: dict | None = None, request_host: str | None = None +) -> dict | None: """En-têtes d'une réponse de preflight OPTIONS pour /api/search.""" - headers = cors_headers_for(origin, resolved) + headers = cors_headers_for(origin, resolved, request_host) if headers is None: return None headers.update({ @@ -176,8 +186,11 @@ class RechercheCorsMiddleware: for k, v in scope.get("headers", []) } origin = headers_in.get("origin", "") + request_host = ( + headers_in.get("host", "") or headers_in.get("x-forwarded-host", "") + ).split(",")[0].strip() if path == SEARCH_PATH and scope.get("method") == "OPTIONS": - preflight = preflight_headers(origin) + preflight = preflight_headers(origin, request_host=request_host) if preflight is not None: body = b"" headers_out = [(k.lower().encode("latin-1"), v.encode("latin-1")) @@ -193,7 +206,7 @@ class RechercheCorsMiddleware: async def send_with_cors(message): if message["type"] == "http.response.start": - cors = cors_headers_for(origin) + cors = cors_headers_for(origin, request_host=request_host) if cors is not None: message["headers"] = list(message.get("headers", [])) + [ (k.lower().encode("latin-1"), v.encode("latin-1"))