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\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',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||
],
|
||||
"files": [],
|
||||
"version": "1.10.17"
|
||||
"version": "1.10.18"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user