fix(telegram): nonce UUID base64url 壓縮 — 徹底解決 BUTTON_DATA_INVALID
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 9m45s

前次修法(truncate random)不完整:host_restart_service(20 chars) 即使去掉 random
仍 68 bytes > 64 限制。

根本修法:UUID (36 chars) → base64url encode UUID bytes → 22 chars
nonce 格式:{action}:{b64url_uuid}:{timestamp}:{random}
最長 case: host_restart_service(20)+22+10+8+3 colons = 63 bytes

generate_callback_nonce: UUID → base64url 22 chars
parse_callback_data: 22-char b64url → 還原完整 UUID,handler 不需改動

全 action 驗證:approve/silence/reject/docker_restart/host_restart_service/renew_cert
全部 ≤ 63 bytes,UUID round-trip 正確。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-04-21 21:30:17 +08:00
parent 4bc183742f
commit 8fd31eca66

View File

@@ -406,16 +406,22 @@ class TelegramSecurityInterceptor:
"""
import secrets
import base64, uuid as _uuid
timestamp = int(time.time())
random_part = secrets.token_hex(4)
nonce = f"{action}:{approval_id}:{timestamp}:{random_part}"
# UUID (36 chars) → base64url (22 chars) to keep nonce ≤ 63 bytes for all action names.
# Longest known action: host_restart_service (20) + 22 + ts(10) + rand(8) + 3 colons = 63 bytes.
try:
short_id = base64.urlsafe_b64encode(
_uuid.UUID(approval_id).bytes
).rstrip(b"=").decode()
except (ValueError, AttributeError):
# Not a valid UUID (e.g. legacy format) — use as-is, may exceed limit but won't crash
short_id = approval_id
# Telegram callback_data limit is 64 bytes.
# Long action names (e.g. docker_restart=14 chars) with UUID approval_id push nonce to 71+ bytes.
# Drop the random suffix when over limit — timestamp still guarantees temporal uniqueness.
if len(nonce.encode()) > 63:
nonce = f"{action}:{approval_id}:{timestamp}"
nonce = f"{action}:{short_id}:{timestamp}:{random_part}"
logger.debug(
"callback_nonce_generated",
@@ -454,15 +460,28 @@ class TelegramSecurityInterceptor:
"is_info_action": True,
}
# 接受 3-part長 action 名截斷 random和 4-part標準兩種格式
if len(parts) not in (3, 4):
if len(parts) != 4:
raise ValueError(f"Invalid callback_data format: {callback_data}")
import base64, uuid as _uuid
raw_id = parts[1]
# Decode base64url-encoded UUID (22 chars) back to full UUID string.
# Legacy nonces with full UUID (36 chars) pass through unchanged.
if len(raw_id) == 22:
try:
decoded = _uuid.UUID(bytes=base64.urlsafe_b64decode(raw_id + "=="))
approval_id = str(decoded)
except Exception:
approval_id = raw_id
else:
approval_id = raw_id
return {
"action": parts[0],
"approval_id": parts[1],
"approval_id": approval_id,
"timestamp": int(parts[2]),
"nonce": callback_data, # 整個字串作為 nonce
"nonce": callback_data,
"is_info_action": False,
}