Skip to content

Commit a94d5c2

Browse files
committed
feat: 重构 GitHub webhook 处理逻辑,提取签名验证和仓库匹配功能到独立模块
1 parent 9c9a230 commit a94d5c2

File tree

2 files changed

+140
-95
lines changed

2 files changed

+140
-95
lines changed

app.py

Lines changed: 31 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import hmac
2-
import hashlib
31
import logging
4-
from typing import Optional
5-
6-
from fastapi import FastAPI, Request, HTTPException, Header
2+
from fastapi import FastAPI, Request, HTTPException
73

84
from settings import Settings
95
from send_message import send_github_notification
6+
from hooks.github_webhook import verify_signature, find_matching_webhook, extract_push_data
107

118
logging.basicConfig(level=logging.INFO)
129
logger = logging.getLogger(__name__)
@@ -15,71 +12,6 @@
1512

1613
settings = Settings().from_yaml()
1714

18-
# 全局变量
19-
WS_URL = settings.WS_URL
20-
WS_ACCESS_TOKEN = settings.WS_ACCESS_TOKEN
21-
GITHUB_WEBHOOK = settings.GITHUB_WEBHOOK
22-
23-
def match_repository(repo_name: str, pattern: str) -> bool:
24-
"""
25-
检查仓库名是否匹配配置中的模式
26-
支持大小写不敏感匹配和通配符(用户名/*)形式
27-
28-
Args:
29-
repo_name: 实际的仓库全名 (例如 'user/repo')
30-
pattern: 配置中的仓库模式 (例如 'user/repo' 或 'user/*')
31-
32-
Returns:
33-
bool: 是否匹配
34-
"""
35-
if not repo_name or not pattern:
36-
return False
37-
38-
repo_name = repo_name.lower()
39-
pattern = pattern.lower()
40-
41-
if pattern.endswith('/*'):
42-
user = pattern[:-2]
43-
return repo_name.startswith(f"{user}/")
44-
45-
return repo_name == pattern
46-
47-
async def verify_signature(request: Request, x_hub_signature_256: Optional[str] = Header(None)):
48-
"""验证 GitHub webhook 签名"""
49-
50-
try:
51-
body = await request.body()
52-
payload = await request.json()
53-
repo_name = payload.get("repository", {}).get("full_name")
54-
except Exception as e:
55-
logger.error("解析请求体失败: %s", str(e))
56-
raise HTTPException(status_code=400, detail="Invalid JSON payload") from e
57-
58-
webhook_secret = None
59-
for webhook in settings.GITHUB_WEBHOOK:
60-
if any(match_repository(repo_name, repo_pattern) for repo_pattern in webhook.REPO):
61-
webhook_secret = webhook.SECRET
62-
break
63-
64-
if not webhook_secret:
65-
logger.warning("仓库 %s 未配置 webhook 密钥,跳过签名验证", repo_name)
66-
return True
67-
68-
if not x_hub_signature_256:
69-
raise HTTPException(status_code=401, detail="Missing X-Hub-Signature-256 header")
70-
71-
signature = hmac.new(
72-
key=webhook_secret.encode(),
73-
msg=body,
74-
digestmod=hashlib.sha256
75-
).hexdigest()
76-
77-
expected_signature = f"sha256={signature}"
78-
if not hmac.compare_digest(expected_signature, x_hub_signature_256):
79-
raise HTTPException(status_code=401, detail="Invalid signature")
80-
81-
return True
82-
8315
@app.post("/github-webhook")
8416
async def github_webhook(request: Request):
8517
"""处理 GitHub webhook 请求"""
@@ -89,13 +21,20 @@ async def github_webhook(request: Request):
8921
logger.info("收到非 JSON 格式的请求,忽略")
9022
return {"status": "ignored", "message": "只处理 application/json 格式的请求"}
9123

92-
event_type = request.headers.get("X-GitHub-Event")
24+
event_type = request.headers.get("X-GitHub-Event", "")
25+
if not event_type:
26+
logger.info("缺少 X-GitHub-Event 头,忽略")
27+
return {"status": "ignored", "message": "缺少 X-GitHub-Event 头"}
9328

9429
try:
95-
await verify_signature(request, request.headers.get("X-Hub-Signature-256"))
96-
except HTTPException:
97-
logger.info("签名验证失败")
98-
return {"status": "ignored", "message": "签名验证失败"}
30+
await verify_signature(
31+
request,
32+
settings.GITHUB_WEBHOOK,
33+
request.headers.get("X-Hub-Signature-256")
34+
)
35+
except HTTPException as e:
36+
logger.info(f"签名验证失败: {e.detail}")
37+
return {"status": "ignored", "message": f"签名验证失败: {e.detail}"}
9938

10039
payload = await request.json()
10140
if not payload:
@@ -105,41 +44,38 @@ async def github_webhook(request: Request):
10544
repo_name = payload.get("repository", {}).get("full_name")
10645
branch = payload.get("ref", "").replace("refs/heads/", "")
10746

108-
matched_webhook = None
109-
for webhook in settings.GITHUB_WEBHOOK:
110-
111-
repo_matches = any(match_repository(repo_name, repo_pattern) for repo_pattern in webhook.REPO)
112-
113-
if (repo_matches and
114-
branch in webhook.BRANCH and
115-
event_type in webhook.EVENTS):
116-
matched_webhook = webhook
117-
break
47+
matched_webhook = find_matching_webhook(
48+
repo_name,
49+
branch,
50+
event_type,
51+
settings.GITHUB_WEBHOOK
52+
)
11853

