new git integraton code

This commit is contained in:
Mark Gregory
2026-06-24 16:11:42 +01:00
parent 44244ec59e
commit f05305617d
4 changed files with 141 additions and 16 deletions
@@ -7,6 +7,8 @@ namespace Modules\Helpdesk\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use JsonException;
use Modules\Helpdesk\Models\GitRepository;
use Modules\Helpdesk\Services\GitWebhookIngestionService;
@@ -162,7 +164,12 @@ final class GitWebhookController
{
$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->hasValidGenericSignature($request, $secret, $rawPayload);
}
@@ -183,7 +190,7 @@ final class GitWebhookController
*/
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'));
return hash_equals($secret, $token)
@@ -205,7 +212,8 @@ final class GitWebhookController
$actual = trim((string) $request->header('Authorization', ''));
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()
->where('is_active', true)
->whereIn('provider', ['gitea', 'git'])
->whereRaw('LOWER(provider) IN (?, ?)', ['gitea', 'git'])
->get();
if ($repositories->isEmpty()) {
Log::warning('Helpdesk Gitea webhook received but no active Gitea/git repository is configured.');
return null;
}
$payloadValues = array_filter([
$payloadValues = array_values(array_filter([
Arr::get($payload, 'repository.full_name'),
Arr::get($payload, 'repository.name'),
Arr::get($payload, 'repository.html_url'),
Arr::get($payload, 'repository.clone_url'),
Arr::get($payload, 'repository.ssh_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 ($payloadValues as $value) {
$payloadValue = trim((string) $value);
$remoteUrl = trim((string) $repository->remote_url);
$repositoryValues = array_filter([
$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))) {
return $repository;
}
$normalisedRepositoryValues = array_values(array_filter(array_map(
fn (mixed $value): ?string => $this->normaliseRepositoryIdentity((string) $value),
$repositoryValues,
)));
if (strcasecmp($payloadValue, (string) $repository->name) === 0) {
return $repository;
foreach ($normalisedPayloadValues as $payloadValue) {
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());
}
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',
]);
}
}
+7
View File
@@ -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.
- Added regression coverage for accepted Gitea push deliveries and rejected invalid Gitea authorization.
- 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.
+1 -1
View File
@@ -23,5 +23,5 @@
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
],
"files": [],
"version": "1.10.17"
"version": "1.10.18"
}