Deploy sync from local workspace (v0.9-rc63)

This commit is contained in:
Jabali Deploy
2026-02-12 00:41:14 +00:00
parent 5d502699ea
commit 0c6402604d
20 changed files with 481 additions and 176 deletions

View File

@@ -58,6 +58,7 @@ php artisan route:cache # Cache routes
## Git Workflow ## Git Workflow
**Important:** Only push to git when explicitly requested by the user. Do not auto-push after commits. **Important:** Only push to git when explicitly requested by the user. Do not auto-push after commits.
**Important:** Push to GitHub from the test server `root@192.168.100.50` (where the GitHub deploy key is configured).
### Version Numbers ### Version Numbers

View File

@@ -17,6 +17,7 @@ Rules and behavior for automated agents working on Jabali.
- Do not push unless the user explicitly asks. - Do not push unless the user explicitly asks.
- Bump `VERSION` before every push. - Bump `VERSION` before every push.
- Keep `install.sh` version fallback in sync with `VERSION`. - Keep `install.sh` version fallback in sync with `VERSION`.
- Push to GitHub from `root@192.168.100.50`.
## Operational ## Operational
- If you add dependencies, update both install and uninstall paths. - If you add dependencies, update both install and uninstall paths.

View File

@@ -160,6 +160,13 @@ Service stack (single-node default):
- PTR (reverse DNS) for mail hostname - PTR (reverse DNS) for mail hostname
- Open ports: 22, 80, 443, 25, 465, 587, 993, 995, 53 - Open ports: 22, 80, 443, 25, 465, 587, 993, 995, 53
## Security Hardening
- `TRUSTED_PROXIES`: comma-separated proxy IPs/CIDRs (or `*` if you intentionally trust all upstream proxies).
- `JABALI_INTERNAL_API_TOKEN`: optional shared token for internal API calls that do not originate from localhost.
- `JABALI_IMPORT_INSECURE_TLS`: optional escape hatch for remote migration discovery. Leave unset for strict TLS verification.
- Git deployment webhooks support signed payloads via `X-Jabali-Signature` / `X-Hub-Signature-256` (HMAC-SHA256).
## Upgrades ## Upgrades
``` ```

View File

@@ -1 +1 @@
VERSION=0.9-rc62 VERSION=0.9-rc63

View File

@@ -83,7 +83,7 @@ class GitDeployment extends Page implements HasActions, HasForms, HasTable
protected function getWebhookUrl(GitDeploymentModel $deployment): string protected function getWebhookUrl(GitDeploymentModel $deployment): string
{ {
return url("/api/webhooks/git/{$deployment->id}/{$deployment->secret_token}"); return url("/api/webhooks/git/{$deployment->id}");
} }
protected function getDeployKey(): string protected function getDeployKey(): string
@@ -162,6 +162,11 @@ class GitDeployment extends Page implements HasActions, HasForms, HasTable
->rows(2) ->rows(2)
->disabled() ->disabled()
->dehydrated(false), ->dehydrated(false),
TextInput::make('webhook_secret')
->label(__('Webhook Secret'))
->helperText(__('Set this as your provider webhook secret. Jabali validates HMAC-SHA256 signatures.'))
->disabled()
->dehydrated(false),
Textarea::make('deploy_key') Textarea::make('deploy_key')
->label(__('Deploy Key')) ->label(__('Deploy Key'))
->rows(3) ->rows(3)
@@ -170,6 +175,7 @@ class GitDeployment extends Page implements HasActions, HasForms, HasTable
]) ])
->fillForm(fn (GitDeploymentModel $record): array => [ ->fillForm(fn (GitDeploymentModel $record): array => [
'webhook_url' => $this->getWebhookUrl($record), 'webhook_url' => $this->getWebhookUrl($record),
'webhook_secret' => $record->secret_token,
'deploy_key' => $this->getDeployKey(), 'deploy_key' => $this->getDeployKey(),
]), ]),
Action::make('edit') Action::make('edit')

View File

