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 $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', '')), 'bitbucket' => $this->hasValidBitbucketSignature($request, $secret, $rawPayload), default => $this->hasValidGenericSignature($request, $secret, $rawPayload), }; } /** * Validate GitHub's current SHA-256 signature and legacy SHA-1 signature. */ private function hasValidGitHubSignature(Request $request, string $secret, string $rawPayload): bool { $sha256Signature = (string) $request->header('X-Hub-Signature-256', ''); $sha1Signature = (string) $request->header('X-Hub-Signature', ''); return hash_equals('sha256=' . hash_hmac('sha256', $rawPayload, $secret), $sha256Signature) || hash_equals('sha1=' . hash_hmac('sha1', $rawPayload, $secret), $sha1Signature); } /** * Validate Bitbucket's webhook HMAC signature. */ private function hasValidBitbucketSignature(Request $request, string $secret, string $rawPayload): bool { $signature = (string) $request->header('X-Hub-Signature', ''); return hash_equals('sha256=' . hash_hmac('sha256', $rawPayload, $secret), $signature) || hash_equals($secret, (string) $request->header('X-Helpdesk-Token', '')); } /** * Validate a generic Git webhook using either a token header or HMAC signature. */ private function hasValidGenericSignature(Request $request, string $secret, string $rawPayload): bool { $token = (string) ($request->header('X-Helpdesk-Token') ?: $request->header('X-Gitlab-Token')); $signature = (string) ($request->header('X-Hub-Signature-256') ?: $request->header('X-Hub-Signature')); return hash_equals($secret, $token) || hash_equals('sha256=' . hash_hmac('sha256', $rawPayload, $secret), $signature) || hash_equals('sha1=' . hash_hmac('sha1', $rawPayload, $secret), $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'), 'bitbucket' => $request->header('X-Event-Key'), default => $request->header('X-Helpdesk-Event') ?: $request->header('X-GitHub-Event') ?: $request->header('X-Gitlab-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'), 'bitbucket' => $request->header('X-Request-UUID') ?: $request->header('X-Request-Id'), default => $request->header('X-Helpdesk-Delivery') ?: $request->header('X-GitHub-Delivery') ?: $request->header('X-Request-Id'), }; } }