# -*- coding: utf-8 -*- """音频直链多重防护(HMAC 签名 + 时效 + 代理流)——框架无关。 自 main.py 原样迁出,唯一改动是把密钥来源从 Flask 的 `app.secret_key` 改为**启动时注入**(`init_secret_key`),从而解除对本模块对 Flask app 的依赖; 签名/校验算法与口径保持逐字一致,迁移期新旧应用共用同一密钥即可互认 token。 """ import base64 import hashlib import hmac import secrets import time from typing import Any, Optional from core.config import AUDIO_HOST_MARKS, AUDIO_TOKEN_TTL, AUDIO_URL_KEYS # 由应用启动时注入(来源:auth_system.get_secret_key() → app.secret_key) _SECRET_KEY: bytes = b'' def init_secret_key(key) -> None: """注入签名密钥(str 或 bytes)。须在生成/校验音频 token 前调用一次。""" global _SECRET_KEY _SECRET_KEY = key.encode('utf-8') if isinstance(key, str) else key def is_audio_url(v) -> bool: return isinstance(v, str) and v.startswith('http') and any(m in v for m in AUDIO_HOST_MARKS) def sign_payload(payload: str) -> str: return hmac.new(_SECRET_KEY, payload.encode('utf-8'), hashlib.sha256).hexdigest()[:32] def make_audio_token(url: str) -> str: exp = int(time.time()) + AUDIO_TOKEN_TTL nonce = secrets.token_hex(4) payload = base64.urlsafe_b64encode(f'{exp}|{nonce}|{url}'.encode('utf-8')).decode().rstrip('=') return payload + '.' + sign_payload(payload) def verify_audio_token(token: str) -> Optional[str]: try: payload, sig = token.rsplit('.', 1) if not hmac.compare_digest(sig, sign_payload(payload)): return None pad = '=' * (-len(payload) % 4) exp, _nonce, url = base64.urlsafe_b64decode(payload + pad).decode('utf-8').split('|', 2) if int(exp) < time.time(): return None return url except Exception: return None def protect_audio_urls(obj: Any) -> bool: """递归就地替换 dict/list 中的音频直链为签名代理地址,返回是否发生替换""" hit = {'v': False} def walk(o): if isinstance(o, dict): for k, v in list(o.items()): if k in AUDIO_URL_KEYS and is_audio_url(v): o[k] = '/audio_stream?token=' + make_audio_token(v) hit['v'] = True else: walk(v) elif isinstance(o, list): for it in o: walk(it) walk(obj) return hit['v']