new git integraton code
This commit is contained in:
@@ -7,6 +7,8 @@ namespace Modules\Helpdesk\Http\Controllers;
|
|||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use JsonException;
|
use JsonException;
|
||||||
use Modules\Helpdesk\Models\GitRepository;
|
use Modules\Helpdesk\Models\GitRepository;
|
||||||
use Modules\Helpdesk\Services\GitWebhookIngestionService;
|
use Modules\Helpdesk\Services\GitWebhookIngestionService;
|
||||||
@@ -162,7 +164,12 @@ final class GitWebhookController
|
|||||||
{
|
{
|
||||||
$giteaSignature = (string) $request->header('X-Gitea-Signature', '');
|
$giteaSignature = (string) $request->header('X-Gitea-Signature', '');
|
||||||
|
|
||||||
return hash_equals(hash_hmac('sha256', $rawPayload, $secret), $giteaSignature)
|
$sha256 = hash_hmac('sha256', $rawPayload, $secret);
|
||||||
|
$sha1 = hash_hmac('sha1', $rawPayload, $secret);
|
||||||
|
|
||||||
|
return hash_equals($sha256, $giteaSignature)
|
||||||
|
|| hash_equals('sha256=' . $sha256, $giteaSignature)
|
||||||
|
|| hash_equals('sha1=' . $sha1, $giteaSignature)
|
||||||
|| $this->hasValidGitHubSignature($request, $secret, $rawPayload)
|
|| $this->hasValidGitHubSignature($request, $secret, $rawPayload)
|
||||||
|| $this->hasValidGenericSignature($request, $secret, $rawPayload);
|
|| $this->hasValidGenericSignature($request, $secret, $rawPayload);
|
||||||
}
|
}
|
||||||
@@ -183,7 +190,7 @@ final class GitWebhookController
|
|||||||
*/
|
*/
|
||||||
private function hasValidGenericSignature(Request $request, string $secret, string $rawPayload): bool
|
private function hasValidGenericSignature(Request $request, string $secret, string $rawPayload): bool
|
||||||
{
|
{
|
||||||
$token = (string) ($request->header('X-Helpdesk-Token') ?: $request->header('X-Gitlab-Token'));
|
$token = (string) ($request->header('X-Helpdesk-Token') ?: $request->header('X-Gitlab-Token') ?: $request->header('X-Gitea-Token'));
|
||||||
$signature = (string) ($request->header('X-Hub-Signature-256') ?: $request->header('X-Hub-Signature'));
|
$signature = (string) ($request->header('X-Hub-Signature-256') ?: $request->header('X-Hub-Signature'));
|
||||||
|
|
||||||
return hash_equals($secret, $token)
|
return hash_equals($secret, $token)
|
||||||
@@ -205,7 +212,8 @@ final class GitWebhookController
|
|||||||
$actual = trim((string) $request->header('Authorization', ''));
|
$actual = trim((string) $request->header('Authorization', ''));
|
||||||
|
|
||||||
return hash_equals($expected, $actual)
|
return hash_equals($expected, $actual)
|
||||||
|| hash_equals('Basic ' . $expected, $actual);
|
|| hash_equals('Basic ' . $expected, $actual)
|
||||||
|
|| (Str::startsWith($expected, 'Basic ') && hash_equals($expected, $actual));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,38 +225,101 @@ final class GitWebhookController
|
|||||||
{
|
{
|
||||||
$repositories = GitRepository::query()
|
$repositories = GitRepository::query()
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->whereIn('provider', ['gitea', 'git'])
|
->whereRaw('LOWER(provider) IN (?, ?)', ['gitea', 'git'])
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
if ($repositories->isEmpty()) {
|
if ($repositories->isEmpty()) {
|
||||||
|
Log::warning('Helpdesk Gitea webhook received but no active Gitea/git repository is configured.');
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$payloadValues = array_filter([
|
$payloadValues = array_values(array_filter([
|
||||||
Arr::get($payload, 'repository.full_name'),
|
Arr::get($payload, 'repository.full_name'),
|
||||||
Arr::get($payload, 'repository.name'),
|
Arr::get($payload, 'repository.name'),
|
||||||
Arr::get($payload, 'repository.html_url'),
|
Arr::get($payload, 'repository.html_url'),
|
||||||
Arr::get($payload, 'repository.clone_url'),
|
Arr::get($payload, 'repository.clone_url'),
|
||||||
Arr::get($payload, 'repository.ssh_url'),
|
Arr::get($payload, 'repository.ssh_url'),
|
||||||
Arr::get($payload, 'repository.url'),
|
Arr::get($payload, 'repository.url'),
|
||||||
], static fn (mixed $value): bool => is_scalar($value) && trim((string) $value) !== '');
|
Arr::get($payload, 'repository.owner.username') && Arr::get($payload, 'repository.name')
|
||||||
|
? Arr::get($payload, 'repository.owner.username') . '/' . Arr::get($payload, 'repository.name')
|
||||||
|
: null,
|
||||||
|
], static fn (mixed $value): bool => is_scalar($value) && trim((string) $value) !== ''));
|
||||||
|
|
||||||
|
$normalisedPayloadValues = array_values(array_filter(array_map(
|
||||||
|
fn (mixed $value): ?string => $this->normaliseRepositoryIdentity((string) $value),
|
||||||
|
$payloadValues,
|
||||||
|
)));
|
||||||
|
|
||||||
foreach ($repositories as $repository) {
|
foreach ($repositories as $repository) {
|
||||||
foreach ($payloadValues as $value) {
|
$repositoryValues = array_filter([
|
||||||
$payloadValue = trim((string) $value);
|
$repository->remote_url,
|
||||||
$remoteUrl = trim((string) $repository->remote_url);
|
$repository->name,
|
||||||
|
Str::of((string) $repository->remote_url)->afterLast(':')->beforeLast('.git')->value(),
|
||||||
|
], static fn (mixed $value): bool => is_scalar($value) && trim((string) $value) !== '');
|
||||||
|
|
||||||
if ($remoteUrl !== '' && ($payloadValue === $remoteUrl || str_contains($remoteUrl, $payloadValue) || str_contains($payloadValue, $remoteUrl))) {
|
$normalisedRepositoryValues = array_values(array_filter(array_map(
|
||||||
return $repository;
|
fn (mixed $value): ?string => $this->normaliseRepositoryIdentity((string) $value),
|
||||||
}
|
$repositoryValues,
|
||||||
|
)));
|
||||||
|
|
||||||
if (strcasecmp($payloadValue, (string) $repository->name) === 0) {
|
foreach ($normalisedPayloadValues as $payloadValue) {
|
||||||
return $repository;
|
foreach ($normalisedRepositoryValues as $repositoryValue) {
|
||||||
|
if ($payloadValue === $repositoryValue
|
||||||
|
|| str_ends_with($repositoryValue, '/' . $payloadValue)
|
||||||
|
|| str_ends_with($payloadValue, '/' . $repositoryValue)) {
|
||||||
|
return $repository;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $repositories->count() === 1 ? $repositories->first() : null;
|
if ($repositories->count() === 1) {
|
||||||
|
return $repositories->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warning('Helpdesk Gitea webhook did not match any configured repository.', [
|
||||||
|
'payload_repository' => Arr::get($payload, 'repository.full_name') ?: Arr::get($payload, 'repository.clone_url') ?: Arr::get($payload, 'repository.html_url'),
|
||||||
|
'configured_repository_count' => $repositories->count(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise a repository name or URL so Gitea payloads match local settings reliably.
|
||||||
|
*/
|
||||||
|
private function normaliseRepositoryIdentity(string $value): ?string
|
||||||
|
{
|
||||||
|
$value = trim($value);
|
||||||
|
|
||||||
|
if ($value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = Str::of($value)
|
||||||
|
->replace('\\', '/')
|
||||||
|
->replace('ssh://git@', 'ssh://')
|
||||||
|
->replace('git@', '')
|
||||||
|
->replace('.git', '')
|
||||||
|
->lower()
|
||||||
|
->value();
|
||||||
|
|
||||||
|
$path = parse_url($value, PHP_URL_PATH);
|
||||||
|
|
||||||
|
if (is_string($path) && trim($path, '/') !== '') {
|
||||||
|
$value = trim($path, '/');
|
||||||
|
} elseif (str_contains($value, ':')) {
|
||||||
|
$value = Str::of($value)->afterLast(':')->trim('/')->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
$segments = array_values(array_filter(explode('/', trim($value, '/'))));
|
||||||
|
|
||||||
|
if ($segments === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode('/', array_slice($segments, -2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -250,4 +250,51 @@ final class GitWebhookIngestionTest extends TestCase
|
|||||||
self::assertSame(0, GitCommit::query()->count());
|
self::assertSame(0, GitCommit::query()->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function test_gitea_local_webhook_accepts_prefixed_signature_and_case_insensitive_provider(): void
|
||||||
|
{
|
||||||
|
$fixtures = $this->seedLookupData();
|
||||||
|
$repository = $this->repository([
|
||||||
|
'provider' => 'Gitea',
|
||||||
|
'project_id' => $fixtures['project']->id,
|
||||||
|
'name' => 'Local',
|
||||||
|
'remote_url' => 'http://git.vm:3000/Admin/WMS-Core',
|
||||||
|
'webhook_secret' => 'gitea-secret',
|
||||||
|
]);
|
||||||
|
$payload = [
|
||||||
|
'ref' => 'refs/heads/main',
|
||||||
|
'repository' => [
|
||||||
|
'name' => 'WMS-Core',
|
||||||
|
'full_name' => 'Admin/WMS-Core',
|
||||||
|
'clone_url' => 'http://git.vm:3000/Admin/WMS-Core.git',
|
||||||
|
'html_url' => 'http://git.vm:3000/Admin/WMS-Core',
|
||||||
|
],
|
||||||
|
'commits' => [
|
||||||
|
[
|
||||||
|
'id' => str_repeat('f', 40),
|
||||||
|
'message' => 'Local Gitea push should be visible on the dashboard',
|
||||||
|
'url' => 'http://git.vm:3000/Admin/WMS-Core/commit/' . str_repeat('f', 40),
|
||||||
|
'timestamp' => '2026-06-24T15:00:00+00:00',
|
||||||
|
'author' => ['name' => 'Mark Gregory', 'email' => 'mark@example.com'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$json = json_encode($payload, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
$response = $this->call('POST', route('api.git.gitea.webhook'), [], [], [], [
|
||||||
|
'CONTENT_TYPE' => 'application/json',
|
||||||
|
'HTTP_ACCEPT' => 'application/json',
|
||||||
|
'HTTP_X_GITEA_EVENT' => 'push',
|
||||||
|
'HTTP_X_GITEA_SIGNATURE' => 'sha256=' . hash_hmac('sha256', $json, 'gitea-secret'),
|
||||||
|
], $json);
|
||||||
|
|
||||||
|
$response->assertStatus(202)->assertJsonPath('status', 'accepted')->assertJsonPath('commit_count', 1);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('helpdesk_git_commits', [
|
||||||
|
'repository_id' => $repository->id,
|
||||||
|
'hash' => str_repeat('f', 40),
|
||||||
|
'branch' => 'main',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -464,3 +464,10 @@
|
|||||||
- Matched incoming Gitea payloads to active Helpdesk Git repositories by repository name and remote URL, with a safe single-repository fallback for local installations.
|
- Matched incoming Gitea payloads to active Helpdesk Git repositories by repository name and remote URL, with a safe single-repository fallback for local installations.
|
||||||
- Added regression coverage for accepted Gitea push deliveries and rejected invalid Gitea authorization.
|
- Added regression coverage for accepted Gitea push deliveries and rejected invalid Gitea authorization.
|
||||||
- Preserved append-only changelog history.
|
- Preserved append-only changelog history.
|
||||||
|
|
||||||
|
- version: 1.10.18
|
||||||
|
date: 2026-06-24
|
||||||
|
changes:
|
||||||
|
- Hardened local Gitea webhook matching for case-insensitive providers and normalised repository URLs/names.
|
||||||
|
- Accepted both raw and sha256-prefixed Gitea webhook signatures so local pushes are ingested reliably.
|
||||||
|
- Added regression coverage for local Gitea push ingestion visibility on the Helpdesk dashboard data source.
|
||||||
|
|||||||
@@ -23,5 +23,5 @@
|
|||||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||||
],
|
],
|
||||||
"files": [],
|
"files": [],
|
||||||
"version": "1.10.17"
|
"version": "1.10.18"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user