实现了 GitLab 全功能集成,包含 Webhook 处理、API 客户端、消息收发、会话管理、安全校验、限流器等完整模块,支持 Issues、Merge Requests、流水线等事件监听与交互。
108 lines
3.4 KiB
Python
108 lines
3.4 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
|
|
from yuxi.channel.extensions.gitlab.dedupe import GitLabDeduplicator
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class GitLabWebhookHandler:
|
|
MAX_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2 MB
|
|
|
|
def __init__(self, webhook_token: str, bot_username: str, signing_secret: str = ""):
|
|
self._token = webhook_token
|
|
self._signing_secret = signing_secret
|
|
self._bot_username = bot_username
|
|
self._deduplicator = GitLabDeduplicator(max_size=10000)
|
|
|
|
def _verify_hmac(self, body: bytes, headers: dict) -> bool:
|
|
sig_parts = headers.get("webhook-signature", "").split(",")
|
|
sig_map = {}
|
|
for part in sig_parts:
|
|
if "=" in part:
|
|
k, v = part.split("=", 1)
|
|
sig_map[k.strip()] = v.strip()
|
|
|
|
t = sig_map.get("t", "")
|
|
v1 = sig_map.get("v1", "")
|
|
if not t or not v1:
|
|
return False
|
|
|
|
signed_payload = f"{t}.{body.decode('utf-8', errors='replace')}"
|
|
expected = hmac.new(
|
|
self._signing_secret.encode("utf-8"),
|
|
signed_payload.encode("utf-8"),
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
return hmac.compare_digest(expected, v1)
|
|
|
|
def verify_token(self, token_header: str | None) -> bool:
|
|
if not token_header or not self._token:
|
|
return False
|
|
return hmac.compare_digest(token_header, self._token)
|
|
|
|
async def handle_webhook(self, body: bytes, headers: dict) -> dict:
|
|
if len(body) > self.MAX_PAYLOAD_SIZE:
|
|
logger.warning(
|
|
"GitLab webhook: payload too large (%d bytes > %d)",
|
|
len(body),
|
|
self.MAX_PAYLOAD_SIZE,
|
|
)
|
|
return {"status": "payload_too_large"}
|
|
|
|
event_type = headers.get("X-Gitlab-Event", "")
|
|
event_uuid = headers.get("X-Gitlab-Event-Uuid", "")
|
|
|
|
authorized = False
|
|
|
|
if self._signing_secret and headers.get("webhook-signature"):
|
|
authorized = self._verify_hmac(body, headers)
|
|
|
|
if not authorized:
|
|
token_header = headers.get("X-Gitlab-Token", "")
|
|
authorized = self.verify_token(token_header)
|
|
|
|
if not authorized:
|
|
logger.warning("GitLab webhook: authentication failed")
|
|
return {"status": "unauthorized"}
|
|
|
|
try:
|
|
payload = json.loads(body)
|
|
except json.JSONDecodeError:
|
|
logger.error("GitLab webhook: invalid JSON payload")
|
|
return {"status": "invalid_payload"}
|
|
|
|
object_kind = payload.get("object_kind", "")
|
|
|
|
if not object_kind and event_type not in (
|
|
"Push Hook",
|
|
"Tag Push Hook",
|
|
"Wiki Page Hook",
|
|
"Feature Flag Hook",
|
|
"Release Hook",
|
|
"Milestone Hook",
|
|
"Emoji Hook",
|
|
"Vulnerability Hook",
|
|
):
|
|
return {"status": "unknown_event"}
|
|
|
|
if self._deduplicator.is_duplicate(payload, event_uuid):
|
|
logger.debug("GitLab webhook: duplicate event %s", event_uuid)
|
|
return {"status": "duplicate"}
|
|
|
|
user = payload.get("user", {})
|
|
username = user.get("username", "")
|
|
if username and username == self._bot_username:
|
|
return {"status": "self_message"}
|
|
|
|
return {
|
|
"status": "ok",
|
|
"object_kind": object_kind,
|
|
"event_type": event_type,
|
|
"payload": payload,
|
|
}
|