94 lines
2.8 KiB
Python
94 lines
2.8 KiB
Python
"""允许本地导航(LocalNav)iframe 内嵌本服务。环境变量 NAV_ALLOW_EMBED / NAV_EMBED_ORIGINS。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import re
|
||
|
||
from nav_session_auth import nav_embed_session_active, request_is_https
|
||
|
||
|
||
def nav_embed_allowed() -> bool:
|
||
return (os.getenv("NAV_ALLOW_EMBED") or "true").strip().lower() in (
|
||
"1",
|
||
"true",
|
||
"yes",
|
||
"on",
|
||
)
|
||
|
||
|
||
def nav_embed_origins() -> str:
|
||
return (os.getenv("NAV_EMBED_ORIGINS") or "*").strip() or "*"
|
||
|
||
|
||
def nav_session_middleware_kwargs() -> dict:
|
||
"""跨站 iframe:SameSite=None;https_only 保持 False,由响应 patch 补 Secure。"""
|
||
if not nav_embed_session_active():
|
||
return {"same_site": "lax", "https_only": False}
|
||
return {"same_site": "none", "https_only": False}
|
||
|
||
|
||
def install_proxy_headers(app) -> None:
|
||
if (os.getenv("NAV_TRUST_PROXY") or "").strip().lower() not in (
|
||
"1",
|
||
"true",
|
||
"yes",
|
||
"on",
|
||
):
|
||
return
|
||
try:
|
||
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
||
|
||
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def install_nav_embed(app) -> None:
|
||
if not nav_embed_allowed():
|
||
return
|
||
origins = nav_embed_origins()
|
||
install_proxy_headers(app)
|
||
|
||
@app.middleware("http")
|
||
async def _nav_embed_frame_headers(request, call_next):
|
||
response = await call_next(request)
|
||
if origins == "*":
|
||
response.headers["Content-Security-Policy"] = "frame-ancestors *"
|
||
else:
|
||
parts = " ".join(o.strip() for o in origins.split(",") if o.strip())
|
||
response.headers["Content-Security-Policy"] = f"frame-ancestors 'self' {parts}"
|
||
if nav_embed_session_active():
|
||
_patch_set_cookie_for_embed(response, request)
|
||
return response
|
||
|
||
|
||
def _patch_set_cookie_for_embed(response, request) -> None:
|
||
"""HTTPS 访问时强制 session Cookie 为 SameSite=None; Secure。"""
|
||
force = (os.getenv("NAV_EMBED_FORCE_SECURE") or "1").strip().lower() in (
|
||
"1",
|
||
"true",
|
||
"yes",
|
||
"on",
|
||
)
|
||
if not request_is_https(request) and not force:
|
||
return
|
||
raw = response.headers.get("set-cookie")
|
||
if not raw:
|
||
return
|
||
|
||
def _fix_one(part: str) -> str:
|
||
p = part.strip()
|
||
if not p or "session=" not in p.lower():
|
||
return p
|
||
p = re.sub(r";\s*SameSite=[^;]*", "", p, flags=re.I)
|
||
p = re.sub(r";\s*Secure(?=;|$)", "", p, flags=re.I)
|
||
p += "; Secure; SameSite=none"
|
||
return p
|
||
|
||
if "," in raw and "session=" in raw.lower():
|
||
parts = re.split(r",(?=\s*[^;,]+=)", raw)
|
||
response.headers["set-cookie"] = ", ".join(_fix_one(x) for x in parts)
|
||
else:
|
||
response.headers["set-cookie"] = _fix_one(raw)
|