83 lines
2.6 KiB
Python
83 lines
2.6 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Per-request frontend base URL (same idea as auth emails: frontendUrl from the client).
|
|
|
|
Set from the incoming HTTP request (X-Frontend-Url, Origin, or Referer) in app middleware.
|
|
Read when building billing alert links during that request (e.g. pool exhausted email).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
from contextvars import ContextVar
|
|
from typing import Optional
|
|
from urllib.parse import urlparse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_CURRENT_FRONTEND_URL: ContextVar[str] = ContextVar("request_frontend_url", default="")
|
|
|
|
_CORS_ORIGIN_REGEX = re.compile(
|
|
r"^https://.*\.(poweron\.swiss|poweron-center\.net)$",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def setRequestFrontendUrl(url: str) -> None:
|
|
"""Store the frontend base URL for the current request (no trailing slash)."""
|
|
_CURRENT_FRONTEND_URL.set((url or "").strip().rstrip("/"))
|
|
|
|
|
|
def getRequestFrontendUrl() -> str:
|
|
"""Return the frontend base URL for the current request, or empty if unset."""
|
|
return _CURRENT_FRONTEND_URL.get()
|
|
|
|
|
|
def _allowedOrigins() -> list[str]:
|
|
try:
|
|
from modules.shared.configuration import APP_CONFIG
|
|
|
|
raw = APP_CONFIG.get("APP_ALLOWED_ORIGINS") or ""
|
|
return [o.strip().rstrip("/") for o in raw.split(",") if o.strip()]
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def _isAllowedFrontendOrigin(origin: str) -> bool:
|
|
if not origin:
|
|
return False
|
|
normalized = origin.strip().rstrip("/")
|
|
if normalized in _allowedOrigins():
|
|
return True
|
|
return bool(_CORS_ORIGIN_REGEX.match(normalized))
|
|
|
|
|
|
def resolveFrontendUrlFromRequest(request) -> str:
|
|
"""
|
|
Resolve frontend base URL from the current HTTP request.
|
|
|
|
Priority: X-Frontend-Url (explicit, like auth body frontendUrl) → Origin → Referer origin.
|
|
Only returns origins that match APP_ALLOWED_ORIGINS or the CORS subdomain regex.
|
|
"""
|
|
explicit = (request.headers.get("X-Frontend-Url") or "").strip().rstrip("/")
|
|
if explicit and _isAllowedFrontendOrigin(explicit):
|
|
return explicit
|
|
if explicit:
|
|
logger.debug("Ignoring X-Frontend-Url not in allowed origins: %s", explicit)
|
|
|
|
origin = (request.headers.get("Origin") or "").strip().rstrip("/")
|
|
if origin and _isAllowedFrontendOrigin(origin):
|
|
return origin
|
|
|
|
referer = (request.headers.get("Referer") or "").strip()
|
|
if referer:
|
|
try:
|
|
ref_origin = f"{urlparse(referer).scheme}://{urlparse(referer).netloc}".rstrip("/")
|
|
if ref_origin and _isAllowedFrontendOrigin(ref_origin):
|
|
return ref_origin
|
|
except Exception:
|
|
pass
|
|
|
|
return ""
|