对接中控:只读持仓快照 API、SSO 打开链与 iframe 祖先配置。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"""中控 Bridge 鉴权与 SSO(短期登录链)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
|
||||
def hub_bridge_token() -> str:
|
||||
return (os.getenv("HUB_BRIDGE_TOKEN") or "").strip()
|
||||
|
||||
|
||||
def extract_hub_token_from_request(headers) -> str:
|
||||
raw = (headers.get("X-Hub-Token") or "").strip()
|
||||
if raw:
|
||||
return raw
|
||||
auth = (headers.get("Authorization") or "").strip()
|
||||
if auth.lower().startswith("bearer "):
|
||||
return auth[7:].strip()
|
||||
return ""
|
||||
|
||||
|
||||
def hub_bridge_token_ok(headers) -> bool:
|
||||
expected = hub_bridge_token()
|
||||
if not expected:
|
||||
return False
|
||||
got = extract_hub_token_from_request(headers)
|
||||
if not got:
|
||||
return False
|
||||
return hmac.compare_digest(got, expected)
|
||||
|
||||
|
||||
def make_sso_token(bridge_token: str, *, ttl_sec: int = 120) -> str:
|
||||
"""exp.hex_hmac — 中控与客户端共用 bridge_token 签名."""
|
||||
exp = int(time.time()) + max(30, int(ttl_sec))
|
||||
payload = str(exp)
|
||||
sig = hmac.new(
|
||||
bridge_token.encode("utf-8"),
|
||||
payload.encode("utf-8"),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
return f"{payload}.{sig}"
|
||||
|
||||
|
||||
def verify_sso_token(token: str, bridge_token: Optional[str] = None) -> bool:
|
||||
expected = (bridge_token if bridge_token is not None else hub_bridge_token()).strip()
|
||||
raw = (token or "").strip()
|
||||
if not expected or not raw or "." not in raw:
|
||||
return False
|
||||
payload, sig = raw.rsplit(".", 1)
|
||||
try:
|
||||
exp = int(payload)
|
||||
except ValueError:
|
||||
return False
|
||||
if exp < int(time.time()):
|
||||
return False
|
||||
want = hmac.new(
|
||||
expected.encode("utf-8"),
|
||||
payload.encode("utf-8"),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
return hmac.compare_digest(sig, want)
|
||||
|
||||
|
||||
def build_embed_open_url(
|
||||
base_url: str,
|
||||
*,
|
||||
bridge_token: str,
|
||||
embed_path: str = "/embed",
|
||||
ttl_sec: int = 120,
|
||||
) -> str:
|
||||
"""中控生成带 SSO 的客户端打开地址."""
|
||||
root = (base_url or "").rstrip("/")
|
||||
path = embed_path or "/embed"
|
||||
if not path.startswith("/"):
|
||||
path = "/" + path
|
||||
token = make_sso_token(bridge_token, ttl_sec=ttl_sec)
|
||||
q = urlencode({"token": token, "next": path})
|
||||
return f"{root}/hub_sso?{q}"
|
||||
|
||||
|
||||
def frame_ancestors_header_value() -> Optional[str]:
|
||||
"""
|
||||
HUB_EMBED_FRAME_ANCESTORS: 空格分隔 Origin,如 https://hub.local http://192.168.1.10:5100
|
||||
未配置则不改响应头(保持现状);配 * 则允许任意父页(仅内网慎用).
|
||||
"""
|
||||
raw = (os.getenv("HUB_EMBED_FRAME_ANCESTORS") or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
if raw == "*":
|
||||
return "*"
|
||||
parts = [p.strip() for p in raw.split() if p.strip()]
|
||||
return " ".join(parts) if parts else None
|
||||
Reference in New Issue
Block a user