feat(telegram): accept signed callback forwards
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 5m59s
E2E Health Check / e2e-health (push) Failing after 32s
CD Pipeline / build-and-deploy (push) Successful in 15m0s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 0s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 12s
CD Pipeline / post-deploy-checks (push) Successful in 5m49s

This commit is contained in:
ogt
2026-07-16 23:52:02 +08:00
parent d3d53a4f69
commit fc8ac4e602
2 changed files with 305 additions and 3 deletions

View File

@@ -1,5 +1,9 @@
from __future__ import annotations
import hashlib
import hmac
import json
import time
from datetime import UTC, datetime, timedelta
from types import SimpleNamespace
@@ -369,6 +373,144 @@ class _CanonicalWebhookGateway:
})
class _CallbackForwardRequest:
def __init__(self, body: bytes) -> None:
self._body = body
async def body(self) -> bytes:
return self._body
class _CallbackForwardRedis:
def __init__(self) -> None:
self.values: dict[str, str] = {}
async def set(self, key: str, value: str, **_kwargs) -> bool:
if key in self.values:
return False
self.values[key] = value
return True
async def setex(self, key: str, _ttl: int, value: str) -> None:
self.values[key] = value
async def delete(self, key: str) -> None:
self.values.pop(key, None)
def _signed_callback_forward_body(token: str) -> tuple[bytes, str, str]:
body = json.dumps(
{
"callback_query": {
"data": "detail:INC-20260716-TEST",
"from": {"id": 7, "username": "operator"},
"id": "callback-forward-77",
"message": {
"chat": {"id": -1001},
"message_id": 9,
"text": "incident",
},
},
"update_id": 77,
},
separators=(",", ":"),
sort_keys=True,
).encode("utf-8")
timestamp = str(int(time.time()))
signature = "sha256=" + hmac.new(
token.encode("utf-8"),
timestamp.encode("ascii") + b"." + body,
hashlib.sha256,
).hexdigest()
return body, timestamp, signature
@pytest.mark.asyncio
async def test_hmac_callback_forward_processes_only_registered_info_action(
monkeypatch,
) -> None:
token = "test-forwarder-token"
body, timestamp, signature = _signed_callback_forward_body(token)
gateway = _CanonicalWebhookGateway()
redis = _CallbackForwardRedis()
monkeypatch.setattr(
telegram_webhook_api,
"settings",
SimpleNamespace(OPENCLAW_TG_BOT_TOKEN=token),
)
monkeypatch.setattr(
telegram_gateway_module,
"get_telegram_gateway",
lambda: gateway,
)
monkeypatch.setattr("src.core.redis_client.get_redis", lambda: redis)
result = await telegram_webhook_api.telegram_callback_forward(
_CallbackForwardRequest(body),
x_telegram_forward_timestamp=timestamp,
x_telegram_forward_signature=signature,
)
assert result == {
"ok": True,
"status": "processed",
"update_id": 77,
"action": "detail",
}
assert gateway.receipts[0]["ingress_source"] == (
"openclaw_188_hmac_forwarder"
)
@pytest.mark.asyncio
async def test_hmac_callback_forward_rejects_invalid_signature(monkeypatch) -> None:
token = "test-forwarder-token"
body, timestamp, _signature = _signed_callback_forward_body(token)
monkeypatch.setattr(
telegram_webhook_api,
"settings",
SimpleNamespace(OPENCLAW_TG_BOT_TOKEN=token),
)
with pytest.raises(HTTPException) as exc_info:
await telegram_webhook_api.telegram_callback_forward(
_CallbackForwardRequest(body),
x_telegram_forward_timestamp=timestamp,
x_telegram_forward_signature="sha256=invalid",
)
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_hmac_callback_forward_rejects_write_action(monkeypatch) -> None:
token = "test-forwarder-token"
body, timestamp, _signature = _signed_callback_forward_body(token)
unsafe_body = body.replace(
b"detail:INC-20260716-TEST",
b"approve:APR-1:1:nonce",
)
signature = "sha256=" + hmac.new(
token.encode("utf-8"),
timestamp.encode("ascii") + b"." + unsafe_body,
hashlib.sha256,
).hexdigest()
monkeypatch.setattr(
telegram_webhook_api,
"settings",
SimpleNamespace(OPENCLAW_TG_BOT_TOKEN=token),
)
with pytest.raises(HTTPException) as exc_info:
await telegram_webhook_api.telegram_callback_forward(
_CallbackForwardRequest(unsafe_body),
x_telegram_forward_timestamp=timestamp,
x_telegram_forward_signature=signature,
)
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_canonical_webhook_forwards_callback_to_gateway_handler(monkeypatch) -> None:
gateway = _CanonicalWebhookGateway()