107 lines
4.0 KiB
PHP
107 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\Helpdesk\Http\Controllers;
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use JsonException;
|
|
use Modules\Helpdesk\Models\GitRepository;
|
|
use Modules\Helpdesk\Services\GitWebhookIngestionService;
|
|
|
|
final class GitWebhookController
|
|
{
|
|
/**
|
|
* Ingest a signed Git provider webhook for a registered Helpdesk repository.
|
|
*/
|
|
public function __invoke(Request $request, GitRepository $repository, GitWebhookIngestionService $service): JsonResponse
|
|
{
|
|
$provider = strtolower((string) ($repository->provider ?: 'git'));
|
|
$event = $this->eventName($request, $provider);
|
|
$deliveryId = $this->deliveryId($request, $provider);
|
|
$rawPayload = $request->getContent();
|
|
|
|
if (! $repository->is_active) {
|
|
$result = ['status' => 'rejected', 'message' => 'Repository is inactive.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1];
|
|
$service->logWebhook($repository, $provider, $deliveryId, $event, $result);
|
|
|
|
return response()->json($result, 403);
|
|
}
|
|
|
|
if (! $this->hasValidSignature($request, $repository, $provider, $rawPayload)) {
|
|
$result = ['status' => 'rejected', 'message' => 'Git webhook signature or token validation failed.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1];
|
|
$service->logWebhook($repository, $provider, $deliveryId, $event, $result);
|
|
|
|
return response()->json($result, 403);
|
|
}
|
|
|
|
try {
|
|
/** @var array<string, mixed> $payload */
|
|
$payload = json_decode($rawPayload, true, 512, JSON_THROW_ON_ERROR);
|
|
} catch (JsonException) {
|
|
$result = ['status' => 'rejected', 'message' => 'Invalid JSON webhook payload.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1];
|
|
$service->logWebhook($repository, $provider, $deliveryId, $event, $result);
|
|
|
|
return response()->json($result, 400);
|
|
}
|
|
|
|
$result = $service->ingest($repository, $payload, $provider, $deliveryId, $event);
|
|
|
|
return response()->json($result, 202);
|
|
}
|
|
|
|
/**
|
|
* Validate provider-specific webhook signatures or tokens.
|
|
*/
|
|
private function hasValidSignature(Request $request, GitRepository $repository, string $provider, string $rawPayload): bool
|
|
{
|
|
$secret = trim((string) $repository->webhook_secret);
|
|
|
|
if ($secret === '') {
|
|
return false;
|
|
}
|
|
|
|
return match ($provider) {
|
|
'github' => $this->hasValidGitHubSignature($request, $secret, $rawPayload),
|
|
'gitlab' => hash_equals($secret, (string) $request->header('X-Gitlab-Token', '')),
|
|
default => hash_equals($secret, (string) $request->header('X-Helpdesk-Token', '')),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate GitHub's X-Hub-Signature-256 HMAC header.
|
|
*/
|
|
private function hasValidGitHubSignature(Request $request, string $secret, string $rawPayload): bool
|
|
{
|
|
$signature = (string) $request->header('X-Hub-Signature-256', '');
|
|
$expected = 'sha256=' . hash_hmac('sha256', $rawPayload, $secret);
|
|
|
|
return hash_equals($expected, $signature);
|
|
}
|
|
|
|
/**
|
|
* Read the event name from provider-specific headers.
|
|
*/
|
|
private function eventName(Request $request, string $provider): ?string
|
|
{
|
|
return match ($provider) {
|
|
'github' => $request->header('X-GitHub-Event'),
|
|
'gitlab' => $request->header('X-Gitlab-Event'),
|
|
default => $request->header('X-Helpdesk-Event', 'push'),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Read the provider delivery/request id used for idempotent audit tracing.
|
|
*/
|
|
private function deliveryId(Request $request, string $provider): ?string
|
|
{
|
|
return match ($provider) {
|
|
'github' => $request->header('X-GitHub-Delivery'),
|
|
'gitlab' => $request->header('X-Gitlab-Event-UUID') ?: $request->header('X-Request-Id'),
|
|
default => $request->header('X-Helpdesk-Delivery'),
|
|
};
|
|
}
|
|
}
|