Files
coolify/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php
T

153 lines
5.1 KiB
PHP

<?php
namespace App\Http\Controllers\Webhook\Concerns;
use App\Models\Application;
use Illuminate\Database\Eloquent\Builder;
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.
$fullName = mb_strtolower($fullName);
$repositoryPath = mb_strtolower($repositoryPath);
// A self-hosted Git host can serve repositories under a sub-path
// (https://example.com/gitea/owner/repo), which is not part of the
// repository name in the webhook payload.
return $repositoryPath === $fullName || str_ends_with($repositoryPath, '/'.$fullName);
}
/**
* @return array{status: string, message: string}
*/
protected function unauthenticatedManualWebhookFailurePayload(): array
{
return [
'status' => 'failed',
'message' => 'No matching application or invalid signature.',
];
}
/**
* Respond to a delivery that could not be authenticated (no matching
* application or no valid signature). A locked scope gets a 429 without
* counting again; otherwise the delivery is counted as a failed attempt.
*
* Deliveries without a matching application are counted too: the failure
* key is scoped to the repository and branch, so this cannot lock out other
* applications, and it keeps the 429 response from revealing which
* repositories exist in this instance.
*
* @param string $attempt Attempt identity from manualWebhookTokenAttempt() or manualWebhookSignedPayloadAttempt().
*/
protected function unauthenticatedManualWebhookResponse(string $failureKey, string $attempt): Response
{
if ($this->hasTooManyManualWebhookFailures($failureKey)) {
return $this->tooManyManualWebhookFailuresResponse($failureKey);
}
$this->recordManualWebhookFailure($failureKey, $attempt);
return response([$this->unauthenticatedManualWebhookFailurePayload()]);
}
/**
* Return the authorized payloads. When no matched application authorized
* the delivery, respond as an unauthenticated delivery.
*
* @param string $attempt Attempt identity from manualWebhookTokenAttempt() or manualWebhookSignedPayloadAttempt().
*/
protected function manualWebhookResponse(Collection $payloads, string $failureKey, string $attempt): Response
{
$failure = $this->unauthenticatedManualWebhookFailurePayload();
$authorizedPayloads = $payloads->reject(fn (array $payload): bool => $payload === $failure)->values();
if ($authorizedPayloads->isEmpty() && $payloads->isNotEmpty()) {
return $this->unauthenticatedManualWebhookResponse($failureKey, $attempt);
}
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;
}
}