Диагностика вебхука без пересылки секрета
Отпечаток секрета (sha256, 8 символов) в /health по токену: две стороны сверяют настройки, не пересылая значение. В журнал доставки добавлены размер тела, его sha256, начало подписи и текст ответа - по ним видно, расходится секрет или принимающая сторона считает подпись не от сырых байтов. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f56ed7b5a0
commit
d94f6f73b2
+8
-1
@@ -25,7 +25,7 @@ from app.pipeline import ModelsMissing, Pipeline, to_wav16k
|
||||
from app.security import (check_token, client_address, ip_allowed,
|
||||
parse_allowlist, token_matches)
|
||||
from app.store import JobStatus, JobStore
|
||||
from app.webhook import deliver_async
|
||||
from app.webhook import deliver_async, secret_fingerprint
|
||||
from app.version import __version__
|
||||
|
||||
log = logging.getLogger("talkscore-asr")
|
||||
@@ -305,6 +305,13 @@ def health(request: Request) -> JSONResponse:
|
||||
"your_ip": client_ip,
|
||||
"your_ip_allowed": ip_allowed(client_ip, allowlist),
|
||||
"ip_filter_active": bool(allowlist),
|
||||
"webhook_secret_set": bool(settings.webhook_secret),
|
||||
# Отпечаток, а не сам секрет: две стороны сверяют его и видят,
|
||||
# совпадают ли настройки, не пересылая значение. Отдаётся только
|
||||
# по токену - для мониторинга он не нужен.
|
||||
**({"webhook_secret_fingerprint": secret_fingerprint(settings.webhook_secret)}
|
||||
if check_token(request.headers.get("authorization"), settings.token)
|
||||
else {}),
|
||||
})
|
||||
|
||||
|
||||
|
||||
+29
-4
@@ -11,7 +11,7 @@ import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
__all__ = ["sign_payload", "deliver", "deliver_async"]
|
||||
__all__ = ["sign_payload", "deliver", "deliver_async", "secret_fingerprint"]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,10 +31,22 @@ def deliver(url: str, payload: dict, secret: str = "",
|
||||
"""Отправляет результат, повторяя при неудаче. Возвращает признак успеха."""
|
||||
import requests
|
||||
|
||||
# ensure_ascii=False: кириллица уходит живыми UTF-8 байтами. Принимающая
|
||||
# сторона обязана считать подпись от сырого тела запроса - если она
|
||||
# пересоберёт JSON из разобранного объекта, байты изменятся и подпись
|
||||
# не сойдётся ни при каком секрете.
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
headers = {"Content-Type": "application/json; charset=utf-8"}
|
||||
if secret:
|
||||
headers["X-Talkscore-Signature"] = sign_payload(body, secret)
|
||||
signature = sign_payload(body, secret) if secret else ""
|
||||
if signature:
|
||||
headers["X-Talkscore-Signature"] = signature
|
||||
|
||||
# Ни подпись, ни хеш тела секретом не являются: по ним ничего не
|
||||
# восстанавливается, зато обе стороны могут сверить, об одном ли теле речь.
|
||||
log.info("вебхук по задаче %s: тело %d байт, sha256 %s, подпись %s",
|
||||
payload.get("job_id"), len(body),
|
||||
hashlib.sha256(body).hexdigest()[:12],
|
||||
signature[:16] or "НЕ ПОДПИСАНО (секрет не задан)")
|
||||
|
||||
for attempt, delay in enumerate(delays, start=1):
|
||||
if delay:
|
||||
@@ -44,7 +56,8 @@ def deliver(url: str, payload: dict, secret: str = "",
|
||||
if response.status_code < 300:
|
||||
log.info("вебхук доставлен по задаче %s", payload.get("job_id"))
|
||||
return True
|
||||
log.warning("вебхук: попытка %d, ответ %s", attempt, response.status_code)
|
||||
log.warning("вебхук: попытка %d, ответ %s %s", attempt,
|
||||
response.status_code, response.text[:200])
|
||||
except Exception as exc: # noqa: BLE001 - причина неважна, важна повторная попытка
|
||||
log.warning("вебхук: попытка %d не удалась (%s)", attempt, exc)
|
||||
log.error("вебхук не доставлен по задаче %s, результат остаётся в очереди",
|
||||
@@ -57,3 +70,15 @@ def deliver_async(url: str, payload: dict, secret: str = "") -> None:
|
||||
thread = threading.Thread(target=deliver, args=(url, payload, secret),
|
||||
name="webhook", daemon=True)
|
||||
thread.start()
|
||||
|
||||
|
||||
def secret_fingerprint(secret: str) -> str:
|
||||
"""Короткий отпечаток секрета для сверки настроек между сторонами.
|
||||
|
||||
Значение пересылать нельзя - оно осядет в переписке и журналах. Отпечаток
|
||||
отвечает на единственный нужный вопрос: одинаковый ли секрет настроен
|
||||
здесь и там. Восстановить по нему секрет нельзя.
|
||||
"""
|
||||
if not secret:
|
||||
return ""
|
||||
return hashlib.sha256(secret.encode("utf-8")).hexdigest()[:8]
|
||||
|
||||
Reference in New Issue
Block a user