mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-11 14:40:30 +00:00
244 lines
10 KiB
Python
244 lines
10 KiB
Python
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
|