fixes global
This commit is contained in:
1 parent
13dc4f39c8
commit
cad0f6aebe
64 files changed
+10379
-254
No files matched your search
@@ -0,0 +1,169 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, Optional, Tuple, Callable
|
||||
|
||||
from app.core._blockchain.ton.nft_license_manager import NFTLicenseManager
|
||||
from app.core.crypto.content_cipher import ContentCipher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StreamingToken:
|
||||
token: str
|
||||
content_id: str
|
||||
owner_address: str
|
||||
issued_at: float
|
||||
expires_at: float
|
||||
|
||||
def is_valid(self, now: Optional[float] = None) -> bool:
|
||||
now = now or time.time()
|
||||
return now < self.expires_at
|
||||
|
||||
|
||||
class ContentAccessManager:
|
||||
"""
|
||||
Управление доступом к зашифрованному контенту по NFT лицензиям в TON.
|
||||
|
||||
Обязанности:
|
||||
- grant_access(): принять tonProof + content_id, проверить лицензию, выдать временный токен
|
||||
- verify_access(): валидация токена при запросе стрима/скачивания
|
||||
- create_streaming_token(): генерация подписанного/непредсказуемого токена с TTL
|
||||
- stream/decrypt: интеграция с ContentCipher — расшифровка возможна только при валидной лицензии/токене
|
||||
"""
|
||||
|
||||
DEFAULT_TOKEN_TTL_SEC = int(os.getenv("STREAM_TOKEN_TTL_SEC", "600")) # 10 минут по умолчанию
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nft_manager: Optional[NFTLicenseManager] = None,
|
||||
cipher: Optional[ContentCipher] = None,
|
||||
):
|
||||
self.nft_manager = nft_manager or NFTLicenseManager()
|
||||
self.cipher = cipher or ContentCipher()
|
||||
# Простой in-memory storage токенов. Для продакшена стоит заменить на Redis или БД.
|
||||
self._tokens: Dict[str, StreamingToken] = {}
|
||||
logger.debug("ContentAccessManager initialized; token_ttl=%s", self.DEFAULT_TOKEN_TTL_SEC)
|
||||
|
||||
def create_streaming_token(self, content_id: str, owner_address: str, ttl_sec: Optional[int] = None) -> StreamingToken:
|
||||
ttl = ttl_sec or self.DEFAULT_TOKEN_TTL_SEC
|
||||
token = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode("ascii").rstrip("=")
|
||||
now = time.time()
|
||||
st = StreamingToken(
|
||||
token=token,
|
||||
content_id=content_id,
|
||||
owner_address=owner_address,
|
||||
issued_at=now,
|
||||
expires_at=now + ttl,
|
||||
)
|
||||
self._tokens[token] = st
|
||||
logger.info("Streaming token issued content_id=%s owner=%s ttl=%s", content_id, owner_address, ttl)
|
||||
return st
|
||||
|
||||
def verify_access(self, token: str, content_id: str) -> Tuple[bool, Optional[str], Optional[StreamingToken]]:
|
||||
if not token:
|
||||
return False, "Missing token", None
|
||||
st = self._tokens.get(token)
|
||||
if not st:
|
||||
return False, "Token not found", None
|
||||
if not st.is_valid():
|
||||
# Удаляем просроченный
|
||||
self._tokens.pop(token, None)
|
||||
return False, "Token expired", None
|
||||
if st.content_id != content_id:
|
||||
return False, "Token/content mismatch", None
|
||||
logger.debug("Streaming token verified for content_id=%s owner=%s", st.content_id, st.owner_address)
|
||||
return True, None, st
|
||||
|
||||
async def grant_access(
|
||||
self,
|
||||
ton_proof: Dict[str, Any],
|
||||
content_id: str,
|
||||
nft_address: Optional[str] = None,
|
||||
token_ttl_sec: Optional[int] = None,
|
||||
) -> Tuple[bool, Optional[str], Optional[Dict[str, Any]]]:
|
||||
"""
|
||||
Композитный сценарий: валидируем tonProof, проверяем владение NFT лицензией,
|
||||
создаем временный токен для стриминга.
|
||||
Возвращает: (ok, error, payload)
|
||||
payload: { token, expires_at, owner_address, nft_item }
|
||||
"""
|
||||
try:
|
||||
ok, err, nft_item = await self.nft_manager.check_license_validity(
|
||||
ton_proof=ton_proof,
|
||||
content_id=content_id,
|
||||
nft_address=nft_address,
|
||||
)
|
||||
if not ok:
|
||||
return False, err, None
|
||||
|
||||
owner_address = nft_proof_owner(ton_proof)
|
||||
token = self.create_streaming_token(content_id, owner_address, token_ttl_sec)
|
||||
payload = {
|
||||
"token": token.token,
|
||||
"expires_at": token.expires_at,
|
||||
"owner_address": token.owner_address,
|
||||
"nft_item": nft_item,
|
||||
}
|
||||
return True, None, payload
|
||||
except Exception as e:
|
||||
logger.exception("grant_access failed")
|
||||
return False, str(e), None
|
||||
|
||||
def decrypt_for_stream(
|
||||
self,
|
||||
encrypted_obj: Dict[str, Any],
|
||||
content_key_provider: Callable[[str], bytes],
|
||||
token: str,
|
||||
content_id: str,
|
||||
associated_data: Optional[bytes] = None,
|
||||
) -> Tuple[bool, Optional[str], Optional[bytes]]:
|
||||
"""
|
||||
Расшифровка данных для стрима. Требует валидного стрим-токена.
|
||||
content_key_provider(content_id) -> bytes (32)
|
||||
"""
|
||||
ok, err, st = self.verify_access(token, content_id)
|
||||
if not ok:
|
||||
return False, err, None
|
||||
|
||||
try:
|
||||
# В идеале проверяем целостность до расшифровки
|
||||
# Здесь можем опционально вызвать verify_content_integrity, если есть сигнатуры
|
||||
# Но основной критерий — валидный токен.
|
||||
key = content_key_provider(content_id)
|
||||
pt = self.cipher.decrypt_content(
|
||||
ciphertext_b64=encrypted_obj["ciphertext_b64"],
|
||||
nonce_b64=encrypted_obj["nonce_b64"],
|
||||
tag_b64=encrypted_obj["tag_b64"],
|
||||
key=key,
|
||||
associated_data=associated_data,
|
||||
)
|
||||
logger.info("Decryption for stream succeeded content_id=%s owner=%s", content_id, st.owner_address)
|
||||
return True, None, pt
|
||||
except Exception as e:
|
||||
logger.exception("decrypt_for_stream failed")
|
||||
return False, str(e), None
|
||||
|
||||
|
||||
def nft_proof_owner(ton_proof: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Извлечь адрес владельца из структуры tonProof запроса клиента.
|
||||
Совместимо с TonConnect unpack_wallet_info формой.
|
||||
"""
|
||||
# Поддержка как плоской формы, так и вложенной ton_proof
|
||||
if "address" in ton_proof:
|
||||
return ton_proof["address"]
|
||||
if "account" in ton_proof and ton_proof["account"] and "address" in ton_proof["account"]:
|
||||
return ton_proof["account"]["address"]
|
||||
if "ton_proof" in ton_proof and ton_proof["ton_proof"] and "address" in ton_proof["ton_proof"]:
|
||||
return ton_proof["ton_proof"]["address"]
|
||||
# В противном случае бросаем: пусть вызывающий слой отловит
|
||||
raise ValueError("Cannot extract owner address from ton_proof")
|
||||
Reference in new issue
Block a user