uploader-bot/app/core/network/node_client.py

486 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Клиент для межузлового общения с ed25519 подписями
"""
import asyncio
import json
import aiohttp
from typing import Dict, Any, Optional, List
from datetime import datetime
from urllib.parse import urljoin
from app.core.crypto import get_ed25519_manager
from app.core.logging import get_logger
logger = get_logger(__name__)
class NodeClient:
"""Клиент для подписанного межузлового общения"""
def __init__(self, timeout: int = 30):
self.timeout = aiohttp.ClientTimeout(total=timeout)
self.session: Optional[aiohttp.ClientSession] = None
async def __aenter__(self):
"""Async context manager entry"""
self.session = aiohttp.ClientSession(timeout=self.timeout)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit"""
if self.session:
await self.session.close()
async def _create_signed_request(
self,
action: str,
data: Dict[str, Any],
target_url: str
) -> Dict[str, Any]:
"""
Создать подписанный запрос для межузлового общения
Args:
action: Тип действия (handshake, content_sync, ping, etc.)
data: Данные сообщения
target_url: URL целевой ноды
Returns:
Заголовки и тело запроса
"""
crypto_manager = get_ed25519_manager()
# Создаем сообщение
message = {
"action": action,
"timestamp": datetime.utcnow().isoformat(),
**data
}
# Подписываем сообщение
signature = crypto_manager.sign_message(message)
# Создаем заголовки
headers = {
"Content-Type": "application/json",
"X-Node-Communication": "true",
"X-Node-ID": crypto_manager.node_id,
"X-Node-Public-Key": crypto_manager.public_key_hex,
"X-Node-Signature": signature
}
return {
"headers": headers,
"json": message
}
async def send_handshake(
self,
target_url: str,
our_node_info: Dict[str, Any]
) -> Dict[str, Any]:
"""
Отправить хэндшейк ноде
Args:
target_url: URL целевой ноды (например, "http://node.example.com:8000")
our_node_info: Информация о нашей ноде
Returns:
Ответ от ноды или информация об ошибке
"""
endpoint_url = urljoin(target_url, "/api/node/handshake")
try:
request_data = await self._create_signed_request(
"handshake",
{"node_info": our_node_info},
target_url
)
logger.info(f"Sending handshake to {target_url}")
async with self.session.post(endpoint_url, **request_data) as response:
response_data = await response.json()
if response.status == 200:
logger.info(f"Handshake successful with {target_url}")
return {
"success": True,
"data": response_data,
"node_url": target_url
}
else:
logger.warning(f"Handshake failed with {target_url}: {response.status}")
return {
"success": False,
"error": f"HTTP {response.status}",
"data": response_data,
"node_url": target_url
}
except asyncio.TimeoutError:
logger.warning(f"Handshake timeout with {target_url}")
return {
"success": False,
"error": "timeout",
"node_url": target_url
}
except Exception as e:
logger.error(f"Handshake error with {target_url}: {e}")
return {
"success": False,
"error": str(e),
"node_url": target_url
}
async def send_content_sync(
self,
target_url: str,
sync_type: str,
content_info: Dict[str, Any]
) -> Dict[str, Any]:
"""
Отправить запрос синхронизации контента
Args:
target_url: URL целевой ноды
sync_type: Тип синхронизации (new_content, content_list, content_request)
content_info: Информация о контенте
Returns:
Ответ от ноды
"""
endpoint_url = urljoin(target_url, "/api/node/content/sync")
try:
request_data = await self._create_signed_request(
"content_sync",
{
"sync_type": sync_type,
"content_info": content_info
},
target_url
)
logger.info(f"Sending content sync ({sync_type}) to {target_url}")
async with self.session.post(endpoint_url, **request_data) as response:
response_data = await response.json()
if response.status == 200:
logger.debug(f"Content sync successful with {target_url}")
return {
"success": True,
"data": response_data,
"node_url": target_url
}
else:
logger.warning(f"Content sync failed with {target_url}: {response.status}")
return {
"success": False,
"error": f"HTTP {response.status}",
"data": response_data,
"node_url": target_url
}
except Exception as e:
logger.error(f"Content sync error with {target_url}: {e}")
return {
"success": False,
"error": str(e),
"node_url": target_url
}
async def send_ping(self, target_url: str) -> Dict[str, Any]:
"""
Отправить пинг ноде
Args:
target_url: URL целевой ноды
Returns:
Ответ от ноды (pong)
"""
endpoint_url = urljoin(target_url, "/api/node/network/ping")
try:
request_data = await self._create_signed_request(
"ping",
{"data": {"test": True}},
target_url
)
start_time = datetime.utcnow()
async with self.session.post(endpoint_url, **request_data) as response:
end_time = datetime.utcnow()
duration = (end_time - start_time).total_seconds() * 1000 # ms
response_data = await response.json()
if response.status == 200:
return {
"success": True,
"data": response_data,
"latency_ms": round(duration, 2),
"node_url": target_url
}
else:
return {
"success": False,
"error": f"HTTP {response.status}",
"data": response_data,
"node_url": target_url
}
except Exception as e:
logger.error(f"Ping error with {target_url}: {e}")
return {
"success": False,
"error": str(e),
"node_url": target_url
}
async def get_node_status(self, target_url: str) -> Dict[str, Any]:
"""
Получить статус ноды (GET запрос без подписи)
Args:
target_url: URL целевой ноды
Returns:
Статус ноды
"""
endpoint_url = urljoin(target_url, "/api/node/network/status")
try:
async with self.session.get(endpoint_url) as response:
response_data = await response.json()
if response.status == 200:
return {
"success": True,
"data": response_data,
"node_url": target_url
}
else:
return {
"success": False,
"error": f"HTTP {response.status}",
"data": response_data,
"node_url": target_url
}
except Exception as e:
logger.error(f"Status request error with {target_url}: {e}")
return {
"success": False,
"error": str(e),
"node_url": target_url
}
async def send_discovery(
self,
target_url: str,
known_nodes: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""
Отправить запрос обнаружения нод
Args:
target_url: URL целевой ноды
known_nodes: Список известных нам нод
Returns:
Список нод от целевой ноды
"""
endpoint_url = urljoin(target_url, "/api/node/network/discover")
try:
request_data = await self._create_signed_request(
"discover",
{"known_nodes": known_nodes},
target_url
)
logger.info(f"Sending discovery request to {target_url}")
async with self.session.post(endpoint_url, **request_data) as response:
response_data = await response.json()
if response.status == 200:
logger.debug(f"Discovery successful with {target_url}")
return {
"success": True,
"data": response_data,
"node_url": target_url
}
else:
logger.warning(f"Discovery failed with {target_url}: {response.status}")
return {
"success": False,
"error": f"HTTP {response.status}",
"data": response_data,
"node_url": target_url
}
except Exception as e:
logger.error(f"Discovery error with {target_url}: {e}")
return {
"success": False,
"error": str(e),
"node_url": target_url
}
class NodeNetworkManager:
"""Менеджер для работы с сетью нод"""
def __init__(self):
self.known_nodes: List[str] = []
self.active_nodes: List[str] = []
async def discover_nodes(self, bootstrap_nodes: List[str]) -> List[str]:
"""
Обнаружить ноды в сети через bootstrap ноды
Args:
bootstrap_nodes: Список bootstrap нод для начального подключения
Returns:
Список обнаруженных активных нод
"""
discovered_nodes = set()
async with NodeClient() as client:
# Получить информацию о нашей ноде
crypto_manager = get_ed25519_manager()
our_node_info = {
"node_id": crypto_manager.node_id,
"version": "3.0.0",
"capabilities": [
"content_upload",
"content_sync",
"decentralized_filtering",
"ed25519_signatures"
],
"network_info": {
"public_key": crypto_manager.public_key_hex,
"protocol_version": "1.0"
}
}
# Попробовать подключиться к bootstrap нодам
for node_url in bootstrap_nodes:
try:
# Выполнить хэндшейк
handshake_result = await client.send_handshake(node_url, our_node_info)
if handshake_result["success"]:
discovered_nodes.add(node_url)
# Запросить список известных нод
discovery_result = await client.send_discovery(node_url, list(discovered_nodes))
if discovery_result["success"]:
# Добавить ноды из ответа
known_nodes = discovery_result["data"]["data"]["known_nodes"]
for node_info in known_nodes:
if "url" in node_info:
discovered_nodes.add(node_info["url"])
except Exception as e:
logger.warning(f"Failed to discover through {node_url}: {e}")
self.known_nodes = list(discovered_nodes)
return self.known_nodes
async def check_node_health(self, nodes: List[str]) -> Dict[str, Dict[str, Any]]:
"""
Проверить состояние нод
Args:
nodes: Список нод для проверки
Returns:
Словарь с результатами проверки для каждой ноды
"""
results = {}
async with NodeClient() as client:
# Создаем задачи для параллельной проверки
tasks = []
for node_url in nodes:
task = asyncio.create_task(client.send_ping(node_url))
tasks.append((node_url, task))
# Ждем завершения всех задач
for node_url, task in tasks:
try:
result = await task
results[node_url] = result
except Exception as e:
results[node_url] = {
"success": False,
"error": str(e),
"node_url": node_url
}
# Обновляем список активных нод
self.active_nodes = [
node_url for node_url, result in results.items()
if result.get("success", False)
]
return results
async def broadcast_content(
self,
content_info: Dict[str, Any],
target_nodes: Optional[List[str]] = None
) -> Dict[str, Dict[str, Any]]:
"""
Транслировать информацию о новом контенте всем активным нодам
Args:
content_info: Информация о контенте
target_nodes: Список целевых нод (по умолчанию все активные)
Returns:
Результаты трансляции для каждой ноды
"""
nodes = target_nodes or self.active_nodes
results = {}
async with NodeClient() as client:
# Создаем задачи для параллельной отправки
tasks = []
for node_url in nodes:
task = asyncio.create_task(
client.send_content_sync(node_url, "new_content", content_info)
)
tasks.append((node_url, task))
# Ждем завершения всех задач
for node_url, task in tasks:
try:
result = await task
results[node_url] = result
except Exception as e:
results[node_url] = {
"success": False,
"error": str(e),
"node_url": node_url
}
return results
# Глобальный экземпляр менеджера сети
network_manager = NodeNetworkManager()
async def get_network_manager() -> NodeNetworkManager:
"""Получить глобальный экземпляр менеджера сети"""
return network_manager