[bugfix] 修复对于gitea webhook签名的检查失败问题

This commit is contained in:
jeffusion
2025-03-14 11:50:17 +08:00
parent e488ebc492
commit 0d09a951b2
5 changed files with 84 additions and 34 deletions

View File

@@ -9,4 +9,7 @@ OPENAI_MODEL=gpt-4o-mini
# 应用配置
PORT=3000
# 建议使用以下命令生成一个安全的随机字符串作为webhook密钥:
# 在Linux/Mac终端: openssl rand -hex 32
# 或者在Node.js中: require('crypto').randomBytes(32).toString('hex')
WEBHOOK_SECRET=your_webhook_secret

View File

@@ -71,17 +71,28 @@
**Pull Request审查webhook**:
- URL: `http://your-server:3000/webhook/gitea/pull_request`
- 内容类型: `application/json`
- 秘钥: 设置为与WEBHOOK_SECRET相同的值
- 秘钥: 设置为与`WEBHOOK_SECRET`环境变量相同的值
- 触发事件: 选择"Pull Request"
**提交状态审查webhook**:
- URL: `http://your-server:3000/webhook/gitea/status`
- 内容类型: `application/json`
- 秘钥: 设置为与WEBHOOK_SECRET相同的值
- 秘钥: 设置为与`WEBHOOK_SECRET`环境变量相同的值
- 触发事件: 选择"Status"
> 注意: 老端点 `/webhook/gitea` 仍然支持Pull Request审查但仅作向后兼容使用。
### Webhook签名验证
为确保请求安全系统使用Gitea的Webhook签名验证机制
1. 设置环境变量`WEBHOOK_SECRET`为一个安全的随机字符串
2. 在Gitea的Webhook配置中使用相同的字符串作为"秘钥"
3. 每次请求时,系统会验证请求头中的`X-Gitea-Signature`
4. 如果签名验证失败,请求会被拒绝处理
验证方法使用SHA-256哈希算法在处理高负载的情况下这能防止恶意请求并保证请求来源的真实性。
## 功能说明
### PR代码审查

View File

@@ -1,8 +1,10 @@
{
"name": "ai-review",
"name": "gitea-ai-reviewer",
"version": "1.0.0",
"description": "AI-driven code review for Gitea",
"packageManager": "yarn@4.6.0",
"engines": {
"bun": ">=1.2.5"
},
"dependencies": {
"@hono/zod-validator": "^0.4.3",
"axios": "^1.8.3",

View File

@@ -1,8 +1,8 @@
import { Context } from 'hono';
import { giteaService, PullRequestFile, PullRequestDetails } from '../services/gitea';
import { aiReviewService } from '../services/ai-review';
// import config from '../config';
// import * as crypto from 'crypto';
import config from '../config';
import * as crypto from 'crypto';
import { logger } from '../utils/logger';
// 判断是否为开发环境
@@ -11,32 +11,41 @@ const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
/**
* 验证Webhook请求签名
*/
// function verifyWebhookSignature(body: string, signature: string): boolean {
// // 开发环境下跳过签名验证
// if (isDev && !signature) {
// logger.warn('开发环境: 跳过Webhook签名验证');
// return true;
// }
function verifyWebhookSignature(body: string, signature: string): boolean {
// 开发环境下跳过签名验证
if (isDev && !signature) {
logger.warn('开发环境: 跳过Webhook签名验证');
return true;
}
// if (!config.app.webhookSecret) return false;
if (!config.app.webhookSecret) {
logger.warn('未配置Webhook密钥跳过签名验证');
return false;
}
// const hmac = crypto.createHmac('sha256', config.app.webhookSecret);
// hmac.update(body);
// const calculatedSignature = `sha256=${hmac.digest('hex')}`;
// Gitea使用SHA-256哈希算法
const hmac = crypto.createHmac('sha256', config.app.webhookSecret);
hmac.update(body);
const calculatedSignature = hmac.digest('hex');
// // 如果签名不存在直接返回false
// if (!signature) return false;
// 如果签名不存在直接返回false
if (!signature) {
logger.warn('请求中无签名头');
return false;
}
// try {
// return crypto.timingSafeEqual(
// Buffer.from(calculatedSignature),
// Buffer.from(signature)
// );
// } catch (error) {
// logger.error('签名验证失败', error);
// return false;
// }
// }
// Gitea的签名没有前缀直接比较
try {
// 使用timingSafeEqual进行常量时间比较防止时序攻击
return crypto.timingSafeEqual(
Buffer.from(calculatedSignature),
Buffer.from(signature)
);
} catch (error) {
logger.error('签名验证失败', error);
return false;
}
}
/**
* 处理Pull Request事件
@@ -44,13 +53,13 @@ const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
export async function handlePullRequestEvent(c: Context): Promise<Response> {
try {
// 验证Webhook签名
// const signature = c.req.header('X-Gitea-Signature') || '';
const signature = c.req.header('X-Gitea-Signature') || '';
const rawBody = await c.req.text();
// if (!verifyWebhookSignature(rawBody, signature)) {
// logger.error('Webhook签名验证失败');
// return c.json({ error: 'Webhook签名验证失败' }, 401);
// }
if (!verifyWebhookSignature(rawBody, signature)) {
logger.error('Webhook签名验证失败');
return c.json({ error: 'Webhook签名验证失败' }, 401);
}
// 解析请求体
const body = JSON.parse(rawBody);
@@ -99,7 +108,15 @@ export async function handlePullRequestEvent(c: Context): Promise<Response> {
*/
export async function handleCommitStatusEvent(c: Context): Promise<Response> {
try {
// 验证Webhook签名
const signature = c.req.header('X-Gitea-Signature') || '';
const rawBody = await c.req.text();
if (!verifyWebhookSignature(rawBody, signature)) {
logger.error('Webhook签名验证失败');
return c.json({ error: 'Webhook签名验证失败' }, 401);
}
const body = JSON.parse(rawBody);
// 记录收到的数据,方便调试

View File

@@ -7,7 +7,24 @@ const app = new Hono();
// 健康检查路由
app.get('/', (c) => {
return c.json({ status: 'ok', message: 'AI Code Review 服务运行中' });
const webhookSecretConfigured = !!config.app.webhookSecret;
return c.json({
status: 'ok',
message: 'AI Code Review 服务运行中',
version: '1.1.0',
webhookSecurityEnabled: webhookSecretConfigured,
configuration: {
webhookEndpoints: {
pullRequest: '/webhook/gitea/pull_request',
commitStatus: '/webhook/gitea/status',
legacy: '/webhook/gitea (仅支持Pull Request事件)'
},
signature: webhookSecretConfigured
? '签名验证已启用 (使用X-Gitea-Signature头)'
: '警告: 签名验证未配置建议设置WEBHOOK_SECRET环境变量'
}
});
});
// Gitea webhook路由 - 处理PR事件