config = $config; $this->logDir = dirname(__DIR__) . '/deploy/logs'; if (!is_dir($this->logDir)) { @mkdir($this->logDir, 0755, true); } } public function handle(): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->respond(405, 'Method Not Allowed'); } $this->checkIp(); $payload = file_get_contents('php://input'); if ($payload === false || $payload === '') { $this->respond(400, 'Empty payload'); } $this->verifySignature($payload); $data = json_decode($payload, true); if (!is_array($data)) { $this->respond(400, 'Invalid JSON'); } $event = $_SERVER['HTTP_X_GITEA_EVENT'] ?? $_SERVER['HTTP_X_GOGS_EVENT'] ?? ''; if ($event === 'ping') { $this->respond(200, 'pong'); } if ($event !== 'push') { $this->respond(200, 'ignored: ' . $event); } $ref = $data['ref'] ?? ''; $branch = $this->config['branch'] ?? 'main'; if ($ref !== 'refs/heads/' . $branch) { $this->respond(200, 'ignored branch: ' . $ref); } $this->log('Push received for ' . $ref . ', starting deploy'); $output = $this->runDeploy(); $this->respond(200, $output); } private function checkIp(): void { $allowed = $this->config['allowed_ips'] ?? []; if ($allowed === [] || $allowed === null) { return; } $ip = $_SERVER['REMOTE_ADDR'] ?? ''; if (!in_array($ip, $allowed, true)) { $this->log('Rejected IP: ' . $ip); $this->respond(403, 'Forbidden'); } } private function verifySignature($payload) { $secret = $this->config['webhook_secret'] ?? ''; if ($secret === '' || $secret === '请改为随机长字符串') { $this->respond(500, 'Webhook secret not configured'); } $signature = $_SERVER['HTTP_X_GITEA_SIGNATURE'] ?? $_SERVER['HTTP_X_GOGS_SIGNATURE'] ?? ''; if ($signature === '') { $this->respond(403, 'Missing signature'); } $expected = hash_hmac('sha256', $payload, $secret); if (!hash_equals($expected, $signature)) { $this->log('Invalid signature'); $this->respond(403, 'Invalid signature'); } } private function runDeploy() { $deployPath = rtrim($this->config['deploy_path'] ?? '', '/'); if ($deployPath === '' || !is_dir($deployPath)) { $this->respond(500, 'deploy_path invalid'); } $script = $this->config['deploy_script'] ?? 'scripts/gitea-deploy.sh'; if ($script[0] !== '/') { $script = $deployPath . '/' . ltrim($script, '/'); } if (!is_file($script)) { $this->respond(500, 'deploy script missing: ' . $script); } $lock = $this->logDir . '/deploy.lock'; $lockFp = fopen($lock, 'c'); if ($lockFp === false || !flock($lockFp, LOCK_EX | LOCK_NB)) { $this->respond(409, 'Deploy already running'); } $env = 'DEPLOY_PATH=' . escapeshellarg($deployPath); if (!empty($this->config['run_composer'])) { $env .= ' RUN_COMPOSER=1'; } $branch = preg_replace('/[^a-zA-Z0-9_\\/-]/', '', $this->config['branch'] ?? 'main'); $env .= ' DEPLOY_BRANCH=' . escapeshellarg($branch); $cmd = $env . ' bash ' . escapeshellarg($script) . ' 2>&1'; $this->log('Exec: ' . $cmd); $output = []; $code = 0; exec($cmd, $output, $code); $text = implode("\n", $output); $this->log("Exit $code\n$text"); flock($lockFp, LOCK_UN); fclose($lockFp); if ($code !== 0) { $this->respond(500, "Deploy failed ($code):\n$text"); } return "Deploy OK\n$text"; } private function log($message) { $line = date('Y-m-d H:i:s') . ' ' . $message . "\n"; @file_put_contents($this->logDir . '/webhook.log', $line, FILE_APPEND | LOCK_EX); } private function respond($code, $body) { http_response_code($code); header('Content-Type: text/plain; charset=utf-8'); echo $body; exit; } }