11954
if not matched_webhook:
12055
logger.info("找不到匹配的 webhook 配置: 仓库 %s, 分支 %s, 事件类型 %s", repo_name, branch, event_type)
12156
return {"status": "ignored", "message": "找不到匹配的 webhook 配置"}
12257

12358
# 处理不同类型的事件,暂时只支持 push 事件
12459
if event_type == "push":
125-
pusher = payload.get("pusher", {}).get("name")
126-
commits = payload.get("commits", [])
127-
commit_count = len(commits)
60+
push_data = extract_push_data(payload)
12861

129-
logger.info("发现新的 push 事件,来自 %s 仓库", repo_name)
130-
logger.info("分支: %s,推送者: %s,提交数量: %s", branch, pusher, commit_count)
62+
logger.info("发现新的 push 事件,来自 %s 仓库", push_data["repo_name"])
63+
logger.info("分支: %s,推送者: %s,提交数量: %s",
64+
push_data["branch"],
65+
push_data["pusher"],
66+
push_data["commit_count"])
13167

13268
# 向配置的所有 OneBot 目标发送通知
13369
for target in matched_webhook.ONEBOT:
13470
logger.info("正在发送消息到 QQ 群 %s", target.id)
13571
await send_github_notification(
13672
ws_url=settings.WS_URL,
13773
access_token=settings.WS_ACCESS_TOKEN,
138-
repo_name=repo_name,
139-
branch=branch,
140-
pusher=pusher,
141-
commit_count=commit_count,
142-
commits=commits,
74+
repo_name=push_data["repo_name"],
75+
branch=push_data["branch"],
76+
pusher=push_data["pusher"],
77+
commit_count=push_data["commit_count"],
78+
commits=push_data["commits"],
14379
onebot_type=target.type,
14480
onebot_id=target.id
14581
)

hooks/github_webhook.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import hmac
2+
import hashlib
3+
import logging
4+
from typing import Optional, Dict, Any, List
5+
6+
from fastapi import Request, HTTPException, Header
7+
8+
logger = logging.getLogger(__name__)
9+
10+
def match_repository(repo_name: str, pattern: str) -> bool:
11+
"""
12+
检查仓库名是否匹配配置中的模式
13+
支持大小写不敏感匹配和通配符(用户名/*)形式
14+
15+
Args:
16+
repo_name: 实际的仓库全名 (例如 'user/repo')
17+
pattern: 配置中的仓库模式 (例如 'user/repo' 或 'user/*')
18+
19+
Returns:
20+
bool: 是否匹配
21+
"""
22+
if not repo_name or not pattern:
23+
return False
24+
25+
repo_name = repo_name.lower()
26+
pattern = pattern.lower()
27+
28+
if pattern.endswith('/*'):
29+
user = pattern[:-2]
30+
return repo_name.startswith(f"{user}/")
31+
32+
return repo_name == pattern
33+
34+
async def verify_signature(request: Request, webhooks: List, x_hub_signature_256: Optional[str] = Header(None)):
35+
"""验证 GitHub webhook 签名"""
36+
37+
try:
38+
body = await request.body()
39+
payload = await request.json()
40+
repo_name = payload.get("repository", {}).get("full_name")
41+
except Exception as e:
42+
logger.error("解析请求体失败: %s", str(e))
43+
raise HTTPException(status_code=400, detail="Invalid JSON payload") from e
44+
45+
webhook_secret = None
46+
for webhook in webhooks:
47+
if any(match_repository(repo_name, repo_pattern) for repo_pattern in webhook.REPO):
48+
webhook_secret = webhook.SECRET
49+
break
50+
51+
if not webhook_secret:
52+
logger.warning("仓库 %s 未配置 webhook 密钥,跳过签名验证", repo_name)
53+
return True
54+
55+
if not x_hub_signature_256:
56+
raise HTTPException(status_code=401, detail="Missing X-Hub-Signature-256 header")
57+
58+
signature = hmac.new(
59+
key=webhook_secret.encode(),
60+
msg=body,
61+
digestmod=hashlib.sha256
62+
).hexdigest()
63+
64+
expected_signature = f"sha256={signature}"
65+
if not hmac.compare_digest(expected_signature, x_hub_signature_256):
66+
raise HTTPException(status_code=401, detail="Invalid signature")
67+
68+
return True
69+
70+
def find_matching_webhook(repo_name: str, branch: str, event_type: str, webhooks: List) -> Optional[Any]:
71+
"""
72+
查找匹配的 webhook 配置
73+
74+
Args:
75+
repo_name: 仓库名
76+
branch: 分支名
77+
event_type: 事件类型
78+
webhooks: webhook 配置列表
79+
80+
Returns:
81+
匹配的 webhook 配置或 None
82+
"""
83+
for webhook in webhooks:
84+
repo_matches = any(match_repository(repo_name, repo_pattern) for repo_pattern in webhook.REPO)
85+
86+
if (repo_matches and
87+
branch in webhook.BRANCH and
88+
event_type in webhook.EVENTS):
89+
return webhook
90+
91+
return None
92+
93+
def extract_push_data(payload: Dict[str, Any]) -> Dict[str, Any]:
94+
"""
95+
从 push 事件中提取相关数据
96+
97+
Args:
98+
payload: GitHub webhook 负载
99+
100+
Returns:
101+
包含提取数据的字典
102+
"""
103+
return {
104+
"repo_name": payload.get("repository", {}).get("full_name"),
105+
"branch": payload.get("ref", "").replace("refs/heads/", ""),
106+
"pusher": payload.get("pusher", {}).get("name"),
107+
"commits": payload.get("commits", []),
108+
"commit_count": len(payload.get("commits", []))
109+
}

0 commit comments

Comments
 (0)