diff --git a/api.py b/api.py index ef40870b2..33ac7828e 100644 --- a/api.py +++ b/api.py @@ -10,7 +10,8 @@ from dotenv import load_dotenv from flask import Flask, request, jsonify -from biz.gitlab.webhook_handler import slugify_url +from biz.github.webhook_handler import verify_github_signature +from biz.gitlab.webhook_handler import slugify_url, verify_gitlab_webhook_secret_token from biz.queue.worker import handle_merge_request_event, handle_push_event, handle_github_pull_request_event, handle_github_push_event from biz.service.review_service import ReviewService from biz.utils.im import notifier @@ -19,6 +20,7 @@ from biz.utils.reporter import Reporter from biz.utils.config_checker import check_config + load_dotenv("conf/.env") api_app = Flask(__name__) @@ -113,7 +115,7 @@ def handle_webhook(): # 判断是GitLab还是GitHub的webhook webhook_source = request.headers.get('X-GitHub-Event') - + if webhook_source: # GitHub webhook return handle_github_webhook(webhook_source, data) else: # GitLab webhook @@ -122,18 +124,26 @@ def handle_webhook(): return jsonify({'message': 'Invalid data format'}), 400 def handle_github_webhook(event_type, data): + # 打印整个payload数据 + logger.info(f'Received GitHub event: {event_type}') + logger.info(f'Payload: {json.dumps(data)}') + # 获取GitHub配置 - github_token = os.getenv('GITHUB_ACCESS_TOKEN') or request.headers.get('X-GitHub-Token') + github_token = os.getenv('GITHUB_ACCESS_TOKEN') if not github_token: return jsonify({'message': 'Missing GitHub access token'}), 400 - + + # 获取GitHub Webhook Secret Token + payload_body = request.get_data() + if os.getenv('VALIDATE_GITHUB_WEBHOOK_SECRET_TOKEN', '0') == '1': + github_webhook_secret_token_env = os.getenv('GITHUB_WEBHOOK_SECRET_TOKEN') + github_webhook_secret_token_request = request.headers.get('X-Hub-Signature-256') + if not verify_github_signature(payload_body, github_webhook_secret_token_env, github_webhook_secret_token_request): + return jsonify({'message': 'GitHub Webhook Secret Token mismatch'}), 403 + github_url = os.getenv('GITHUB_URL') or 'https://github.com' github_url_slug = slugify_url(github_url) - - # 打印整个payload数据 - logger.info(f'Received GitHub event: {event_type}') - logger.info(f'Payload: {json.dumps(data)}') - + if event_type == "pull_request": # 使用handle_queue进行异步处理 handle_queue(handle_github_pull_request_event, data, github_token, github_url, github_url_slug) @@ -167,17 +177,23 @@ def handle_gitlab_webhook(data): except Exception as e: return jsonify({"error": f"Failed to parse homepage URL: {str(e)}"}), 400 - # 优先从环境变量获取,如果没有,则从请求头获取 - gitlab_token = os.getenv('GITLAB_ACCESS_TOKEN') or request.headers.get('X-Gitlab-Token') + # 打印整个payload数据,或根据需求进行处理 + logger.info(f'Received event: {object_kind}') + logger.info(f'Payload: {json.dumps(data)}') + + gitlab_token = os.getenv('GITLAB_ACCESS_TOKEN') # 如果gitlab_token为空,返回错误 if not gitlab_token: return jsonify({'message': 'Missing GitLab access token'}), 400 - gitlab_url_slug = slugify_url(gitlab_url) + if os.getenv('VALIDATE_GITLAB_WEBHOOK_SECRET_TOKEN', '0') == '1': + gitlab_webhook_secret_token_env = os.getenv('GITLAB_WEBHOOK_SECRET_TOKEN') + gitlab_webhook_secret_token_request = request.headers.get('X-Gitlab-Token') + if not verify_gitlab_webhook_secret_token(gitlab_webhook_secret_token_env, gitlab_webhook_secret_token_request): + logger.error(f"GitLab Webhook Secret Token mismatch") + return jsonify({'message': 'GitLab Webhook Secret Token mismatch'}), 403 - # 打印整个payload数据,或根据需求进行处理 - logger.info(f'Received event: {object_kind}') - logger.info(f'Payload: {json.dumps(data)}') + gitlab_url_slug = slugify_url(gitlab_url) # 处理Merge Request Hook if object_kind == "merge_request": diff --git a/biz/github/webhook_handler.py b/biz/github/webhook_handler.py index f2948e73b..d21771c34 100644 --- a/biz/github/webhook_handler.py +++ b/biz/github/webhook_handler.py @@ -1,4 +1,5 @@ -import json +import hashlib +import hmac import os import re import time @@ -23,7 +24,7 @@ def filter_changes(changes: list): if change.get('status') == 'removed': logger.info(f"Detected file deletion via status field: {change.get('new_path')}") continue - + # 如果没有status字段或status不为"removed",继续检查diff模式 diff = change.get('diff', '') if diff: @@ -34,12 +35,12 @@ def filter_changes(changes: list): if all(line.startswith('-') or not line for line in diff_lines): logger.info(f"Detected file deletion via diff pattern: {change.get('new_path')}") continue - + not_deleted_changes.append(change) - + logger.info(f"SUPPORTED_EXTENSIONS: {SUPPORTED_EXTENSIONS}") logger.info(f"After filtering deleted files: {not_deleted_changes}") - + # 过滤 `new_path` 以支持的扩展名结尾的元素, 仅保留diff和new_path字段 filtered_changes = [ { @@ -53,6 +54,29 @@ def filter_changes(changes: list): return filtered_changes +# from https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries#python-example +def verify_github_signature(payload_body, secret_token, signature_header): + """Verify that the payload was sent from GitHub by validating SHA256. + + Args: + payload_body: original request body to verify (request.body()) + secret_token: GitHub app webhook token (WEBHOOK_SECRET) + signature_header: header received from GitHub (x-hub-signature-256) + """ + # 仅当设置了环境变量时才验证秘密令牌 + if not secret_token: + return True + if not signature_header: + logger.error("x-hub-signature-256 header is missing!") + return False + hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256) + expected_signature = "sha256=" + hash_object.hexdigest() + if not hmac.compare_digest(expected_signature, signature_header): + logger.error("Request signatures didn't match!") + return False + return True + + class PullRequestHandler: def __init__(self, webhook_data: dict, github_token: str, github_url: str): self.pull_request_number = None @@ -133,7 +157,7 @@ def get_pull_request_commits(self) -> list: } response = requests.get(url, headers=headers) logger.debug(f"Get commits response from GitHub: {response.status_code}, {response.text}") - + # 检查请求是否成功 if response.status_code == 200: # 将GitHub的commits转换为GitLab格式的commits @@ -330,12 +354,12 @@ def get_push_changes(self) -> list: elif self.webhook_data.get('deleted', False): # 删除分支处理 return [] - + return self.repository_compare(before, after) else: # 如果before和after不存在,尝试通过commits获取 logger.info("before or after not found in webhook data, trying to get changes from commits.") - + changes = [] for commit in self.commit_list: commit_id = commit.get('id') @@ -344,5 +368,5 @@ def get_push_changes(self) -> list: if parent_id: commit_changes = self.repository_compare(parent_id, commit_id) changes.extend(commit_changes) - - return changes \ No newline at end of file + + return changes diff --git a/biz/gitlab/webhook_handler.py b/biz/gitlab/webhook_handler.py index 897cc42c6..ef0b5d6a8 100644 --- a/biz/gitlab/webhook_handler.py +++ b/biz/gitlab/webhook_handler.py @@ -47,6 +47,13 @@ def slugify_url(original_url: str) -> str: return target +def verify_gitlab_webhook_secret_token(secret_token_env, secret_token_request): + # 仅当设置了环境变量时才验证秘密令牌 + if secret_token_env: + if secret_token_env != secret_token_request: + return False + return True + class MergeRequestHandler: def __init__(self, webhook_data: dict, gitlab_token: str, gitlab_url: str): diff --git a/conf/.env.dist b/conf/.env.dist index 58572906d..6bd71116c 100644 --- a/conf/.env.dist +++ b/conf/.env.dist @@ -55,9 +55,13 @@ REPORT_CRONTAB_EXPRESSION=0 18 * * 1-5 #Gitlab配置 #GITLAB_URL={YOUR_GITLAB_URL} #部分老版本Gitlab webhook不传递URL,需要开启此配置,示例:https://gitlab.example.com #GITLAB_ACCESS_TOKEN={YOUR_GITLAB_ACCESS_TOKEN} #系统会优先使用此GITLAB_ACCESS_TOKEN,如果未配置,则使用Webhook 传递的Secret Token +#VALIDATE_GITLAB_WEBHOOK_SECRET_TOKEN=1 +#GITLAB_WEBHOOK_SECRET_TOKEN={YOUR_GITLAB_WEBHOOK_SECRET_TOKEN} #在 webhook 中使用相同的秘密令牌来验证其签名 #Github配置(如果使用 Github 作为代码托管平台,需要配置此项) #GITHUB_ACCESS_TOKEN={YOUR_GITHUB_ACCESS_TOKEN} +#VALIDATE_GITHUB_ACCESS_TOKEN=1 +#GITHUB_WEBHOOK_SECRET_TOKEN={YOUR_GITHUB_WEBHOOK_SECRET_TOKEN} #在 webhook 中使用相同的秘密令牌来验证其签名 # 开启Push Review功能(如果不需要push事件触发Code Review,设置为0) PUSH_REVIEW_ENABLED=1