新增了Alexa渠道的所有核心功能模块,包括配置管理、请求校验、会话管理、安全验证、去重、响应格式化、渐进式响应、主动推送、提醒功能、配对绑定以及Webhook端点,并完成了插件注册和配置项定义。
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
import logging
|
|
import time
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_DEFAULT_TOKEN_CACHE_TTL = 300
|
|
|
|
|
|
class AlexaSecurity:
|
|
|
|
VALID_POLICIES = {"open", "pairing", "allowlist", "disabled"}
|
|
|
|
def __init__(self, account):
|
|
self._account = account
|
|
self._policy = account.dm_policy if account.dm_policy in self.VALID_POLICIES else "open"
|
|
self._allowlist: set[str] = set(account.allow_from) if account.allow_from else set()
|
|
self._account_linking_enabled = getattr(account, "account_linking_enabled", False)
|
|
self._oauth_client_id = getattr(account, "oauth_client_id", "")
|
|
self._oauth_client_secret = getattr(account, "oauth_client_secret", "")
|
|
self._oauth_introspect_url = getattr(account, "oauth_introspect_url", "")
|
|
self._token_cache: dict[str, dict] = {}
|
|
self._http = httpx.Client(timeout=httpx.Timeout(10.0))
|
|
|
|
@property
|
|
def account_linking_enabled(self) -> bool:
|
|
return self._account_linking_enabled
|
|
|
|
def resolve_dm_policy(self) -> str:
|
|
return self._policy
|
|
|
|
def check_allowlist(self, user_id: str) -> bool:
|
|
if self._policy == "open":
|
|
return True
|
|
if self._policy == "disabled":
|
|
return False
|
|
if not self._allowlist:
|
|
return False
|
|
return user_id in self._allowlist
|
|
|
|
def add_to_allowlist(self, user_id: str):
|
|
self._allowlist.add(user_id)
|
|
|
|
def remove_from_allowlist(self, user_id: str):
|
|
self._allowlist.discard(user_id)
|
|
|
|
def verify_access_token(self, access_token: str) -> dict:
|
|
if not access_token:
|
|
return {"user_id": None, "verified": False}
|
|
|
|
cached = self._token_cache.get(access_token)
|
|
if cached and cached.get("expires_at", 0) > time.time():
|
|
return cached
|
|
|
|
result = self._do_verify(access_token)
|
|
result["expires_at"] = result.get("expires_at", time.time() + _DEFAULT_TOKEN_CACHE_TTL)
|
|
self._token_cache[access_token] = result
|
|
return result
|
|
|
|
def _do_verify(self, access_token: str) -> dict:
|
|
if self._oauth_introspect_url:
|
|
return self._verify_via_introspect(access_token)
|
|
return self._verify_via_jwt(access_token)
|
|
|
|
def _verify_via_introspect(self, access_token: str) -> dict:
|
|
try:
|
|
resp = self._http.post(
|
|
self._oauth_introspect_url,
|
|
json={"token": access_token},
|
|
headers={"Authorization": f"Bearer {self._oauth_client_secret}"},
|
|
)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
return {
|
|
"user_id": data.get("sub") or data.get("user_id"),
|
|
"verified": data.get("active", False),
|
|
}
|
|
logger.warning("OAuth introspect returned %d", resp.status_code)
|
|
except Exception:
|
|
logger.exception("OAuth token introspection failed")
|
|
return {"user_id": None, "verified": False}
|
|
|
|
def _verify_via_jwt(self, access_token: str) -> dict:
|
|
try:
|
|
from server.utils.auth_utils import AuthUtils
|
|
|
|
payload = AuthUtils.verify_access_token(access_token)
|
|
user_id = payload.get("sub")
|
|
if user_id:
|
|
return {"user_id": user_id, "verified": True}
|
|
except Exception:
|
|
logger.exception("JWT token verification failed")
|
|
|
|
return {"user_id": None, "verified": False}
|
|
|
|
def close(self):
|
|
if self._http:
|
|
self._http.close() |