Files
remnawave-bedolaga-telegram…/app/webapi/routes/websocket.py
Fringg 1f0fef114b refactor: complete structlog migration with contextvars, kwargs, and logging hardening
- Add ContextVarsMiddleware for automatic user_id/chat_id/username binding
  via structlog contextvars (aiogram) and http_method/http_path (FastAPI)
- Use bound_contextvars() context manager instead of clear_contextvars()
  to safely restore previous state instead of wiping all context
- Register ContextVarsMiddleware as outermost middleware (before GlobalError)
  so all error logs include user context
- Replace structlog.get_logger() with structlog.get_logger(__name__) across
  270 calls in 265 files for meaningful logger names
- Switch wrapper_class from BoundLogger to make_filtering_bound_logger()
  for pre-processor level filtering (performance optimization)
- Migrate 1411 %-style positional arg logger calls to structlog kwargs
  style across 161 files via AST script
- Migrate log_rotation_service.py from stdlib logging to structlog
- Add payment module prefixes to TelegramNotifierProcessor.IGNORED_LOGGER_PREFIXES
  and ExcludePaymentFilter.PAYMENT_MODULES to prevent payment data leaking
  to Telegram notifications and general log files
- Fix LoggingMiddleware: add from_user null-safety for channel posts,
  switch time.time() to time.monotonic() for duration measurement
- Remove duplicate logger assignments in purchase.py, config.py,
  inline.py, and admin/payments.py
2026-02-16 09:18:12 +03:00

119 lines
4.6 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.

from __future__ import annotations
import json
import structlog
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from fastapi.security import APIKeyHeader
from app.database.database import AsyncSessionLocal
from app.services.event_emitter import event_emitter
from app.services.web_api_token_service import web_api_token_service
logger = structlog.get_logger(__name__)
router = APIRouter()
api_key_header_scheme = APIKeyHeader(name='X-API-Key', auto_error=False)
async def verify_websocket_token(
websocket: WebSocket,
token: str | None = None,
) -> bool:
"""Проверить токен для WebSocket подключения."""
if not token:
# Пытаемся получить токен из query параметров
token = websocket.query_params.get('token') or websocket.query_params.get('api_key')
if not token:
return False
async with AsyncSessionLocal() as db:
try:
webhook_token = await web_api_token_service.authenticate(
db,
token,
remote_ip=websocket.client.host if websocket.client else None,
)
if webhook_token:
logger.debug('WebSocket token authenticated successfully')
else:
logger.warning('WebSocket token authentication failed: token not found or invalid')
return webhook_token is not None
except Exception as error:
logger.warning('WebSocket authentication error', error=error, exc_info=True)
return False
@router.websocket('/ws')
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket endpoint для real-time обновлений."""
client_host = websocket.client.host if websocket.client else 'unknown'
logger.debug('WebSocket connection attempt from', client_host=client_host)
# Сначала проверяем авторизацию ДО принятия соединения
token = websocket.query_params.get('token') or websocket.query_params.get('api_key')
if not token:
logger.debug('WebSocket: No token provided from', client_host=client_host)
# Принимаем и сразу закрываем с кодом ошибки
await websocket.accept()
await websocket.close(code=1008, reason='Unauthorized: No token provided')
return
if not await verify_websocket_token(websocket, token):
logger.debug('WebSocket: Invalid token from', client_host=client_host)
# Принимаем и сразу закрываем с кодом ошибки
await websocket.accept()
await websocket.close(code=1008, reason='Unauthorized: Invalid token')
return
# Только после успешной проверки принимаем соединение
try:
await websocket.accept()
logger.debug('WebSocket connection accepted from', client_host=client_host)
except Exception as e:
logger.error('WebSocket: Failed to accept connection from', client_host=client_host, e=e)
return
# Регистрируем подключение
event_emitter.register_websocket(websocket)
try:
# Отправляем приветственное сообщение
await websocket.send_json(
{
'type': 'connection',
'status': 'connected',
'message': 'WebSocket connection established',
}
)
# Обрабатываем входящие сообщения (ping/pong для keepalive)
while True:
try:
data = await websocket.receive_text()
message = json.loads(data)
# Обработка ping
if message.get('type') == 'ping':
await websocket.send_json({'type': 'pong'})
# Можно добавить другие типы сообщений (подписки на конкретные события и т.д.)
except json.JSONDecodeError:
logger.warning('Invalid JSON received from WebSocket client')
except WebSocketDisconnect:
break
except Exception as error:
logger.exception('Error processing WebSocket message', error=error)
except WebSocketDisconnect:
logger.debug('WebSocket client disconnected')
except Exception as error:
logger.exception('WebSocket error', error=error)
finally:
# Отменяем регистрацию при отключении
event_emitter.unregister_websocket(websocket)