From 6c57f8d5a718272c33cdc21e415aa720cc3716fb Mon Sep 17 00:00:00 2001 From: Alex Assistant Date: Thu, 19 Feb 2026 16:57:09 +0300 Subject: [PATCH] Add secure deploy webhook with local config example --- .gitignore | 3 ++ README.md | 33 ++++++++++++--- deploy-config.php.example | 25 ++++++++++++ deploy.php | 86 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 deploy-config.php.example create mode 100644 deploy.php diff --git a/.gitignore b/.gitignore index 88052f0..ff265ae 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,8 @@ Thumbs.db .idea/ .vscode/ +# Local secrets +deploy-config.php + # Logs/temp *.log diff --git a/README.md b/README.md index 29ea61b..449e973 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - при каждом открытии страницы проверяет, появились ли новые/обновлённые фото, - создаёт и обновляет превью в `thumbs/`, - показывает категории и фото в веб-интерфейсе, -- открывает большую фотографию в лайтбоксе или в новой вкладке. +- открывает большую фотографию в лайтбоксе. ## Структура @@ -14,11 +14,13 @@ photo-gallery/ ├─ index.php # основной скрипт: индексация + HTML ├─ style.css # стили (material-like, строгий) -├─ app.js # лайтбокс -├─ photos/ # исходные фото по категориям (папкам) -├─ thumbs/ # автогенерируемые превью +├─ app.js # лайтбокс +├─ deploy.php # webhook-триггер деплоя +├─ deploy-config.php.example # пример конфига webhook +├─ photos/ # исходные фото по категориям (папкам) +├─ thumbs/ # автогенерируемые превью └─ data/ - └─ last_indexed.txt # timestamp последней индексации + └─ last_indexed.txt # timestamp последней индексации ``` ## Как работает индексация @@ -89,6 +91,27 @@ bash scripts/deploy.sh BRANCH=master bash scripts/deploy.sh ``` +## Удалённый запуск деплоя по ссылке (webhook) + +1. На хостинге создай конфиг из примера: + +```bash +cp deploy-config.php.example deploy-config.php +``` + +2. Заполни в `deploy-config.php` минимум: +- `token` (длинный секрет) +- при желании `allowed_ips` +- при желании `basic_auth_user/basic_auth_pass` + +3. Запуск деплоя: + +```text +https://<домен>/deploy.php?token=<твой_секрет> +``` + +Рекомендация: включить IP whitelist и Basic Auth. + ## Примечания - Превью генерируются в формате JPEG с качеством ~82. diff --git a/deploy-config.php.example b/deploy-config.php.example new file mode 100644 index 0000000..b5ec4ae --- /dev/null +++ b/deploy-config.php.example @@ -0,0 +1,25 @@ + 'CHANGE_ME_TO_LONG_RANDOM_TOKEN', + + // Optional: HTTP Basic auth layer + 'basic_auth_user' => '', + 'basic_auth_pass' => '', + + // Optional: allow only these client IPs (empty = allow all) + 'allowed_ips' => [ + // '1.2.3.4', + ], + + // Deploy options + 'branch' => 'main', + 'deploy_script' => __DIR__ . '/scripts/deploy.sh', + + // Where to write webhook logs + 'log_file' => __DIR__ . '/data/deploy-webhook.log', +]; diff --git a/deploy.php b/deploy.php new file mode 100644 index 0000000..c321ec3 --- /dev/null +++ b/deploy.php @@ -0,0 +1,86 @@ + $config */ +$config = require $configPath; +$tokenExpected = (string)($config['token'] ?? ''); +$allowedIps = (array)($config['allowed_ips'] ?? []); +$basicUser = (string)($config['basic_auth_user'] ?? ''); +$basicPass = (string)($config['basic_auth_pass'] ?? ''); +$branch = (string)($config['branch'] ?? 'main'); +$deployScript = (string)($config['deploy_script'] ?? (__DIR__ . '/scripts/deploy.sh')); +$logFile = (string)($config['log_file'] ?? (__DIR__ . '/data/deploy-webhook.log')); + +header('Content-Type: text/plain; charset=utf-8'); + +if ($basicUser !== '' || $basicPass !== '') { + $authUser = $_SERVER['PHP_AUTH_USER'] ?? ''; + $authPass = $_SERVER['PHP_AUTH_PW'] ?? ''; + if (!hash_equals($basicUser, (string)$authUser) || !hash_equals($basicPass, (string)$authPass)) { + header('WWW-Authenticate: Basic realm="Deploy"'); + http_response_code(401); + echo "Unauthorized\n"; + exit; + } +} + +$clientIp = (string)($_SERVER['REMOTE_ADDR'] ?? 'unknown'); +if ($allowedIps !== [] && !in_array($clientIp, $allowedIps, true)) { + http_response_code(403); + echo "Forbidden: IP not allowed\n"; + logLine($logFile, "DENY ip={$clientIp} reason=ip_not_allowed"); + exit; +} + +$tokenIncoming = (string)($_REQUEST['token'] ?? ''); +if ($tokenExpected === '' || !hash_equals($tokenExpected, $tokenIncoming)) { + http_response_code(403); + echo "Forbidden: invalid token\n"; + logLine($logFile, "DENY ip={$clientIp} reason=bad_token"); + exit; +} + +if (!is_file($deployScript)) { + http_response_code(500); + echo "Deploy script not found\n"; + logLine($logFile, "ERROR ip={$clientIp} reason=script_missing path={$deployScript}"); + exit; +} + +$cmd = 'BRANCH=' . escapeshellarg($branch) . ' bash ' . escapeshellarg($deployScript) . ' 2>&1'; +exec($cmd, $output, $code); + +$preview = implode("\n", array_slice($output, -30)); +logLine( + $logFile, + "RUN ip={$clientIp} code={$code} branch={$branch} output=" . str_replace(["\n", "\r"], ['\\n', ''], $preview) +); + +if ($code !== 0) { + http_response_code(500); + echo "Deploy failed\n\n"; + echo $preview . "\n"; + exit; +} + +echo "OK: deploy completed\n\n"; +echo $preview . "\n"; + +function logLine(string $path, string $line): void +{ + $dir = dirname($path); + if (!is_dir($dir)) { + @mkdir($dir, 0775, true); + } + $ts = date('Y-m-d H:i:s'); + @file_put_contents($path, "[{$ts}] {$line}\n", FILE_APPEND); +}