@@ -11,9 +11,17 @@ use Illuminate\Http\Request;
class GitWebhookController extends Controller class GitWebhookController extends Controller
{ {
public function __invoke(Request $request, GitDeployment $deployment, string $token): JsonResponse public function __invoke(Request $request, GitDeployment $deployment, ?string $token = null): JsonResponse
{ {
if (! hash_equals($deployment->secret_token, $token)) { $payload = $request->getContent();
$providedSignature = (string) ($request->header('X-Jabali-Signature') ?? $request->header('X-Hub-Signature-256') ?? '');
$providedSignature = preg_replace('/^sha256=/i', '', trim($providedSignature)) ?: '';
$expectedSignature = hash_hmac('sha256', $payload, $deployment->secret_token);
$hasValidSignature = $providedSignature !== '' && hash_equals($expectedSignature, $providedSignature);
$hasValidLegacyToken = $token !== null && hash_equals($deployment->secret_token, $token);
if (! $hasValidSignature && ! $hasValidLegacyToken) {
return response()->json(['message' => 'Invalid token'], 403); return response()->json(['message' => 'Invalid token'], 403);
} }

View File

@@ -5,17 +5,18 @@ namespace App\Providers;
use App\Models\Domain; use App\Models\Domain;
use App\Observers\DomainObserver; use App\Observers\DomainObserver;
use Filament\Support\Facades\FilamentAsset; use Filament\Support\Facades\FilamentAsset;
use Illuminate\Support\ServiceProvider; use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
/** /**
* Register any application services. * Register any application services.
*/ */
public function register(): void public function register(): void {}
{
}
/** /**
* Bootstrap any application services. * Bootstrap any application services.
@@ -24,6 +25,31 @@ class AppServiceProvider extends ServiceProvider
{ {
Domain::observe(DomainObserver::class); Domain::observe(DomainObserver::class);
RateLimiter::for('api', function (Request $request): array {
$identifier = $request->user()?->getAuthIdentifier() ?? $request->ip();
return [
Limit::perMinute(120)->by('api:'.$identifier),
];
});
RateLimiter::for('internal-api', function (Request $request): array {
$remoteAddr = (string) $request->server('REMOTE_ADDR', $request->ip());
return [
Limit::perMinute(60)->by('internal:'.$remoteAddr),
];
});
RateLimiter::for('git-webhooks', function (Request $request): array {
$deploymentId = $request->route('deployment');
$deploymentKey = is_object($deploymentId) ? (string) $deploymentId->getKey() : (string) $deploymentId;
return [
Limit::perMinute(120)->by('webhook:'.$deploymentKey.':'.$request->ip()),
];
});
$versionFile = base_path('VERSION'); $versionFile = base_path('VERSION');
$appVersion = File::exists($versionFile) ? trim(File::get($versionFile)) : null; $appVersion = File::exists($versionFile) ? trim(File::get($versionFile)) : null;
FilamentAsset::appVersion($appVersion ?: null); FilamentAsset::appVersion($appVersion ?: null);

View File

@@ -13056,6 +13056,37 @@ function importDiscover(array $params): array
} }
} }
/**
* Whether insecure TLS is allowed for remote import discovery APIs.
*/
function allowInsecureImportTls(): bool
{
$value = strtolower(trim((string) getenv('JABALI_IMPORT_INSECURE_TLS')));
return in_array($value, ['1', 'true', 'yes', 'on'], true);
}
/**
* Configure secure cURL defaults for remote control panel discovery calls.
*/
function configureImportApiCurl($ch): void
{
if (allowInsecureImportTls()) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
return;
}
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
$caBundle = '/etc/ssl/certs/ca-certificates.crt';
if (is_readable($caBundle)) {
curl_setopt($ch, CURLOPT_CAINFO, $caBundle);
}
}
/** /**
* Discover accounts from a cPanel backup file * Discover accounts from a cPanel backup file
*/ */
@@ -13442,8 +13473,7 @@ function discoverCpanelRemote(string $host, int $port, string $user, string $pas
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); configureImportApiCurl($ch);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 60); curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl_setopt($ch, CURLOPT_HTTPHEADER, [ curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: WHM ' . $user . ':' . $password, 'Authorization: WHM ' . $user . ':' . $password,
@@ -13502,8 +13532,7 @@ function discoverDirectAdminRemote(string $host, int $port, string $user, string
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $detailUrl); curl_setopt($ch, CURLOPT_URL, $detailUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); configureImportApiCurl($ch);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_USERPWD, "$user:$password"); curl_setopt($ch, CURLOPT_USERPWD, "$user:$password");
@@ -13536,8 +13565,7 @@ function discoverDirectAdminRemote(string $host, int $port, string $user, string
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); configureImportApiCurl($ch);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 60); curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl_setopt($ch, CURLOPT_USERPWD, "$user:$password"); curl_setopt($ch, CURLOPT_USERPWD, "$user:$password");

View File

@@ -3,6 +3,7 @@
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
@@ -12,7 +13,24 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: '*'); $trustedProxies = env('TRUSTED_PROXIES');
$resolvedProxies = match (true) {
is_string($trustedProxies) && trim($trustedProxies) === '*' => '*',
is_string($trustedProxies) && trim($trustedProxies) !== '' => array_values(array_filter(array_map(
static fn (string $proxy): string => trim($proxy),
explode(',', $trustedProxies)
))),
default => ['127.0.0.1', '::1'],
};
$middleware->trustProxies(
at: $resolvedProxies,
headers: Request::HEADER_X_FORWARDED_FOR
| Request::HEADER_X_FORWARDED_HOST
| Request::HEADER_X_FORWARDED_PORT
| Request::HEADER_X_FORWARDED_PROTO
);
$middleware->throttleApi('api');
$middleware->append(\App\Http\Middleware\SecurityHeaders::class); $middleware->append(\App\Http\Middleware\SecurityHeaders::class);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {

119
composer.lock generated
View File

@@ -5286,16 +5286,16 @@
}, },
{ {
"name": "psy/psysh", "name": "psy/psysh",
"version": "v0.12.18", "version": "v0.12.20",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/bobthecow/psysh.git", "url": "https://github.com/bobthecow/psysh.git",
"reference": "ddff0ac01beddc251786fe70367cd8bbdb258196" "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196", "url": "https://api.github.com/repos/bobthecow/psysh/zipball/19678eb6b952a03b8a1d96ecee9edba518bb0373",
"reference": "ddff0ac01beddc251786fe70367cd8bbdb258196", "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -5359,9 +5359,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/bobthecow/psysh/issues", "issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.12.18" "source": "https://github.com/bobthecow/psysh/tree/v0.12.20"
}, },
"time": "2025-12-17T14:35:46+00:00" "time": "2026-02-11T15:05:28+00:00"
}, },
{ {
"name": "ralouphie/getallheaders", "name": "ralouphie/getallheaders",
@@ -5981,16 +5981,16 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v7.4.3", "version": "v7.4.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6" "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/732a9ca6cd9dfd940c639062d5edbde2f6727fb6", "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894",
"reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6", "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -6055,7 +6055,7 @@
"terminal" "terminal"
], ],
"support": { "support": {
"source": "https://github.com/symfony/console/tree/v7.4.3" "source": "https://github.com/symfony/console/tree/v7.4.4"
}, },
"funding": [ "funding": [
{ {
@@ -6075,7 +6075,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-23T14:50:43+00:00" "time": "2026-01-13T11:36:38+00:00"
}, },
{ {
"name": "symfony/css-selector", "name": "symfony/css-selector",
@@ -7803,16 +7803,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v7.4.3", "version": "v7.4.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f" "reference": "608476f4604102976d687c483ac63a79ba18cc97"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/2f8e1a6cdf590ca63715da4d3a7a3327404a523f", "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97",
"reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f", "reference": "608476f4604102976d687c483ac63a79ba18cc97",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -7844,7 +7844,7 @@
"description": "Executes commands in sub-processes", "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/process/tree/v7.4.3" "source": "https://github.com/symfony/process/tree/v7.4.5"
}, },
"funding": [ "funding": [
{ {
@@ -7864,7 +7864,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-19T10:00:43+00:00" "time": "2026-01-26T15:07:59+00:00"
}, },
{ {
"name": "symfony/routing", "name": "symfony/routing",
@@ -8040,16 +8040,16 @@
}, },
{ {
"name": "symfony/string", "name": "symfony/string",
"version": "v8.0.1", "version": "v8.0.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/string.git", "url": "https://github.com/symfony/string.git",
"reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" "reference": "758b372d6882506821ed666032e43020c4f57194"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
"reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", "reference": "758b372d6882506821ed666032e43020c4f57194",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8106,7 +8106,7 @@
"utf8" "utf8"
], ],
"support": { "support": {
"source": "https://github.com/symfony/string/tree/v8.0.1" "source": "https://github.com/symfony/string/tree/v8.0.4"
}, },
"funding": [ "funding": [
{ {
@@ -8126,7 +8126,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-01T09:13:36+00:00" "time": "2026-01-12T12:37:40+00:00"
}, },
{ {
"name": "symfony/translation", "name": "symfony/translation",
@@ -8383,16 +8383,16 @@
}, },
{ {
"name": "symfony/var-dumper", "name": "symfony/var-dumper",
"version": "v7.4.3", "version": "v7.4.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/var-dumper.git", "url": "https://github.com/symfony/var-dumper.git",
"reference": "7e99bebcb3f90d8721890f2963463280848cba92" "reference": "0e4769b46a0c3c62390d124635ce59f66874b282"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/7e99bebcb3f90d8721890f2963463280848cba92", "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e4769b46a0c3c62390d124635ce59f66874b282",
"reference": "7e99bebcb3f90d8721890f2963463280848cba92", "reference": "0e4769b46a0c3c62390d124635ce59f66874b282",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8446,7 +8446,7 @@
"dump" "dump"
], ],
"support": { "support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.4.3" "source": "https://github.com/symfony/var-dumper/tree/v7.4.4"
}, },
"funding": [ "funding": [
{ {
@@ -8466,7 +8466,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-18T07:04:31+00:00" "time": "2026-01-01T22:13:48+00:00"
}, },
{ {
"name": "tijsverkoyen/css-to-inline-styles", "name": "tijsverkoyen/css-to-inline-styles",
@@ -9820,28 +9820,28 @@
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",
"version": "5.1.0", "version": "5.1.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
"reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903",
"reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.2" "php": ">=8.2"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.0" "phpunit/phpunit": "^11.3"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "5.0-dev" "dev-main": "5.1-dev"
} }
}, },
"autoload": { "autoload": {
@@ -9869,15 +9869,27 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
"security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1"
}, },
"funding": [ "funding": [
{ {
"url": "https://github.com/sebastianbergmann", "url": "https://github.com/sebastianbergmann",
"type": "github" "type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator",
"type": "tidelift"
} }
], ],
"time": "2024-08-27T05:02:59+00:00" "time": "2026-02-02T13:52:54+00:00"
}, },
{ {
"name": "phpunit/php-invoker", "name": "phpunit/php-invoker",
@@ -10065,16 +10077,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "11.5.48", "version": "11.5.53",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "fe3665c15e37140f55aaf658c81a2eb9030b6d89" "reference": "a997a653a82845f1240d73ee73a8a4e97e4b0607"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fe3665c15e37140f55aaf658c81a2eb9030b6d89", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a997a653a82845f1240d73ee73a8a4e97e4b0607",
"reference": "fe3665c15e37140f55aaf658c81a2eb9030b6d89", "reference": "a997a653a82845f1240d73ee73a8a4e97e4b0607",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10089,18 +10101,19 @@
"phar-io/version": "^3.2.1", "phar-io/version": "^3.2.1",
"php": ">=8.2", "php": ">=8.2",
"phpunit/php-code-coverage": "^11.0.12", "phpunit/php-code-coverage": "^11.0.12",
"phpunit/php-file-iterator": "^5.1.0", "phpunit/php-file-iterator": "^5.1.1",
"phpunit/php-invoker": "^5.0.1", "phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1", "phpunit/php-text-template": "^4.0.1",
"phpunit/php-timer": "^7.0.1", "phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2", "sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.3", "sebastian/code-unit": "^3.0.3",
"sebastian/comparator": "^6.3.2", "sebastian/comparator": "^6.3.3",
"sebastian/diff": "^6.0.2", "sebastian/diff": "^6.0.2",
"sebastian/environment": "^7.2.1", "sebastian/environment": "^7.2.1",
"sebastian/exporter": "^6.3.2", "sebastian/exporter": "^6.3.2",
"sebastian/global-state": "^7.0.2", "sebastian/global-state": "^7.0.2",
"sebastian/object-enumerator": "^6.0.1", "sebastian/object-enumerator": "^6.0.1",
"sebastian/recursion-context": "^6.0.3",
"sebastian/type": "^5.1.3", "sebastian/type": "^5.1.3",
"sebastian/version": "^5.0.2", "sebastian/version": "^5.0.2",
"staabm/side-effects-detector": "^1.0.5" "staabm/side-effects-detector": "^1.0.5"
@@ -10146,7 +10159,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.48" "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.53"
}, },
"funding": [ "funding": [
{ {
@@ -10170,7 +10183,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-16T16:26:27+00:00" "time": "2026-02-10T12:28:25+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",
@@ -10344,16 +10357,16 @@
}, },
{ {
"name": "sebastian/comparator", "name": "sebastian/comparator",
"version": "6.3.2", "version": "6.3.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git", "url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9",
"reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10412,7 +10425,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues", "issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy", "security": "https://github.com/sebastianbergmann/comparator/security/policy",
"source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3"
}, },
"funding": [ "funding": [
{ {
@@ -10432,7 +10445,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-10T08:07:46+00:00" "time": "2026-01-24T09:26:40+00:00"
}, },
{ {
"name": "sebastian/complexity", "name": "sebastian/complexity",
@@ -11346,5 +11359,5 @@
"php": "^8.2" "php": "^8.2"
}, },
"platform-dev": {}, "platform-dev": {},
"plugin-api-version": "2.9.0" "plugin-api-version": "2.6.0"
} }

View File

@@ -123,4 +123,15 @@ return [
'store' => env('APP_MAINTENANCE_STORE', 'database'), 'store' => env('APP_MAINTENANCE_STORE', 'database'),
], ],
/*
|--------------------------------------------------------------------------
| Internal API Token
|--------------------------------------------------------------------------
|
| Optional shared token for internal endpoints that may be called from
| non-localhost environments (for example, when using a reverse proxy).
|
*/
'internal_api_token' => env('JABALI_INTERNAL_API_TOKEN'),
]; ];

View File

@@ -15,6 +15,12 @@ This page provides git deployment features for the jabali panel.
- Use git deployment to complete common operational tasks. - Use git deployment to complete common operational tasks.
- Review this page after configuration changes to confirm results. - Review this page after configuration changes to confirm results.
## Webhook Security
- Use the provided `Webhook URL` with the `Webhook Secret`.
- Jabali validates `X-Jabali-Signature` (or `X-Hub-Signature-256`) as HMAC-SHA256 over the raw request body.
- Legacy tokenized webhook URLs remain supported for older integrations.
## Typical examples ## Typical examples
- Example 1: Use git deployment to complete common operational tasks. - Example 1: Use git deployment to complete common operational tasks.

View File

@@ -41,7 +41,7 @@ DNSSEC can be enabled per domain and generates KSK/ZSK keys, DS records, and sig
- Target OS: Fresh Debian 12/13 install with no pre-existing web/mail stack. - Target OS: Fresh Debian 12/13 install with no pre-existing web/mail stack.
- Installer: install.sh, builds assets as www-data and ensures permissions. - Installer: install.sh, builds assets as www-data and ensures permissions.
- Upgrade: php artisan jabali:upgrade manages dependencies, caches, and permissions for public/build and node_modules. - Upgrade: php artisan jabali:upgrade manages dependencies, caches, and permissions for public/build and node_modules.
- Deploy helper: scripts/deploy.sh syncs code to a server, runs composer/npm, migrations, and caches, and can push to Gitea/GitHub with automatic VERSION bump. - Deploy helper: scripts/deploy.sh rsyncs to `root@192.168.100.50`, commits there, bumps VERSION, updates install.sh fallback, pushes to Git remotes from that server, then runs composer/npm, migrations, and caches.
## Packaging ## Packaging
Debian packaging is supported via scripts: Debian packaging is supported via scripts:

View File

@@ -112,9 +112,10 @@ browser (Ctrl+Shift+R) after deployment.
## Deploy script ## Deploy script
The repository ships with a deploy helper at `scripts/deploy.sh`. It syncs the The repository ships with a deploy helper at `scripts/deploy.sh`. It rsyncs the
project to a remote server over SSH, then runs composer/npm, migrations, and project to `root@192.168.100.50:/var/www/jabali`, commits on that server, bumps
cache rebuilds as the web user. `VERSION`, updates the `install.sh` fallback, and pushes to Git remotes from
that server. Then it runs composer/npm, migrations, and cache rebuilds.
Defaults (override via flags or env vars): Defaults (override via flags or env vars):
- Host: `192.168.100.50` - Host: `192.168.100.50`
@@ -137,20 +138,32 @@ scripts/deploy.sh --dry-run
scripts/deploy.sh --skip-npm --skip-cache scripts/deploy.sh --skip-npm --skip-cache
``` ```
Push to Git remotes (optional): Push behavior controls:
``` ```
# Push to Gitea and/or GitHub before deploying # Deploy only (no push)
scripts/deploy.sh --push-gitea --push-github scripts/deploy.sh --skip-push
# Push to explicit URLs # Push only one remote
scripts/deploy.sh --push-gitea --gitea-url http://192.168.100.100:3001/shukivaknin/jabali-panel.git \ scripts/deploy.sh --no-push-github
--push-github --github-url git@github.com:shukiv/jabali-panel.git scripts/deploy.sh --no-push-gitea
# Push to explicit URLs from the test server
scripts/deploy.sh --gitea-url ssh://git@192.168.100.100:2222/shukivaknin/jabali-panel.git \
--github-url git@github.com:shukiv/jabali-panel.git
```
GitHub push location:
```
# Push to GitHub from the test server (required)
ssh root@192.168.100.50
cd /var/www/jabali
git push origin main
``` ```
Notes: Notes:
- `--push-gitea` / `--push-github` require a clean worktree. - Pushes run from `root@192.168.100.50` (not from local machine).
- When pushing, the script bumps `VERSION`, updates the `install.sh` fallback, - Before each push, the script bumps `VERSION`, updates `install.sh` fallback,
and commits the version bump before pushing. and commits on the deploy server.
- Rsync excludes `.env`, `storage/`, `vendor/`, `node_modules/`, `public/build/`, - Rsync excludes `.env`, `storage/`, `vendor/`, `node_modules/`, `public/build/`,
`bootstrap/cache/`, and SQLite DB files. Handle those separately if needed. `bootstrap/cache/`, and SQLite DB files. Handle those separately if needed.
- `--delete` passes `--delete` to rsync (dangerous). - `--delete` passes `--delete` to rsync (dangerous).

View File

@@ -51,6 +51,7 @@ From /var/www/jabali:
- Do not push unless explicitly asked. - Do not push unless explicitly asked.
- Bump VERSION before every push. - Bump VERSION before every push.
- Keep install.sh version fallback in sync with VERSION. - Keep install.sh version fallback in sync with VERSION.
- Push to GitHub from `root@192.168.100.50`.
## Where to Look for Examples ## Where to Look for Examples
- app/Filament/Admin/Pages and app/Filament/Jabali/Pages - app/Filament/Admin/Pages and app/Filament/Jabali/Pages

12
package-lock.json generated
View File

@@ -12,7 +12,7 @@
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"axios": "^1.11.0", "axios": "^1.13.5",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0", "laravel-vite-plugin": "^2.0.0",
"postcss": "^8.4.32", "postcss": "^8.4.32",
@@ -1181,13 +1181,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.2", "version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.11",
"form-data": "^4.0.4", "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },

View File

@@ -11,7 +11,7 @@
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"axios": "^1.11.0", "axios": "^1.13.5",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0", "laravel-vite-plugin": "^2.0.0",
"postcss": "^8.4.32", "postcss": "^8.4.32",

View File

@@ -25,13 +25,26 @@ Route::post('/phpmyadmin/verify-token', function (Request $request) {
Cache::forget('phpmyadmin_token_'.$token); Cache::forget('phpmyadmin_token_'.$token);
return response()->json($data); return response()->json($data);
}); })->middleware('throttle:internal-api');
$allowInternalRequest = static function (Request $request): bool {
$remoteAddr = (string) $request->server('REMOTE_ADDR', $request->ip());
$isLocalRequest = in_array($remoteAddr, ['127.0.0.1', '::1'], true);
$configuredToken = trim((string) config('app.internal_api_token', ''));
$providedToken = trim((string) (
$request->header('X-Jabali-Internal-Token')
?? $request->input('internal_token')
?? ''
));
$hasValidToken = $configuredToken !== '' && $providedToken !== '' && hash_equals($configuredToken, $providedToken);
return $isLocalRequest || $hasValidToken;
};
// Internal API for jabali-cache WordPress plugin // Internal API for jabali-cache WordPress plugin
Route::post('/internal/page-cache', function (Request $request) { Route::post('/internal/page-cache', function (Request $request) use ($allowInternalRequest) {
// Only allow requests from localhost if (! $allowInternalRequest($request)) {
$clientIp = $request->ip();
if (! in_array($clientIp, ['127.0.0.1', '::1', 'localhost'])) {
return response()->json(['error' => 'Forbidden'], 403); return response()->json(['error' => 'Forbidden'], 403);
} }
@@ -68,7 +81,7 @@ Route::post('/internal/page-cache', function (Request $request) {
if (preg_match("/define\s*\(\s*['\"]AUTH_KEY['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $wpConfig, $matches)) { if (preg_match("/define\s*\(\s*['\"]AUTH_KEY['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $wpConfig, $matches)) {
$authKey = $matches[1]; $authKey = $matches[1];
$expectedSecret = substr(md5($authKey), 0, 32); $expectedSecret = substr(md5($authKey), 0, 32);
if ($secret !== $expectedSecret) { if (! hash_equals($expectedSecret, $secret)) {
return response()->json(['error' => 'Invalid secret'], 401); return response()->json(['error' => 'Invalid secret'], 401);
} }
} else { } else {
@@ -94,13 +107,11 @@ Route::post('/internal/page-cache', function (Request $request) {
} catch (\Exception $e) { } catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500); return response()->json(['error' => $e->getMessage()], 500);
} }
}); })->middleware('throttle:internal-api');
// Internal API for smart page cache purging (called by jabali-cache WordPress plugin) // Internal API for smart page cache purging (called by jabali-cache WordPress plugin)
Route::post('/internal/page-cache-purge', function (Request $request) { Route::post('/internal/page-cache-purge', function (Request $request) use ($allowInternalRequest) {
// Only allow requests from localhost if (! $allowInternalRequest($request)) {
$clientIp = $request->ip();
if (! in_array($clientIp, ['127.0.0.1', '::1', 'localhost'])) {
return response()->json(['error' => 'Forbidden'], 403); return response()->json(['error' => 'Forbidden'], 403);
} }
@@ -137,7 +148,7 @@ Route::post('/internal/page-cache-purge', function (Request $request) {
if (preg_match("/define\s*\(\s*['\"]AUTH_KEY['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $wpConfig, $matches)) { if (preg_match("/define\s*\(\s*['\"]AUTH_KEY['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)/", $wpConfig, $matches)) {
$authKey = $matches[1]; $authKey = $matches[1];
$expectedSecret = substr(md5($authKey), 0, 32); $expectedSecret = substr(md5($authKey), 0, 32);
if ($secret !== $expectedSecret) { if (! hash_equals($expectedSecret, $secret)) {
return response()->json(['error' => 'Invalid secret'], 401); return response()->json(['error' => 'Invalid secret'], 401);
} }
} else { } else {
@@ -164,9 +175,12 @@ Route::post('/internal/page-cache-purge', function (Request $request) {
} catch (\Exception $e) { } catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500); return response()->json(['error' => $e->getMessage()], 500);
} }
}); })->middleware('throttle:internal-api');
Route::post('/webhooks/git/{deployment}/{token}', GitWebhookController::class); Route::post('/webhooks/git/{deployment}', GitWebhookController::class)
->middleware('throttle:git-webhooks');
Route::post('/webhooks/git/{deployment}/{token}', GitWebhookController::class)
->middleware('throttle:git-webhooks');
Route::middleware(['auth:sanctum', 'abilities:automation']) Route::middleware(['auth:sanctum', 'abilities:automation'])
->prefix('automation') ->prefix('automation')

View File

@@ -22,8 +22,9 @@ SKIP_CACHE=0
SKIP_AGENT_RESTART=0 SKIP_AGENT_RESTART=0
DELETE_REMOTE=0 DELETE_REMOTE=0
DRY_RUN=0 DRY_RUN=0
PUSH_GITEA=0 SKIP_PUSH=0
PUSH_GITHUB=0 PUSH_GITEA=1
PUSH_GITHUB=1
SET_VERSION="" SET_VERSION=""
usage() { usage() {
@@ -43,13 +44,16 @@ Options:
--skip-agent-restart Skip restarting jabali-agent service --skip-agent-restart Skip restarting jabali-agent service
--delete Pass --delete to rsync (dangerous) --delete Pass --delete to rsync (dangerous)
--dry-run Dry-run rsync only --dry-run Dry-run rsync only
--push-gitea Push current branch to Gitea before deploy --skip-push Skip all git push operations
--push-gitea Push current branch to Gitea from deploy server (default: on)
--no-push-gitea Disable Gitea push
--gitea-remote NAME Gitea git remote name (default: gitea) --gitea-remote NAME Gitea git remote name (default: gitea)
--gitea-url URL Push to this URL instead of a named remote --gitea-url URL Push to this URL instead of a named remote
--push-github Push current branch to GitHub before deploy --push-github Push current branch to GitHub from deploy server (default: on)
--no-push-github Disable GitHub push
--github-remote NAME GitHub git remote name (default: origin) --github-remote NAME GitHub git remote name (default: origin)
--github-url URL Push to this URL instead of a named remote --github-url URL Push to this URL instead of a named remote
--version VALUE Set VERSION to a specific value before push --version VALUE Set VERSION to a specific value before remote push
-h, --help Show this help -h, --help Show this help
Environment overrides: Environment overrides:
@@ -107,10 +111,20 @@ while [[ $# -gt 0 ]]; do
DRY_RUN=1 DRY_RUN=1
shift shift
;; ;;
--skip-push)
SKIP_PUSH=1
PUSH_GITEA=0
PUSH_GITHUB=0
shift
;;
--push-gitea) --push-gitea)
PUSH_GITEA=1 PUSH_GITEA=1
shift shift
;; ;;
--no-push-gitea)
PUSH_GITEA=0
shift
;;
--gitea-remote) --gitea-remote)
GITEA_REMOTE="$2" GITEA_REMOTE="$2"
shift 2 shift 2
@@ -123,6 +137,10 @@ while [[ $# -gt 0 ]]; do
PUSH_GITHUB=1 PUSH_GITHUB=1
shift shift
;; ;;
--no-push-github)
PUSH_GITHUB=0
shift
;;
--github-remote) --github-remote)
GITHUB_REMOTE="$2" GITHUB_REMOTE="$2"
shift 2 shift 2
@@ -149,81 +167,121 @@ done
REMOTE="${DEPLOY_USER}@${DEPLOY_HOST}" REMOTE="${DEPLOY_USER}@${DEPLOY_HOST}"
ensure_clean_worktree() { ensure_remote_git_clean() {
if ! git -C "$ROOT_DIR" diff --quiet || ! git -C "$ROOT_DIR" diff --cached --quiet; then local status_output
echo "Working tree is dirty. Commit or stash changes before pushing." status_output="$(remote_run "if [[ ! -d \"$DEPLOY_PATH/.git\" ]]; then echo '__NO_GIT__'; exit 0; fi; cd \"$DEPLOY_PATH\" && git status --porcelain")"
if [[ "$status_output" == "__NO_GIT__" ]]; then
echo "Remote path is not a git repository: $DEPLOY_PATH"
exit 1
fi
if [[ -n "$status_output" ]]; then
echo "Remote git worktree is dirty at $DEPLOY_PATH. Commit or stash remote changes first."
echo "$status_output"
exit 1 exit 1
fi fi
} }
get_current_version() { remote_commit_and_push() {
sed -n 's/^VERSION=//p' "$ROOT_DIR/VERSION" local local_head push_branch
} local_head="$(git -C "$ROOT_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown)"
bump_version() { if [[ -n "$PUSH_BRANCH" ]]; then
local current new base num push_branch="$PUSH_BRANCH"
current="$(get_current_version)"
if [[ -n "$SET_VERSION" ]]; then
new="$SET_VERSION"
else else
if [[ "$current" =~ ^(.+-rc)([0-9]+)?$ ]]; then push_branch="$(remote_run "cd \"$DEPLOY_PATH\" && git rev-parse --abbrev-ref HEAD")"
base="${BASH_REMATCH[1]}" if [[ -z "$push_branch" || "$push_branch" == "HEAD" ]]; then
num="${BASH_REMATCH[2]}" push_branch="main"
if [[ -z "$num" ]]; then fi
num=1 fi
else
num=$((num + 1)) ssh -o StrictHostKeyChecking=no "$REMOTE" \
fi DEPLOY_PATH="$DEPLOY_PATH" \
new="${base}${num}" PUSH_BRANCH="$push_branch" \
elif [[ "$current" =~ ^(.+?)([0-9]+)$ ]]; then PUSH_GITEA="$PUSH_GITEA" \
new="${BASH_REMATCH[1]}$((BASH_REMATCH[2] + 1))" PUSH_GITHUB="$PUSH_GITHUB" \
GITEA_REMOTE="$GITEA_REMOTE" \
GITEA_URL="$GITEA_URL" \
GITHUB_REMOTE="$GITHUB_REMOTE" \
GITHUB_URL="$GITHUB_URL" \
SET_VERSION="$SET_VERSION" \
LOCAL_HEAD="$local_head" \
bash -s <<'EOF'
set -euo pipefail
cd "$DEPLOY_PATH"
if [[ ! -d .git ]]; then
echo "Remote path is not a git repository: $DEPLOY_PATH" >&2
exit 1
fi
git config --global --add safe.directory "$DEPLOY_PATH" >/dev/null 2>&1 || true
if ! git config user.name >/dev/null; then
git config user.name "Jabali Deploy"
fi
if ! git config user.email >/dev/null; then
git config user.email "root@$(hostname -f 2>/dev/null || hostname)"
fi
current="$(sed -n 's/^VERSION=//p' VERSION || true)"
if [[ -z "$current" ]]; then
echo "VERSION file missing or invalid on remote." >&2
exit 1
fi
if [[ -n "${SET_VERSION:-}" ]]; then
new="$SET_VERSION"
else
if [[ "$current" =~ ^(.+-rc)([0-9]+)?$ ]]; then
base="${BASH_REMATCH[1]}"
num="${BASH_REMATCH[2]}"
if [[ -z "$num" ]]; then
num=1
else else
echo "Cannot auto-bump VERSION from '$current'. Use --version to set it explicitly." num=$((num + 1))
exit 1
fi fi
fi new="${base}${num}"
elif [[ "$current" =~ ^(.+?)([0-9]+)$ ]]; then
if [[ "$new" == "$current" ]]; then new="${BASH_REMATCH[1]}$((BASH_REMATCH[2] + 1))"
echo "VERSION is already '$current'. Use --version to set a new value." else
echo "Cannot auto-bump VERSION from '$current'. Use --version to set it explicitly." >&2
exit 1 exit 1
fi fi
fi
printf 'VERSION=%s\n' "$new" > "$ROOT_DIR/VERSION" if [[ "$new" == "$current" ]]; then
perl -0pi -e "s/JABALI_VERSION=\\\"\\$\\{JABALI_VERSION:-[^\\\"]+\\}\\\"/JABALI_VERSION=\\\"\\$\\{JABALI_VERSION:-$new\\}\\\"/g" "$ROOT_DIR/install.sh" echo "VERSION is already '$current'. Use --version to set a new value." >&2
exit 1
fi
git -C "$ROOT_DIR" add VERSION install.sh printf 'VERSION=%s\n' "$new" > VERSION
if ! git -C "$ROOT_DIR" diff --cached --quiet; then sed -i -E "s|JABALI_VERSION=\"\\$\\{JABALI_VERSION:-[^}]+\\}\"|JABALI_VERSION=\"\\\${JABALI_VERSION:-$new}\"|" install.sh
git -C "$ROOT_DIR" commit -m "Bump VERSION to $new"
fi
}
prepare_push() { git add -A
ensure_clean_worktree if git diff --cached --quiet; then
bump_version echo "No changes detected after sync/version bump; skipping commit."
} else
git commit -m "Deploy sync from ${LOCAL_HEAD} (v${new})"
fi
push_remote() { if [[ "$PUSH_GITEA" -eq 1 ]]; then
local label="$1" if [[ -n "$GITEA_URL" ]]; then
local remote_name="$2" git push "$GITEA_URL" "$PUSH_BRANCH"
local remote_url="$3"
local target
if [[ -n "$remote_url" ]]; then
target="$remote_url"
else else
if ! git -C "$ROOT_DIR" remote get-url "$remote_name" >/dev/null 2>&1; then git push "$GITEA_REMOTE" "$PUSH_BRANCH"
echo "$label remote '$remote_name' not found. Use --${label,,}-url or --${label,,}-remote."
exit 1
fi
target="$remote_name"
fi fi
fi
if [[ -z "$PUSH_BRANCH" ]]; then if [[ "$PUSH_GITHUB" -eq 1 ]]; then
PUSH_BRANCH="$(git -C "$ROOT_DIR" rev-parse --abbrev-ref HEAD)" if [[ -n "$GITHUB_URL" ]]; then
git push "$GITHUB_URL" "$PUSH_BRANCH"
else
git push "$GITHUB_REMOTE" "$PUSH_BRANCH"
fi fi
fi
git -C "$ROOT_DIR" push "$target" "$PUSH_BRANCH" EOF
} }
rsync_project() { rsync_project() {
@@ -275,18 +333,9 @@ ensure_remote_permissions() {
echo "Deploying to ${REMOTE}:${DEPLOY_PATH}" echo "Deploying to ${REMOTE}:${DEPLOY_PATH}"
if [[ "$PUSH_GITEA" -eq 1 ]]; then if [[ "$DRY_RUN" -eq 0 && "$SKIP_PUSH" -eq 0 && ( "$PUSH_GITEA" -eq 1 || "$PUSH_GITHUB" -eq 1 ) ]]; then
prepare_push echo "Validating remote git worktree..."
echo "Pushing to Gitea..." ensure_remote_git_clean
push_remote "Gitea" "$GITEA_REMOTE" "$GITEA_URL"
fi
if [[ "$PUSH_GITHUB" -eq 1 ]]; then
if [[ "$PUSH_GITEA" -ne 1 ]]; then
prepare_push
fi
echo "Pushing to GitHub..."
push_remote "GitHub" "$GITHUB_REMOTE" "$GITHUB_URL"
fi fi
if [[ "$SKIP_SYNC" -eq 0 ]]; then if [[ "$SKIP_SYNC" -eq 0 ]]; then
@@ -299,6 +348,11 @@ if [[ "$DRY_RUN" -eq 1 ]]; then
exit 0 exit 0
fi fi
if [[ "$SKIP_PUSH" -eq 0 && ( "$PUSH_GITEA" -eq 1 || "$PUSH_GITHUB" -eq 1 ) ]]; then
echo "Committing and pushing from ${REMOTE}..."
remote_commit_and_push
fi
echo "Ensuring remote permissions..." echo "Ensuring remote permissions..."
ensure_remote_permissions ensure_remote_permissions

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Jobs\RunGitDeployment;
use App\Models\Domain;
use App\Models\GitDeployment;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
use Tests\TestCase;
class ApiSecurityHardeningTest extends TestCase
{
use RefreshDatabase;
public function test_git_webhook_rejects_unsigned_request_on_tokenless_route(): void
{
Bus::fake();
$deployment = $this->createDeployment();
$response = $this->postJson("/api/webhooks/git/{$deployment->id}", ['ref' => 'refs/heads/main']);
$response->assertStatus(403);
Bus::assertNotDispatched(RunGitDeployment::class);
}
public function test_git_webhook_accepts_hmac_signature(): void
{
Bus::fake();
$deployment = $this->createDeployment();
$payload = ['ref' => 'refs/heads/main'];
$signature = hash_hmac('sha256', (string) json_encode($payload), $deployment->secret_token);
$response = $this
->withHeader('X-Jabali-Signature', $signature)
->postJson("/api/webhooks/git/{$deployment->id}", $payload);
$response->assertStatus(200);
Bus::assertDispatched(RunGitDeployment::class);
}
public function test_git_webhook_accepts_legacy_token_route(): void
{
Bus::fake();
$deployment = $this->createDeployment();
$response = $this->postJson(
"/api/webhooks/git/{$deployment->id}/{$deployment->secret_token}",
['ref' => 'refs/heads/main']
);
$response->assertStatus(200);
Bus::assertDispatched(RunGitDeployment::class);
}
public function test_internal_api_rejects_non_local_without_internal_token(): void
{
config()->set('app.internal_api_token', null);
$response = $this
->withServerVariables(['REMOTE_ADDR' => '203.0.113.10'])
->postJson('/api/internal/page-cache', []);
$response->assertStatus(403);
}
public function test_internal_api_allows_non_local_with_internal_token(): void
{
config()->set('app.internal_api_token', 'test-internal-token');
$response = $this
->withServerVariables(['REMOTE_ADDR' => '203.0.113.10'])
->withHeader('X-Jabali-Internal-Token', 'test-internal-token')
->postJson('/api/internal/page-cache', []);
$response->assertStatus(400);
$response->assertJson(['error' => 'Domain is required']);
}
private function createDeployment(): GitDeployment
{
$user = User::factory()->create();
$domain = Domain::factory()->for($user)->create();
return GitDeployment::create([
'user_id' => $user->id,
'domain_id' => $domain->id,
'repo_url' => 'https://example.com/repo.git',
'branch' => 'main',
'deploy_path' => '/home/'.$user->username.'/domains/'.$domain->domain.'/public_html',
'auto_deploy' => true,
'secret_token' => 'test-secret-token-1234567890',
]);
}
}