mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-07 22:45:27 -04:00
- Proxy: list and delete Traefik ACME certificates from the server proxy page via new TraefikAcmeService and Get/DeleteTraefikCertificate actions - DNS: track ownership and cross-resource references for managed DNS records so records are only deleted when no longer referenced; release records asynchronously on resource deletion via ReleaseManagedDnsRecordsJob and ManagedDnsRecordCleanup; harden Cloudflare provider deletion results - Databases: fail closed on start when prerequisites or the CA certificate are missing (DatabaseStartException, Server::ensureCaCertificate) and clean up stale start activities via ResourceStartActivity - Webhooks: throttle repeated manual webhook signature failures for GitHub, GitLab, Gitea and Bitbucket - Deployments: improve compose build-context handling and compose file load error reporting - Install scripts: rework terminal UI output in install.sh (stable and nightly) - Misc: settings sidebar accordion fixes, log drain toggle rollback, add Serverside to README sponsors - Add migrations and tests covering the above
130 lines
3.9 KiB
PHP
130 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Webhook\Concerns;
|
|
|
|
use App\Models\Application;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Collection;
|
|
|
|
trait MatchesManualWebhookApplications
|
|
{
|
|
use ThrottlesManualWebhookFailures;
|
|
|
|
protected function manualWebhookRepositoryFullName(mixed $fullName): ?string
|
|
{
|
|
if (! is_string($fullName)) {
|
|
return null;
|
|
}
|
|
|
|
$fullName = trim($fullName, " \t\n\r\0\x0B/");
|
|
|
|
if ($fullName === '') {
|
|
return null;
|
|
}
|
|
|
|
if (! preg_match('/\A[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+\z/', $fullName)) {
|
|
return null;
|
|
}
|
|
|
|
return $this->normalizeManualWebhookRepositoryPath($fullName);
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, Application>
|
|
*/
|
|
protected function manualWebhookApplications(Builder $query, string $fullName): Collection
|
|
{
|
|
return $query->get()
|
|
->filter(fn (Application $application): bool => $this->manualWebhookRepositoryMatches($application->git_repository, $fullName))
|
|
->values();
|
|
}
|
|
|
|
protected function manualWebhookRepositoryMatches(?string $gitRepository, string $fullName): bool
|
|
{
|
|
$repositoryPath = $this->canonicalManualWebhookRepository($gitRepository);
|
|
|
|
if ($repositoryPath === null) {
|
|
return false;
|
|
}
|
|
|
|
// Git hosts (GitHub, GitLab, Gitea, Bitbucket) treat owner/repo names
|
|
// case-insensitively, so compare the canonical paths case-insensitively.
|
|
return hash_equals(mb_strtolower($fullName), mb_strtolower($repositoryPath));
|
|
}
|
|
|
|
/**
|
|
* @return array{status: string, message: string}
|
|
*/
|
|
protected function unauthenticatedManualWebhookFailurePayload(): array
|
|
{
|
|
return [
|
|
'status' => 'failed',
|
|
'message' => 'Invalid signature.',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Respond to a delivery that could not be authenticated (no matching
|
|
* application or no signature) and count it as a failed attempt.
|
|
*/
|
|
protected function unauthenticatedManualWebhookResponse(Request $request, string $provider): Response
|
|
{
|
|
$this->recordManualWebhookFailure($request, $provider);
|
|
|
|
return response([$this->unauthenticatedManualWebhookFailurePayload()]);
|
|
}
|
|
|
|
protected function manualWebhookResponse(Collection $payloads, Request $request, string $provider): Response
|
|
{
|
|
$failure = $this->unauthenticatedManualWebhookFailurePayload();
|
|
$authorizedPayloads = $payloads->reject(fn (array $payload): bool => $payload === $failure)->values();
|
|
if ($authorizedPayloads->isEmpty() && $payloads->isNotEmpty()) {
|
|
return $this->unauthenticatedManualWebhookResponse($request, $provider);
|
|
}
|
|
|
|
return response($authorizedPayloads);
|
|
}
|
|
|
|
protected function canonicalManualWebhookRepository(?string $gitRepository): ?string
|
|
{
|
|
if (! is_string($gitRepository)) {
|
|
return null;
|
|
}
|
|
|
|
$gitRepository = trim($gitRepository);
|
|
|
|
if ($gitRepository === '') {
|
|
return null;
|
|
}
|
|
|
|
$path = null;
|
|
$parts = parse_url($gitRepository);
|
|
|
|
if (is_array($parts) && isset($parts['scheme'])) {
|
|
$path = data_get($parts, 'path');
|
|
} elseif (($scp = parseScpStyleGitUrl($gitRepository)) !== null) {
|
|
$path = $scp['path'];
|
|
} else {
|
|
$path = $gitRepository;
|
|
}
|
|
|
|
if (! is_string($path) || $path === '') {
|
|
return null;
|
|
}
|
|
|
|
return $this->normalizeManualWebhookRepositoryPath($path);
|
|
}
|
|
|
|
protected function normalizeManualWebhookRepositoryPath(string $path): string
|
|
{
|
|
$path = trim($path);
|
|
$path = strtok($path, '?#') ?: $path;
|
|
$path = trim($path, '/');
|
|
$path = preg_replace('/\.git\z/i', '', $path) ?? $path;
|
|
|
|
return $path;
|
|
}
|
|
}
|