From 9b3538c77ab2d0b202bf2501dfa6b53e4f14bc51 Mon Sep 17 00:00:00 2001 From: wikiboss Date: Thu, 8 Oct 2026 16:27:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8C=96=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC=E3=80=81Webhook?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E4=B8=8ETDD=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=A5=97=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 42 +++++ application/common/library/WebhookHandler.php | 94 ++++++++++ deploy.sh | 57 ++++++ public/webhook.php | 49 +++++ tests/WebhookTest.php | 175 ++++++++++++++++++ 5 files changed, 417 insertions(+) create mode 100644 AGENTS.md create mode 100644 application/common/library/WebhookHandler.php create mode 100644 deploy.sh create mode 100644 public/webhook.php create mode 100644 tests/WebhookTest.php diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f07e273 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ + + + + +\### 站点信息 + +118.184.172.94 + +root + +密码Bjsslxz1 + +目录 + +/www/wwwroot/www.gynhc.com + +https://www.gynhc.com/a.php + + + +远程代码仓 + +https://gitcode.gynhc.com/wikiboss/nahaichuan.git + + + +浅色专业风 + +背景:#FFFFFF / #F8FAFC + +主色:#2563EB 科技蓝 + +辅助:#06B6D4 青、#7C3AED 紫 + +文字:#0F172A / #475569 + + + + + +现代中式高端软件公司官网。主菜单:服务、产品、案例、关于我们、联系我们。独立页面:服务、产品、案例。风格:严谨商务、高级感、现代中式、浅色专业风。背景 #FFFFFF 与 #F8FAFC,主色 #2563EB,辅助 #06B6D4 与 #7C3AED,文字 #0F172A 与 #475569。标题宋体,正文黑体或 Inter。12 列网格,1280px,大留白,细线,水墨抽象,窗棂,印章点缀,圆角 4 至 6px。服务:软件开发、APP与小程序定制、智能体开发。产品:商城、收银、进销存、SaaS版本。首页含首屏、服务产品案例入口、流程、评价、CTA。服务页含分类、能力、流程、交付、FAQ。产品页含矩阵、功能、版本、定价、演示。案例页含筛选、挑战方案结果、数据。关于我们含品牌、团队、资质。联系我们含表单、地图、电话。响应式,禁止 emoji、卡通、霓虹、赛博朋克、国潮堆砌。 + diff --git a/application/common/library/WebhookHandler.php b/application/common/library/WebhookHandler.php new file mode 100644 index 0000000..6de0e96 --- /dev/null +++ b/application/common/library/WebhookHandler.php @@ -0,0 +1,94 @@ +secret = $secret; + $this->targetBranch = $targetBranch; + $this->deployExecutor = $deployExecutor; + } + + public function handle(array $server, array $get, string $rawBody): array + { + // 1. 验证权限 (支持 Header 传 Token、URL Token 以及 HMAC-SHA256 签名) + if (!$this->verifyAuth($server, $get, $rawBody)) { + return ['code' => 403, 'data' => ['status' => 'error', 'message' => 'Invalid token']]; + } + + // 2. 验证 HTTP 方式 + $method = strtoupper($server['REQUEST_METHOD'] ?? 'GET'); + if ($method !== 'POST') { + return ['code' => 405, 'data' => ['status' => 'error', 'message' => 'Method not allowed']]; + } + + // 3. 响应 Ping / Test 事件 + $event = strtolower($server['HTTP_X_GITEA_EVENT'] ?? $server['HTTP_X_GIT_EVENT'] ?? $server['HTTP_X_GITHUB_EVENT'] ?? ''); + if ($event === 'ping') { + return ['code' => 200, 'data' => ['status' => 'success', 'message' => 'pong']]; + } + + // 4. 分支过滤 + $payload = json_decode($rawBody, true) ?: []; + $ref = $payload['ref'] ?? ''; + $branch = str_replace('refs/heads/', '', $ref); + + if ($branch !== $this->targetBranch) { + return [ + 'code' => 200, + 'data' => [ + 'status' => 'skipped', + 'message' => "Branch {$branch} ignored, target is {$this->targetBranch}" + ] + ]; + } + + // 5. 触发部署 + $output = ''; + if (is_callable($this->deployExecutor)) { + $output = call_user_func($this->deployExecutor, $payload); + } + + return [ + 'code' => 200, + 'data' => [ + 'status' => 'success', + 'message' => 'Deployment triggered', + 'branch' => $branch, + 'output' => $output + ] + ]; + } + + private function verifyAuth(array $server, array $get, string $rawBody): bool + { + // A. 明文 Token 校验 + $token = $server['HTTP_X_GITEA_TOKEN'] ?? $server['HTTP_X_GIT_TOKEN'] ?? $server['HTTP_X_GITLAB_TOKEN'] ?? ($get['token'] ?? ''); + if ($token !== '' && hash_equals($this->secret, $token)) { + return true; + } + + // B. HMAC-SHA256 签名校验 (Gitea / GitHub) + $signature = $server['HTTP_X_GITEA_SIGNATURE'] ?? $server['HTTP_X_HUB_SIGNATURE_256'] ?? ''; + if ($signature !== '') { + if (strpos($signature, 'sha256=') === 0) { + $signature = substr($signature, 7); + } + $expected = hash_hmac('sha256', $rawBody, $this->secret); + if (hash_equals($expected, $signature)) { + return true; + } + } + + return false; + } +} diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..951a258 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# 纳海川官网自动部署脚本 +set -e + +DEPLOY_DIR="/www/wwwroot/www.gynhc.com" +LOG_FILE="${DEPLOY_DIR}/runtime/deploy.log" +TARGET_BRANCH="${1:-master}" +DATE=$(date '+%Y-%m-%d %H:%M:%S') + +echo "[${DATE}] >>> 开始部署分支: ${TARGET_BRANCH}..." >> "${LOG_FILE}" + +cd "${DEPLOY_DIR}" + +# 验证是否为 git 仓库 +if [ ! -d ".git" ]; then + echo "[${DATE}] 错误: ${DEPLOY_DIR} 尚未初始化为 Git 仓库" >> "${LOG_FILE}" + echo "Git repo not initialized" + exit 1 +fi + +# 备份关键环境配置 +if [ -f "application/database.php" ]; then + cp -p application/database.php application/database.php.deploy_bak +fi + +# 忽略文件权限变化 +git config core.fileMode false + +# 拉取最新代码并硬重置 +echo "[${DATE}] 执行 git fetch origin ${TARGET_BRANCH}..." >> "${LOG_FILE}" +git fetch origin "${TARGET_BRANCH}" >> "${LOG_FILE}" 2>&1 +git reset --hard "origin/${TARGET_BRANCH}" >> "${LOG_FILE}" 2>&1 + +# 还原配置文件保护 +if [ -f "application/database.php.deploy_bak" ]; then + cp -p application/database.php.deploy_bak application/database.php + rm -f application/database.php.deploy_bak +fi + +# 保持目录可写 +chmod -R 777 runtime 2>/dev/null || true +chmod -R 775 public/uploads 2>/dev/null || true + +# 平滑重启 PHP-FPM 刷新 OPcache +if [ -x "/etc/init.d/php-fpm-73" ]; then + if command -v sudo >/dev/null 2>&1; then + sudo -n /etc/init.d/php-fpm-73 reload >> "${LOG_FILE}" 2>&1 || true + else + /etc/init.d/php-fpm-73 reload >> "${LOG_FILE}" 2>&1 || true + fi +fi + +COMMIT_INFO=$(git log -1 --pretty=format:"%h - %s (%an, %cd)") +echo "[${DATE}] 当前最新版本: ${COMMIT_INFO}" >> "${LOG_FILE}" +echo "[${DATE}] >>> 部署成功" >> "${LOG_FILE}" + +echo "Deploy success: ${COMMIT_INFO}" diff --git a/public/webhook.php b/public/webhook.php new file mode 100644 index 0000000..fa30566 --- /dev/null +++ b/public/webhook.php @@ -0,0 +1,49 @@ +&1"; + $output = shell_exec($cmd); + return trim($output ?? ''); +}; + +$handler = new \app\common\library\WebhookHandler($secret, 'master', $deployExecutor); +$rawBody = file_get_contents('php://input') ?: ''; +$result = $handler->handle($_SERVER, $_GET, $rawBody); + +// 记录日志 +$date = date('Y-m-d H:i:s'); +$ip = $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN'; +$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; +$status = $result['data']['status'] ?? 'unknown'; +$msg = $result['data']['message'] ?? ''; +$out = $result['data']['output'] ?? ''; + +$logEntry = "[{$date}] [{$ip}] {$method} => Status: {$status}, Msg: {$msg}"; +if (!empty($out)) { + $logEntry .= "\nDeploy Output:\n" . $out; +} +$logEntry .= "\n" . str_repeat('-', 50) . "\n"; + +@file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX); + +http_response_code($result['code']); +echo json_encode($result['data'], JSON_UNESCAPED_UNICODE); diff --git a/tests/WebhookTest.php b/tests/WebhookTest.php new file mode 100644 index 0000000..4df52bc --- /dev/null +++ b/tests/WebhookTest.php @@ -0,0 +1,175 @@ +testInvalidTokenRejection(); + $this->testUrlTokenAllowed(); + $this->testHeaderTokenAllowed(); + $this->testHmacSha256SignatureAllowed(); + $this->testHubSignature256Allowed(); + $this->testNonPostMethodRejection(); + $this->testPingEventSuccess(); + $this->testIgnoredBranchSkipped(); + $this->testTargetBranchTriggersDeploy(); + + echo "\n测试结果: {$this->passed} 通过, {$this->failed} 失败\n"; + if ($this->failed > 0) { + exit(1); + } + } + + private function assert($condition, $message) + { + if ($condition) { + echo " [PASS] {$message}\n"; + $this->passed++; + } else { + echo " [FAIL] {$message}\n"; + $this->failed++; + } + } + + /** + * 测试非法 Token 被拦截 + */ + public function testInvalidTokenRejection() + { + $handler = new \app\common\library\WebhookHandler('test_secret_123'); + $server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_GITEA_TOKEN' => 'wrong_token']; + $res = $handler->handle($server, [], '{}'); + + $this->assert($res['code'] === 403, "非法 Token 应返回 HTTP 403"); + $this->assert($res['data']['status'] === 'error', "非法 Token 应为 error 状态"); + } + + /** + * 测试通过 URL 传 Token 允许通过 + */ + public function testUrlTokenAllowed() + { + $handler = new \app\common\library\WebhookHandler('test_secret_123'); + $server = ['REQUEST_METHOD' => 'POST']; + $get = ['token' => 'test_secret_123']; + $res = $handler->handle($server, $get, json_encode(['ref' => 'refs/heads/master'])); + + $this->assert($res['code'] === 200, "合法 URL Token 应返回 HTTP 200"); + } + + /** + * 测试通过 Header 传 Token 允许通过 + */ + public function testHeaderTokenAllowed() + { + $handler = new \app\common\library\WebhookHandler('test_secret_123'); + $server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_GITEA_TOKEN' => 'test_secret_123']; + $res = $handler->handle($server, [], json_encode(['ref' => 'refs/heads/master'])); + + $this->assert($res['code'] === 200, "合法 Header Token 应返回 HTTP 200"); + } + + /** + * 测试通过 Gitea HMAC-SHA256 签名校验 + */ + public function testHmacSha256SignatureAllowed() + { + $handler = new \app\common\library\WebhookHandler('test_secret_123'); + $body = json_encode(['ref' => 'refs/heads/master']); + $sig = hash_hmac('sha256', $body, 'test_secret_123'); + $server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_GITEA_SIGNATURE' => $sig]; + $res = $handler->handle($server, [], $body); + + $this->assert($res['code'] === 200, "合法 HMAC-SHA256 签名应返回 HTTP 200"); + } + + /** + * 测试通过 GitHub/Gitea sha256= 前缀签名校验 + */ + public function testHubSignature256Allowed() + { + $handler = new \app\common\library\WebhookHandler('test_secret_123'); + $body = json_encode(['ref' => 'refs/heads/master']); + $sig = 'sha256=' . hash_hmac('sha256', $body, 'test_secret_123'); + $server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_HUB_SIGNATURE_256' => $sig]; + $res = $handler->handle($server, [], $body); + + $this->assert($res['code'] === 200, "合法 X-Hub-Signature-256 签名应返回 HTTP 200"); + } + + /** + * 测试非 POST 请求被拒绝 + */ + public function testNonPostMethodRejection() + { + $handler = new \app\common\library\WebhookHandler('test_secret_123'); + $server = ['REQUEST_METHOD' => 'GET']; + $get = ['token' => 'test_secret_123']; + $res = $handler->handle($server, $get, ''); + + $this->assert($res['code'] === 405, "非 POST 请求应返回 HTTP 405"); + } + + /** + * 测试 Gitea Ping / Test 事件直接响应成功 + */ + public function testPingEventSuccess() + { + $handler = new \app\common\library\WebhookHandler('test_secret_123'); + $server = [ + 'REQUEST_METHOD' => 'POST', + 'HTTP_X_GITEA_EVENT' => 'ping', + 'HTTP_X_GITEA_TOKEN' => 'test_secret_123' + ]; + $res = $handler->handle($server, [], '{}'); + + $this->assert($res['code'] === 200, "Ping 事件应返回 HTTP 200"); + $this->assert($res['data']['message'] === 'pong', "Ping 事件应返回 pong 响应"); + } + + /** + * 测试非目标分支自动跳过 + */ + public function testIgnoredBranchSkipped() + { + $handler = new \app\common\library\WebhookHandler('test_secret_123', 'master'); + $server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_GITEA_TOKEN' => 'test_secret_123']; + $body = json_encode(['ref' => 'refs/heads/feature_test']); + $res = $handler->handle($server, [], $body); + + $this->assert($res['code'] === 200, "非目标分支应返回 HTTP 200"); + $this->assert($res['data']['status'] === 'skipped', "非目标分支状态应为 skipped"); + } + + /** + * 测试目标分支正确触发回调 + */ + public function testTargetBranchTriggersDeploy() + { + $triggered = false; + $handler = new \app\common\library\WebhookHandler('test_secret_123', 'master', function () use (&$triggered) { + $triggered = true; + return "Deploy OK"; + }); + + $server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_GITEA_TOKEN' => 'test_secret_123']; + $body = json_encode(['ref' => 'refs/heads/master']); + $res = $handler->handle($server, [], $body); + + $this->assert($res['code'] === 200, "目标分支应返回 HTTP 200"); + $this->assert($triggered === true, "目标分支必须触发回调"); + $this->assert($res['data']['status'] === 'success', "部署状态应为 success"); + $this->assert($res['data']['output'] === 'Deploy OK', "输出应一致"); + } +} + +$test = new WebhookTest(); +$test->run();