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
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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user