38 lines
1.6 KiB
Python
38 lines
1.6 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import hashlib
|
||
|
|
import hmac
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
# Forgejo (Gitea-compatible) signs webhook payloads with HMAC-SHA256 over the
|
||
|
|
# raw request body, sent as a hex digest in X-Forgejo-Signature (Forgejo) or
|
||
|
|
# X-Gitea-Signature (Gitea) -- both forges use the same scheme during the
|
||
|
|
# transition, so both header names are accepted.
|
||
|
|
SIGNATURE_HEADERS = ("X-Forgejo-Signature", "X-Gitea-Signature")
|
||
|
|
|
||
|
|
RELEVANT_PATH_PREFIX = "registry/indexes/"
|
||
|
|
|
||
|
|
|
||
|
|
def verify_signature(secret: str, body: bytes, signature: str | None) -> bool:
|
||
|
|
"""Constant-time HMAC-SHA256 verification. Returns False (never raises)
|
||
|
|
for a missing/malformed signature or a misconfigured (empty) secret --
|
||
|
|
callers must treat False as 'reject', not 'skip verification'."""
|
||
|
|
if not secret or not signature:
|
||
|
|
return False
|
||
|
|
expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
|
||
|
|
return hmac.compare_digest(expected, signature.strip())
|
||
|
|
|
||
|
|
|
||
|
|
def push_touches_registry_index(payload: dict[str, Any]) -> bool:
|
||
|
|
"""True if any commit in a Forgejo/Gitea push-event payload adds,
|
||
|
|
modifies, or removes a path under registry/indexes/. Only inspects
|
||
|
|
paths -- never parses file content (design principle 2: no
|
||
|
|
push-parsing of payloads into registry state, the webhook only
|
||
|
|
triggers a pull-based recompose)."""
|
||
|
|
for commit in payload.get("commits") or []:
|
||
|
|
for key in ("added", "modified", "removed"):
|
||
|
|
for path in commit.get(key) or []:
|
||
|
|
if path.startswith(RELEVANT_PATH_PREFIX):
|
||
|
|
return True
|
||
|
|
return False
|