88 lines
2.8 KiB
Python
88 lines
2.8 KiB
Python
"""iframe 嵌入登录(LocalNav 代签 + /embed-auth 写 SameSite=None Cookie)。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import os
|
|
import time
|
|
from secrets import compare_digest
|
|
|
|
EMBED_BOOTSTRAP_TTL_SEC = int(os.getenv("NAV_EMBED_BOOTSTRAP_TTL_SEC", "120"))
|
|
|
|
|
|
def _secret(explicit: str | None = None) -> bytes:
|
|
raw = (explicit or os.getenv("NAV_SESSION_SECRET") or "").strip()
|
|
if not raw:
|
|
raw = "gate-scout-nav-embed-insecure"
|
|
return raw.encode("utf-8")
|
|
|
|
|
|
def _b64url_encode(data: bytes) -> str:
|
|
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
|
|
|
|
|
|
def _b64url_decode(text: str) -> bytes:
|
|
pad = "=" * (-len(text) % 4)
|
|
return base64.urlsafe_b64decode(text + pad)
|
|
|
|
|
|
def create_embed_bootstrap_token(username: str, *, secret: str | None = None) -> str:
|
|
"""短效 token,供 /embed-auth 在 iframe 内写入 session。"""
|
|
payload = {
|
|
"kind": "embed",
|
|
"exp": int(time.time()) + max(30, EMBED_BOOTSTRAP_TTL_SEC),
|
|
"u": (username or "admin").strip(),
|
|
}
|
|
body = _b64url_encode(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
|
|
sig = hmac.new(_secret(secret), body.encode("ascii"), hashlib.sha256).hexdigest()
|
|
return f"{body}.{sig}"
|
|
|
|
|
|
def validate_embed_bootstrap_token(token: str | None, *, secret: str | None = None) -> tuple[bool, str]:
|
|
raw = (token or "").strip()
|
|
if not raw or "." not in raw:
|
|
return False, ""
|
|
body, sig = raw.rsplit(".", 1)
|
|
try:
|
|
expect = hmac.new(_secret(secret), body.encode("ascii"), hashlib.sha256).hexdigest()
|
|
if not compare_digest(expect, sig):
|
|
return False, ""
|
|
payload = json.loads(_b64url_decode(body))
|
|
except Exception:
|
|
return False, ""
|
|
if not isinstance(payload, dict) or payload.get("kind") != "embed":
|
|
return False, ""
|
|
if int(payload.get("exp") or 0) <= int(time.time()):
|
|
return False, ""
|
|
return True, str(payload.get("u") or "admin").strip()
|
|
|
|
|
|
def safe_next_path(raw: str | None) -> str:
|
|
p = (raw or "/dashboard").strip() or "/dashboard"
|
|
if not p.startswith("/") or p.startswith("//") or "://" in p:
|
|
return "/dashboard"
|
|
return p
|
|
|
|
|
|
def request_is_https(request) -> bool:
|
|
proto = (
|
|
(request.headers.get("x-forwarded-proto") or getattr(request.url, "scheme", "http") or "http")
|
|
.split(",")[0]
|
|
.strip()
|
|
.lower()
|
|
)
|
|
return proto == "https"
|
|
|
|
|
|
def nav_embed_session_active() -> bool:
|
|
raw = (os.getenv("NAV_EMBED_SESSION") or "auto").strip().lower()
|
|
if raw in ("1", "true", "yes", "on"):
|
|
return True
|
|
if raw in ("0", "false", "no", "off"):
|
|
return False
|
|
origins = (os.getenv("NAV_EMBED_ORIGINS") or "").strip()
|
|
return bool(origins and origins != "*")
|