198 lines
6.9 KiB
Python
198 lines
6.9 KiB
Python
import asyncio
|
|
import functools
|
|
import hmac
|
|
import logging
|
|
|
|
from aiogram import Bot, Dispatcher
|
|
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
|
|
from aiohttp import web
|
|
from aiohttp.web_log import AccessLogger, KeyMethod
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from bot.payment_providers import iter_provider_specs, iter_service_keys
|
|
from bot.utils.request_security import request_client_ip
|
|
from config.settings import Settings
|
|
|
|
|
|
class SecureSimpleRequestHandler(SimpleRequestHandler):
|
|
def verify_secret(self, telegram_secret_token: str, bot: Bot) -> bool:
|
|
if not self.secret_token:
|
|
return False
|
|
return hmac.compare_digest(telegram_secret_token, self.secret_token)
|
|
|
|
|
|
class TrustedProxyAccessLogger(AccessLogger):
|
|
"""Aiohttp access logger that respects trusted X-Forwarded-For headers."""
|
|
|
|
def compile_format(self, log_format):
|
|
methods = []
|
|
for atom in self.FORMAT_RE.findall(log_format):
|
|
if atom[1] == "":
|
|
format_key = self.LOG_FORMAT_MAP[atom[0]]
|
|
method = getattr(type(self), f"_format_{atom[0]}", None)
|
|
if method is None:
|
|
method = getattr(AccessLogger, f"_format_{atom[0]}")
|
|
methods.append(KeyMethod(format_key, method))
|
|
else:
|
|
format_key = (self.LOG_FORMAT_MAP[atom[2]], atom[1])
|
|
method = getattr(type(self), f"_format_{atom[2]}", None)
|
|
if method is None:
|
|
method = getattr(AccessLogger, f"_format_{atom[2]}")
|
|
methods.append(KeyMethod(format_key, functools.partial(method, atom[1])))
|
|
|
|
compiled = self.FORMAT_RE.sub(r"%s", log_format)
|
|
compiled = self.CLEANUP_RE.sub(r"%\1", compiled)
|
|
return compiled, methods
|
|
|
|
@staticmethod
|
|
def _format_a(request, response, time):
|
|
if request is None:
|
|
return "-"
|
|
settings = request.app.get("settings") if hasattr(request, "app") else None
|
|
trusted_proxies = getattr(settings, "trusted_proxies", None)
|
|
client_ip = request_client_ip(request, trusted_proxies=trusted_proxies)
|
|
return client_ip or "-"
|
|
|
|
|
|
def _inject_shared_instances(
|
|
app: web.Application,
|
|
dp: Dispatcher,
|
|
bot: Bot,
|
|
settings: Settings,
|
|
async_session_factory: sessionmaker,
|
|
) -> None:
|
|
app["bot"] = bot
|
|
app["dp"] = dp
|
|
app["settings"] = settings
|
|
app["async_session_factory"] = async_session_factory
|
|
app["i18n"] = dp.get("i18n_instance")
|
|
shared_keys = [
|
|
"subscription_service",
|
|
"referral_service",
|
|
"panel_service",
|
|
"panel_webhook_service",
|
|
"lknpd_service",
|
|
*iter_service_keys(),
|
|
]
|
|
for key in shared_keys:
|
|
if hasattr(dp, "workflow_data") and key in dp.workflow_data: # type: ignore
|
|
app[key] = dp.workflow_data[key] # type: ignore
|
|
|
|
|
|
async def build_and_start_web_app(
|
|
dp: Dispatcher,
|
|
bot: Bot,
|
|
settings: Settings,
|
|
async_session_factory: sessionmaker,
|
|
):
|
|
app = web.Application()
|
|
_inject_shared_instances(app, dp, bot, settings, async_session_factory)
|
|
|
|
async def _healthcheck(request: web.Request) -> web.Response:
|
|
payload = {"status": "ok"}
|
|
try:
|
|
from db.database_setup import async_engine
|
|
|
|
pool = async_engine.pool if async_engine is not None else None
|
|
if pool is not None:
|
|
payload["db_pool"] = {
|
|
"checked_in": pool.checkedin(),
|
|
"checked_out": pool.checkedout(),
|
|
"size": pool.size(),
|
|
"overflow": pool.overflow(),
|
|
}
|
|
except Exception:
|
|
logging.exception("Failed to collect DB pool health metrics")
|
|
return web.json_response(payload)
|
|
|
|
app.router.add_get("/healthz", _healthcheck)
|
|
app.router.add_get("/health", _healthcheck)
|
|
|
|
setup_application(app, dp, bot=bot)
|
|
|
|
telegram_uses_webhook_mode = bool(settings.WEBHOOK_BASE_URL)
|
|
|
|
if telegram_uses_webhook_mode:
|
|
telegram_webhook_path = settings.telegram_webhook_path
|
|
SecureSimpleRequestHandler(
|
|
dispatcher=dp,
|
|
bot=bot,
|
|
secret_token=settings.WEBHOOK_SECRET_TOKEN,
|
|
).register(app, path=telegram_webhook_path)
|
|
logging.info(
|
|
f"Telegram webhook route configured at: [POST] {telegram_webhook_path} (relative to base URL)" # noqa: E501
|
|
)
|
|
|
|
from bot.services.panel_webhook_service import panel_webhook_route
|
|
|
|
registered_webhook_paths: set[str] = set()
|
|
for spec in iter_provider_specs():
|
|
webhook_route = spec.load_webhook_route()
|
|
if not spec.webhook_path or not webhook_route:
|
|
continue
|
|
if spec.webhook_requires_base_url and not settings.WEBHOOK_BASE_URL:
|
|
continue
|
|
path = spec.webhook_path(settings)
|
|
if not path or not path.startswith("/") or path in registered_webhook_paths:
|
|
continue
|
|
registered_webhook_paths.add(path)
|
|
app.router.add_post(path, webhook_route)
|
|
logging.info("%s webhook route configured at: [POST] %s", spec.label, path)
|
|
|
|
panel_path = settings.panel_webhook_path
|
|
if panel_path.startswith("/"):
|
|
app.router.add_post(panel_path, panel_webhook_route)
|
|
logging.info(f"Panel webhook route configured at: [POST] {panel_path}")
|
|
|
|
runners = []
|
|
|
|
webhooks_runner = web.AppRunner(app, access_log_class=TrustedProxyAccessLogger)
|
|
await webhooks_runner.setup()
|
|
runners.append(webhooks_runner)
|
|
site = web.TCPSite(
|
|
webhooks_runner,
|
|
host=settings.WEB_SERVER_HOST,
|
|
port=settings.WEB_SERVER_PORT,
|
|
)
|
|
|
|
await site.start()
|
|
logging.info(
|
|
f"AIOHTTP server started on http://{settings.WEB_SERVER_HOST}:{settings.WEB_SERVER_PORT}"
|
|
)
|
|
|
|
if settings.WEBAPP_ENABLED:
|
|
from bot.app.web.subscription_webapp import create_subscription_webapp_application
|
|
|
|
subscription_app = create_subscription_webapp_application(
|
|
dp,
|
|
bot,
|
|
settings,
|
|
async_session_factory,
|
|
)
|
|
subscription_runner = web.AppRunner(
|
|
subscription_app,
|
|
access_log_class=TrustedProxyAccessLogger,
|
|
)
|
|
await subscription_runner.setup()
|
|
runners.append(subscription_runner)
|
|
subscription_site = web.TCPSite(
|
|
subscription_runner,
|
|
host=settings.WEBAPP_SERVER_HOST,
|
|
port=settings.WEBAPP_SERVER_PORT,
|
|
)
|
|
await subscription_site.start()
|
|
logging.info(
|
|
"Subscription WebApp server started on http://%s:%s",
|
|
settings.WEBAPP_SERVER_HOST,
|
|
settings.WEBAPP_SERVER_PORT,
|
|
)
|
|
|
|
try:
|
|
await asyncio.Event().wait()
|
|
finally:
|
|
for runner in reversed(runners):
|
|
try:
|
|
await runner.cleanup()
|
|
except Exception as cleanup_error:
|
|
logging.warning("Failed to cleanup aiohttp runner: %s", cleanup_error)
|