fix(webhooks): harden webhook config and secret handling

This commit is contained in:
kavore
2026-02-08 21:30:39 +03:00
parent 0d637340f5
commit 4853a49112
7 changed files with 177 additions and 61 deletions
+13 -4
View File
@@ -43,10 +43,19 @@ async def build_and_start_web_app(
telegram_uses_webhook_mode = bool(settings.WEBHOOK_BASE_URL)
if telegram_uses_webhook_mode:
telegram_webhook_path = f"/{settings.BOT_TOKEN}"
app.router.add_post(telegram_webhook_path, SimpleRequestHandler(dispatcher=dp, bot=bot))
telegram_webhook_path = settings.telegram_webhook_path
app.router.add_post(
telegram_webhook_path,
SimpleRequestHandler(
dispatcher=dp,
bot=bot,
secret_token=settings.TELEGRAM_WEBHOOK_SECRET,
),
)
logging.info(
f"Telegram webhook route configured at: [POST] {telegram_webhook_path} (relative to base URL)"
"Telegram webhook route configured at: [POST] %s (secret_token=%s)",
telegram_webhook_path,
"set" if settings.TELEGRAM_WEBHOOK_SECRET else "not_set",
)
from bot.handlers.user.payment import yookassa_webhook_route
@@ -59,7 +68,7 @@ async def build_and_start_web_app(
cp_path = settings.cryptopay_webhook_path
if cp_path.startswith("/"):
app.router.add_post(cp_path, cryptopay_webhook_route)
logging.info(f"CryptoPay webhook route configured at: [POST] {cp_path}")
logging.info("CryptoPay webhook route configured at: [POST] %s", cp_path)
fk_path = settings.freekassa_webhook_path
if fk_path.startswith("/"):
+32 -37
View File
@@ -58,52 +58,47 @@ async def on_startup_configured(dispatcher: Dispatcher):
telegram_webhook_url_to_set = settings.WEBHOOK_BASE_URL
if telegram_webhook_url_to_set:
full_telegram_webhook_url = (
f"{str(telegram_webhook_url_to_set).rstrip('/')}/{settings.BOT_TOKEN}"
)
full_telegram_webhook_url = settings.telegram_full_webhook_url
if not full_telegram_webhook_url:
logging.error(
"STARTUP: Telegram webhook URL could not be built (WEBHOOK_BASE_URL missing)."
)
raise SystemExit("WEBHOOK_BASE_URL is required. Polling mode is disabled.")
logging.info(
f"STARTUP: Attempting to set Telegram webhook to: {full_telegram_webhook_url if full_telegram_webhook_url != 'ERROR_URL_TOKEN_DETECTED' else 'HIDDEN DUE TO TOKEN'}"
"STARTUP: Attempting to set Telegram webhook (path=%s)",
settings.telegram_webhook_path,
)
if full_telegram_webhook_url != "ERROR_URL_TOKEN_DETECTED":
try:
current_webhook_info = await bot.get_webhook_info()
logging.info(
f"STARTUP: Current Telegram webhook info BEFORE setting: {current_webhook_info.model_dump_json(exclude_none=True, indent=2)}"
)
try:
current_webhook_info = await bot.get_webhook_info()
if current_webhook_info.url:
logging.info("STARTUP: Telegram webhook already set (non-empty URL).")
else:
logging.info("STARTUP: Telegram webhook currently empty (will set).")
set_success = await bot.set_webhook(
url=full_telegram_webhook_url,
drop_pending_updates=True,
allowed_updates=dispatcher.resolve_used_update_types(),
)
if set_success:
logging.info(
f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned SUCCESS (True)."
)
else:
logging.error(
f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned FAILURE (False)."
)
set_success = await bot.set_webhook(
url=full_telegram_webhook_url,
drop_pending_updates=True,
allowed_updates=dispatcher.resolve_used_update_types(),
secret_token=settings.TELEGRAM_WEBHOOK_SECRET,
)
if set_success:
logging.info("STARTUP: bot.set_webhook returned SUCCESS (True).")
else:
logging.error("STARTUP: bot.set_webhook returned FAILURE (False).")
new_webhook_info = await bot.get_webhook_info()
logging.info(
f"STARTUP: Telegram Webhook info AFTER setting: {new_webhook_info.model_dump_json(exclude_none=True, indent=2)}"
)
if not new_webhook_info.url:
logging.error(
"STARTUP: CRITICAL - Telegram Webhook URL is EMPTY after set attempt. Check bot token and URL validity."
)
except Exception as e_setwebhook:
new_webhook_info = await bot.get_webhook_info()
if not new_webhook_info.url:
logging.error(
f"STARTUP: EXCEPTION during set/get Telegram webhook: {e_setwebhook}",
exc_info=True,
"STARTUP: CRITICAL - Telegram Webhook URL is EMPTY after set attempt. Check bot token and URL validity."
)
else:
except Exception as e_setwebhook:
logging.error(
"STARTUP: Skipped setting Telegram webhook due to security or configuration error."
"STARTUP: EXCEPTION during set/get Telegram webhook: %s",
e_setwebhook,
exc_info=True,
)
else:
logging.error(
+38 -7
View File
@@ -59,6 +59,29 @@ class PanelApiService:
headers["Authorization"] = f"Bearer {self.api_key}"
return headers
@staticmethod
def _sanitize_payload_for_log(payload: Any) -> Any:
if isinstance(payload, dict):
redacted: Dict[str, Any] = {}
for key, value in payload.items():
lowered = str(key).lower()
if any(mask_key in lowered for mask_key in (
"token",
"secret",
"password",
"authorization",
"api_key",
"apikey",
"key",
)):
redacted[key] = "***"
else:
redacted[key] = PanelApiService._sanitize_payload_for_log(value)
return redacted
if isinstance(payload, list):
return [PanelApiService._sanitize_payload_for_log(item) for item in payload]
return payload
async def _request(self,
method: str,
endpoint: str,
@@ -83,8 +106,8 @@ class PanelApiService:
if current_params:
try:
url_with_params_for_log += "?" + urlencode(current_params)
except Exception:
pass
except Exception as exc:
logging.debug("Failed to encode params for panel API log URL: %s", exc)
json_payload_for_log = kwargs.get('json') if method.upper() in [
"POST", "PATCH", "PUT"
@@ -92,10 +115,11 @@ class PanelApiService:
log_prefix = f"Panel API Req: {method.upper()} {url_with_params_for_log}"
if json_payload_for_log:
try:
payload_str = json.dumps(json_payload_for_log)
sanitized_payload = self._sanitize_payload_for_log(json_payload_for_log)
payload_str = json.dumps(sanitized_payload)
log_prefix += f" | Payload: {payload_str[:300]}{'...' if len(payload_str) > 300 else ''}"
except Exception:
log_prefix += f" | Payload: {str(json_payload_for_log)[:300]}..."
log_prefix += " | Payload: <unavailable>"
try:
async with aiohttp_session.request(method.upper(),
url_for_request,
@@ -106,7 +130,8 @@ class PanelApiService:
log_suffix = f"| Status: {response_status}"
if log_full_response or not (200 <= response_status < 300):
should_log_full_body = bool(log_full_response and self.settings.LOG_LEVEL == "DEBUG")
if should_log_full_body or not (200 <= response_status < 300):
try:
parsed_json_for_log = json.loads(response_text)
pretty_response_text = json.dumps(parsed_json_for_log,
@@ -404,7 +429,10 @@ class PanelApiService:
return response
logging.error(
f"Failed to create panel user '{username_on_panel}'. Payload: {payload}, Response: {response if not log_response else '(full response logged above)'}"
"Failed to create panel user '%s'. Payload: %s, Response: %s",
username_on_panel,
self._sanitize_payload_for_log(payload),
response if not log_response else "(full response logged above)",
)
return response
@@ -426,7 +454,10 @@ class PanelApiService:
return full_response.get("response")
logging.error(
f"Failed to update user {user_uuid} details on panel. Payload: {update_payload}, Response: {full_response if not log_response else '(logged above)'}"
"Failed to update user %s details on panel. Payload: %s, Response: %s",
user_uuid,
self._sanitize_payload_for_log(update_payload),
full_response if not log_response else "(logged above)",
)
return None
+13 -10
View File
@@ -136,16 +136,19 @@ class PanelWebhookService:
)
async def handle_webhook(self, raw_body: bytes, signature_header: Optional[str]) -> web.Response:
if self.settings.PANEL_WEBHOOK_SECRET:
if not signature_header:
return web.Response(status=403, text="no_signature")
expected_sig = hmac.new(
self.settings.PANEL_WEBHOOK_SECRET.encode(),
raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected_sig, signature_header):
return web.Response(status=403, text="invalid_signature")
if not self.settings.PANEL_WEBHOOK_SECRET:
logging.critical("Panel webhook rejected: PANEL_WEBHOOK_SECRET is not configured")
return web.Response(status=503, text="panel_webhook_secret_required")
if not signature_header:
return web.Response(status=403, text="no_signature")
expected_sig = hmac.new(
self.settings.PANEL_WEBHOOK_SECRET.encode(),
raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected_sig, signature_header):
return web.Response(status=403, text="invalid_signature")
try:
payload = json.loads(raw_body.decode())
+1 -1
View File
@@ -81,7 +81,7 @@ class StarsService:
title=description,
description=description,
payload=payload,
provider_token="",
provider_token=self.settings.STARS_PROVIDER_TOKEN or "",
currency="XTR",
prices=prices,
)