import asyncio import logging import json import hashlib import hmac import base64 from typing import Optional, Dict, Any from aiohttp import web from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.services.payment_service import PaymentService from app.database.database import get_db logger = logging.getLogger(__name__) class YooKassaWebhookHandler: @staticmethod def verify_webhook_signature(body: str, signature: str, secret: str) -> bool: try: signature_parts = signature.strip().split(' ') if len(signature_parts) < 4: logger.error(f"Неверный формат подписи YooKassa: {signature}") return False version = signature_parts[0] payment_id = signature_parts[1] timestamp = signature_parts[2] received_signature = signature_parts[3] if version != "v1": logger.error(f"Неподдерживаемая версия подписи: {version}") return False logger.info(f"Проверка подписи v1 для платежа {payment_id}, timestamp: {timestamp}") expected_signature_1 = hmac.new( secret.encode('utf-8'), body.encode('utf-8'), hashlib.sha256 ).digest() expected_signature_1_b64 = base64.b64encode(expected_signature_1).decode('utf-8') signed_payload_2 = f"{payment_id}.{timestamp}.{body}" expected_signature_2 = hmac.new( secret.encode('utf-8'), signed_payload_2.encode('utf-8'), hashlib.sha256 ).digest() expected_signature_2_b64 = base64.b64encode(expected_signature_2).decode('utf-8') signed_payload_3 = f"{timestamp}.{body}" expected_signature_3 = hmac.new( secret.encode('utf-8'), signed_payload_3.encode('utf-8'), hashlib.sha256 ).digest() expected_signature_3_b64 = base64.b64encode(expected_signature_3).decode('utf-8') logger.debug(f"Получена подпись: {received_signature}") logger.debug(f"Ожидаемая подпись (вариант 1): {expected_signature_1_b64}") logger.debug(f"Ожидаемая подпись (вариант 2): {expected_signature_2_b64}") logger.debug(f"Ожидаемая подпись (вариант 3): {expected_signature_3_b64}") is_valid = ( hmac.compare_digest(received_signature, expected_signature_1_b64) or hmac.compare_digest(received_signature, expected_signature_2_b64) or hmac.compare_digest(received_signature, expected_signature_3_b64) ) if is_valid: logger.info("✅ Подпись YooKassa webhook проверена успешно") else: logger.warning("⚠️ Подпись YooKassa webhook не совпадает ни с одним вариантом") return is_valid except Exception as e: logger.error(f"Ошибка проверки подписи YooKassa: {e}") return False def __init__(self, payment_service: PaymentService): self.payment_service = payment_service async def handle_webhook(self, request: web.Request) -> web.Response: try: logger.info(f"📥 Получен YooKassa webhook: {request.method} {request.path}") logger.info(f"📋 Headers: {dict(request.headers)}") body = await request.text() if not body: logger.warning("⚠️ Получен пустой webhook от YooKassa") return web.Response(status=400, text="Empty body") logger.info(f"📄 Body: {body}") signature = request.headers.get('Signature') or request.headers.get('X-YooKassa-Signature') if settings.YOOKASSA_WEBHOOK_SECRET and signature: logger.info(f"🔐 Получена подпись: {signature}") if not YooKassaWebhookHandler.verify_webhook_signature(body, signature, settings.YOOKASSA_WEBHOOK_SECRET): logger.warning("❌ Подпись не совпала, но продолжаем обработку (режим отладки)") else: logger.info("✅ Подпись webhook проверена успешно") elif settings.YOOKASSA_WEBHOOK_SECRET and not signature: logger.warning("⚠️ Webhook без подписи, но секрет настроен") elif signature and not settings.YOOKASSA_WEBHOOK_SECRET: logger.info("ℹ️ Подпись получена, но проверка отключена (YOOKASSA_WEBHOOK_SECRET не настроен)") else: logger.info("ℹ️ Проверка подписи отключена") try: webhook_data = json.loads(body) except json.JSONDecodeError as e: logger.error(f"❌ Ошибка парсинга JSON webhook YooKassa: {e}") return web.Response(status=400, text="Invalid JSON") logger.info(f"📊 Обработка webhook YooKassa: {webhook_data.get('event', 'unknown_event')}") logger.debug(f"🔍 Полные данные webhook: {webhook_data}") event_type = webhook_data.get("event") if not event_type: logger.warning("⚠️ Webhook YooKassa без типа события") return web.Response(status=400, text="No event type") if event_type not in ["payment.succeeded", "payment.waiting_for_capture"]: logger.info(f"ℹ️ Игнорируем событие YooKassa: {event_type}") return web.Response(status=200, text="OK") async for db in get_db(): try: success = await self.payment_service.process_yookassa_webhook(db, webhook_data) if success: logger.info(f"✅ Успешно обработан webhook YooKassa: {event_type}") return web.Response(status=200, text="OK") else: logger.error(f"❌ Ошибка обработки webhook YooKassa: {event_type}") return web.Response(status=500, text="Processing error") finally: await db.close() except Exception as e: logger.error(f"❌ Критическая ошибка обработки webhook YooKassa: {e}", exc_info=True) return web.Response(status=500, text="Internal server error") def setup_routes(self, app: web.Application) -> None: webhook_path = settings.YOOKASSA_WEBHOOK_PATH app.router.add_post(webhook_path, self.handle_webhook) app.router.add_get(webhook_path, self._get_handler) app.router.add_options(webhook_path, self._options_handler) logger.info(f"✅ Настроен YooKassa webhook на пути: POST {webhook_path}") async def _get_handler(self, request: web.Request) -> web.Response: return web.json_response({ "status": "ok", "message": "YooKassa webhook endpoint is working", "method": "GET", "path": request.path, "note": "Use POST method for actual webhooks" }) async def _options_handler(self, request: web.Request) -> web.Response: return web.Response( status=200, headers={ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, X-YooKassa-Signature', } ) def create_yookassa_webhook_app(payment_service: PaymentService) -> web.Application: app = web.Application() webhook_handler = YooKassaWebhookHandler(payment_service) webhook_handler.setup_routes(app) async def health_check(request): return web.json_response({ "status": "ok", "service": "yookassa_webhook", "port": settings.YOOKASSA_WEBHOOK_PORT, "path": settings.YOOKASSA_WEBHOOK_PATH, "enabled": settings.is_yookassa_enabled() }) app.router.add_get("/health", health_check) return app async def start_yookassa_webhook_server(payment_service: PaymentService) -> None: if not settings.is_yookassa_enabled(): logger.info("ℹ️ YooKassa отключена, webhook сервер не запускается") return try: app = create_yookassa_webhook_app(payment_service) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite( runner, host='0.0.0.0', port=settings.YOOKASSA_WEBHOOK_PORT ) await site.start() logger.info(f"✅ YooKassa webhook сервер запущен на порту {settings.YOOKASSA_WEBHOOK_PORT}") logger.info(f"🎯 YooKassa webhook URL: http://0.0.0.0:{settings.YOOKASSA_WEBHOOK_PORT}{settings.YOOKASSA_WEBHOOK_PATH}") try: while True: await asyncio.sleep(1) except asyncio.CancelledError: logger.info("🛑 YooKassa webhook сервер получил сигнал остановки") finally: await site.stop() await runner.cleanup() logger.info("✅ YooKassa webhook сервер остановлен") except Exception as e: logger.error(f"❌ Ошибка запуска YooKassa webhook сервера: {e}", exc_info=True) raise