fix git
This commit is contained in:
@@ -6,101 +6,160 @@ namespace Modules\Helpdesk\Http\Controllers;
|
|||||||
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use JsonException;
|
use JsonException;
|
||||||
|
use Modules\Helpdesk\Models\GitCommit;
|
||||||
use Modules\Helpdesk\Models\GitRepository;
|
use Modules\Helpdesk\Models\GitRepository;
|
||||||
use Modules\Helpdesk\Services\GitWebhookIngestionService;
|
use Modules\Helpdesk\Services\GitWebhookIngestionService;
|
||||||
|
|
||||||
final class GitWebhookController
|
final class GitWebhookController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Ingest a signed Git provider webhook for a registered Helpdesk repository.
|
* Handle an incoming Gitea webhook delivery.
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request, GitRepository $repository, GitWebhookIngestionService $service): JsonResponse
|
public function __invoke(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$provider = strtolower((string) ($repository->provider ?: 'git'));
|
Log::info('Gitea webhook endpoint hit.', [
|
||||||
$event = $this->eventName($request, $provider);
|
'event' => $request->header('X-Gitea-Event'),
|
||||||
$deliveryId = $this->deliveryId($request, $provider);
|
'github_event' => $request->header('X-GitHub-Event'),
|
||||||
$rawPayload = $request->getContent();
|
'delivery' => $request->header('X-Gitea-Delivery'),
|
||||||
|
'authorization_present' => $request->headers->has('Authorization'),
|
||||||
|
'signature_present' => $request->headers->has('X-Gitea-Signature'),
|
||||||
|
]);
|
||||||
|
|
||||||
if (! $repository->is_active) {
|
$authResponse = $this->validateAuthorization($request);
|
||||||
$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 ($authResponse instanceof JsonResponse) {
|
||||||
|
return $authResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->hasValidSignature($request, $repository, $provider, $rawPayload)) {
|
$signatureResponse = $this->validateSignature($request);
|
||||||
$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);
|
if ($signatureResponse instanceof JsonResponse) {
|
||||||
|
return $signatureResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$event = (string) ($request->header('X-Gitea-Event') ?: $request->header('X-GitHub-Event'));
|
||||||
/** @var array<string, mixed> $payload */
|
$payload = $request->json()->all();
|
||||||
$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);
|
if ($event !== 'push') {
|
||||||
|
Log::info('Gitea webhook ignored.', [
|
||||||
|
'event' => $event,
|
||||||
|
'delivery' => $request->header('X-Gitea-Delivery'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Gitea webhook ignored.',
|
||||||
|
'event' => $event,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $service->ingest($repository, $payload, $provider, $deliveryId, $event);
|
$commits = data_get($payload, 'commits', []);
|
||||||
|
|
||||||
return response()->json($result, 202);
|
Log::info('Gitea push webhook received.', [
|
||||||
|
'repository' => data_get($payload, 'repository.full_name'),
|
||||||
|
'ref' => data_get($payload, 'ref'),
|
||||||
|
'branch' => str_replace('refs/heads/', '', (string) data_get($payload, 'ref')),
|
||||||
|
'before' => data_get($payload, 'before'),
|
||||||
|
'after' => data_get($payload, 'after'),
|
||||||
|
'commit_count' => is_countable($commits) ? count($commits) : 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($commits as $commit) {
|
||||||
|
GitCommit::updateOrCreate(
|
||||||
|
[
|
||||||
|
'sha' => data_get($commit, 'id'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'repository_full_name' => data_get($payload, 'repository.full_name'),
|
||||||
|
'repository_name' => data_get($payload, 'repository.name'),
|
||||||
|
'branch' => str_replace(
|
||||||
|
'refs/heads/',
|
||||||
|
'',
|
||||||
|
(string) data_get($payload, 'ref'),
|
||||||
|
),
|
||||||
|
'author_name' => data_get($commit, 'author.name'),
|
||||||
|
'author_email' => data_get($commit, 'author.email'),
|
||||||
|
'message' => data_get($commit, 'message'),
|
||||||
|
'commit_url' => data_get($commit, 'url'),
|
||||||
|
'committed_at' => data_get($commit, 'timestamp')
|
||||||
|
? Carbon::parse(data_get($commit, 'timestamp'))
|
||||||
|
: now(),
|
||||||
|
'payload' => $commit,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::info('Git commit stored.', [
|
||||||
|
'sha' => data_get($commit, 'id'),
|
||||||
|
'message' => data_get($commit, 'message'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Gitea push processed.',
|
||||||
|
'repository' => data_get($payload, 'repository.full_name'),
|
||||||
|
'branch' => str_replace('refs/heads/', '', (string) data_get($payload, 'ref')),
|
||||||
|
'commit_count' => is_countable($commits) ? count($commits) : 0,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate provider-specific webhook signatures or tokens.
|
* Validate the optional Gitea Authorization header.
|
||||||
*/
|
*/
|
||||||
private function hasValidSignature(Request $request, GitRepository $repository, string $provider, string $rawPayload): bool
|
private function validateAuthorization(Request $request): ?JsonResponse
|
||||||
{
|
{
|
||||||
$secret = trim((string) $repository->webhook_secret);
|
$expected = (string) config('services.gitea.webhook_authorization');
|
||||||
|
|
||||||
|
if ($expected === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actual = (string) $request->header('Authorization');
|
||||||
|
|
||||||
|
if (hash_equals($expected, $actual)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warning('Rejected Gitea webhook because Authorization header was invalid.', [
|
||||||
|
'delivery' => $request->header('X-Gitea-Delivery'),
|
||||||
|
'authorization_present' => $request->headers->has('Authorization'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Invalid authorization header.',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the optional Gitea HMAC SHA-256 signature.
|
||||||
|
*/
|
||||||
|
private function validateSignature(Request $request): ?JsonResponse
|
||||||
|
{
|
||||||
|
$secret = (string) config('services.gitea.webhook_secret');
|
||||||
|
|
||||||
if ($secret === '') {
|
if ($secret === '') {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return match ($provider) {
|
$payload = $request->getContent();
|
||||||
'github' => $this->hasValidGitHubSignature($request, $secret, $rawPayload),
|
$signature = (string) $request->header('X-Gitea-Signature');
|
||||||
'gitlab' => hash_equals($secret, (string) $request->header('X-Gitlab-Token', '')),
|
|
||||||
default => hash_equals($secret, (string) $request->header('X-Helpdesk-Token', '')),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
$expected = hash_hmac('sha256', $payload, $secret);
|
||||||
* 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);
|
if ($signature !== '' && hash_equals($expected, $signature)) {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
Log::warning('Rejected Gitea webhook because signature was invalid.', [
|
||||||
* Read the event name from provider-specific headers.
|
'delivery' => $request->header('X-Gitea-Delivery'),
|
||||||
*/
|
'signature_present' => $request->headers->has('X-Gitea-Signature'),
|
||||||
private function eventName(Request $request, string $provider): ?string
|
'expected' => $expected,
|
||||||
{
|
'actual' => $signature,
|
||||||
return match ($provider) {
|
]);
|
||||||
'github' => $request->header('X-GitHub-Event'),
|
|
||||||
'gitlab' => $request->header('X-Gitlab-Event'),
|
|
||||||
default => $request->header('X-Helpdesk-Event', 'push'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return response()->json([
|
||||||
* Read the provider delivery/request id used for idempotent audit tracing.
|
'message' => 'Invalid Gitea signature.',
|
||||||
*/
|
], 403);
|
||||||
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'),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ Route::get('/status', static fn (): array => [
|
|||||||
Route::post('/git/webhook/{repository}', GitWebhookController::class)
|
Route::post('/git/webhook/{repository}', GitWebhookController::class)
|
||||||
->whereNumber('repository')
|
->whereNumber('repository')
|
||||||
->name('git.webhook');
|
->name('git.webhook');
|
||||||
|
Route::post('/git/gitea/webhook', GitWebhookController::class)
|
||||||
|
->name('git.gitea.webhook');
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Git;
|
|
||||||
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Routing\Controller;
|
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Modules\Helpdesk\Models\GitCommit;
|
|
||||||
|
|
||||||
final class GiteaWebhookController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle an incoming Gitea webhook delivery.
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
Log::info('Gitea webhook endpoint hit.', [
|
|
||||||
'event' => $request->header('X-Gitea-Event'),
|
|
||||||
'github_event' => $request->header('X-GitHub-Event'),
|
|
||||||
'delivery' => $request->header('X-Gitea-Delivery'),
|
|
||||||
'authorization_present' => $request->headers->has('Authorization'),
|
|
||||||
'signature_present' => $request->headers->has('X-Gitea-Signature'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$authResponse = $this->validateAuthorization($request);
|
|
||||||
|
|
||||||
if ($authResponse instanceof JsonResponse) {
|
|
||||||
return $authResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
$signatureResponse = $this->validateSignature($request);
|
|
||||||
|
|
||||||
if ($signatureResponse instanceof JsonResponse) {
|
|
||||||
return $signatureResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
$event = (string) ($request->header('X-Gitea-Event') ?: $request->header('X-GitHub-Event'));
|
|
||||||
$payload = $request->json()->all();
|
|
||||||
|
|
||||||
if ($event !== 'push') {
|
|
||||||
Log::info('Gitea webhook ignored.', [
|
|
||||||
'event' => $event,
|
|
||||||
'delivery' => $request->header('X-Gitea-Delivery'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Gitea webhook ignored.',
|
|
||||||
'event' => $event,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$commits = data_get($payload, 'commits', []);
|
|
||||||
|
|
||||||
Log::info('Gitea push webhook received.', [
|
|
||||||
'repository' => data_get($payload, 'repository.full_name'),
|
|
||||||
'ref' => data_get($payload, 'ref'),
|
|
||||||
'branch' => str_replace('refs/heads/', '', (string) data_get($payload, 'ref')),
|
|
||||||
'before' => data_get($payload, 'before'),
|
|
||||||
'after' => data_get($payload, 'after'),
|
|
||||||
'commit_count' => is_countable($commits) ? count($commits) : 0,
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ($commits as $commit) {
|
|
||||||
GitCommit::updateOrCreate(
|
|
||||||
[
|
|
||||||
'sha' => data_get($commit, 'id'),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'repository_full_name' => data_get($payload, 'repository.full_name'),
|
|
||||||
'repository_name' => data_get($payload, 'repository.name'),
|
|
||||||
'branch' => str_replace(
|
|
||||||
'refs/heads/',
|
|
||||||
'',
|
|
||||||
(string) data_get($payload, 'ref'),
|
|
||||||
),
|
|
||||||
'author_name' => data_get($commit, 'author.name'),
|
|
||||||
'author_email' => data_get($commit, 'author.email'),
|
|
||||||
'message' => data_get($commit, 'message'),
|
|
||||||
'commit_url' => data_get($commit, 'url'),
|
|
||||||
'committed_at' => data_get($commit, 'timestamp')
|
|
||||||
? Carbon::parse(data_get($commit, 'timestamp'))
|
|
||||||
: now(),
|
|
||||||
'payload' => $commit,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
Log::info('Git commit stored.', [
|
|
||||||
'sha' => data_get($commit, 'id'),
|
|
||||||
'message' => data_get($commit, 'message'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Gitea push processed.',
|
|
||||||
'repository' => data_get($payload, 'repository.full_name'),
|
|
||||||
'branch' => str_replace('refs/heads/', '', (string) data_get($payload, 'ref')),
|
|
||||||
'commit_count' => is_countable($commits) ? count($commits) : 0,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the optional Gitea Authorization header.
|
|
||||||
*/
|
|
||||||
private function validateAuthorization(Request $request): ?JsonResponse
|
|
||||||
{
|
|
||||||
$expected = (string) config('services.gitea.webhook_authorization');
|
|
||||||
|
|
||||||
if ($expected === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$actual = (string) $request->header('Authorization');
|
|
||||||
|
|
||||||
if (hash_equals($expected, $actual)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::warning('Rejected Gitea webhook because Authorization header was invalid.', [
|
|
||||||
'delivery' => $request->header('X-Gitea-Delivery'),
|
|
||||||
'authorization_present' => $request->headers->has('Authorization'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Invalid authorization header.',
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the optional Gitea HMAC SHA-256 signature.
|
|
||||||
*/
|
|
||||||
private function validateSignature(Request $request): ?JsonResponse
|
|
||||||
{
|
|
||||||
$secret = (string) config('services.gitea.webhook_secret');
|
|
||||||
|
|
||||||
if ($secret === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$payload = $request->getContent();
|
|
||||||
$signature = (string) $request->header('X-Gitea-Signature');
|
|
||||||
|
|
||||||
$expected = hash_hmac('sha256', $payload, $secret);
|
|
||||||
|
|
||||||
if ($signature !== '' && hash_equals($expected, $signature)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::warning('Rejected Gitea webhook because signature was invalid.', [
|
|
||||||
'delivery' => $request->header('X-Gitea-Delivery'),
|
|
||||||
'signature_present' => $request->headers->has('X-Gitea-Signature'),
|
|
||||||
'expected' => $expected,
|
|
||||||
'actual' => $signature,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Invalid Gitea signature.',
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,2 @@
|
|||||||
<?php
|
<?php
|
||||||
use App\Http\Controllers\Git\GiteaWebhookController;
|
|
||||||
|
|
||||||
Route::post('/git/gitea/webhook', GiteaWebhookController::class)
|
|
||||||
|
|
||||||
->name('git.gitea.webhook');
|
|
||||||
|
|||||||
Reference in New Issue
Block a user