mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-28 02:06:37 -04:00
fix(git): parse generic scp-style SSH URLs with custom users
Centralize scp-style Git URL parsing so user@host:path (including custom usernames and embedded ports) is accepted and converted to HTTPS for public clones, API create, webhooks, validation, and commit/branch links.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config(['app.maintenance.driver' => 'file']);
|
||||
Storage::fake('ssh-keys');
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->bearerToken = $this->user->createToken('public-ssh-url-api-test', ['*'])->plainTextToken;
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
});
|
||||
|
||||
test('public application api converts scp-style ssh urls to https', function () {
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->postJson('/api/v1/applications/public', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'git_repository' => 'custom-user@git.example.com:2222/organization/repository.git',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => '3000',
|
||||
'autogenerate_domain' => false,
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
|
||||
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
|
||||
|
||||
expect($application->git_repository)->toBe('https://git.example.com/organization/repository.git');
|
||||
});
|
||||
@@ -69,6 +69,28 @@ it('applies http 1 transport to https fetches after clone', function () {
|
||||
->toContain("git -c http.version=HTTP/1.1 -c advice.detachedHead=false checkout 'abc123def456abc123def456abc123def456abc1'");
|
||||
});
|
||||
|
||||
it('rewrites generic ssh submodule remotes to https for public clones', function () {
|
||||
$application = applicationWithGitSettings(shallow: false);
|
||||
$application->settings->is_git_submodules_enabled = true;
|
||||
|
||||
$source = new GithubApp;
|
||||
$source->forceFill([
|
||||
'html_url' => 'https://github.com',
|
||||
'api_url' => 'https://api.github.com',
|
||||
'is_public' => true,
|
||||
]);
|
||||
$application->setRelation('source', $source);
|
||||
|
||||
$result = $application->generateGitImportCommands(
|
||||
deployment_uuid: 'test-deployment',
|
||||
exec_in_docker: false,
|
||||
);
|
||||
|
||||
expect($result['commands'])
|
||||
->toContain('sed -i "s#[A-Za-z0-9._-]*@\(.*\):#https://\\1/#g"')
|
||||
->not->toContain('s#git@\(.*\):#https://\\1/#g');
|
||||
});
|
||||
|
||||
it('does not add http transport config to ssh deploy key clones', function () {
|
||||
$application = applicationWithGitSettings();
|
||||
$application->private_key_id = 1;
|
||||
|
||||
@@ -61,6 +61,14 @@ test('convertGitUrlsForSourceAndSshUrlWithCustomPort', function () {
|
||||
]);
|
||||
});
|
||||
|
||||
test('convertGitUrlsForSourceAndSshUrlWithCustomUsernameAndPort', function () {
|
||||
$result = convertGitUrl('custom-user@git.domain.com:766/group/project.git', 'source', null);
|
||||
expect($result)->toBe([
|
||||
'repository' => 'custom-user@git.domain.com:group/project.git',
|
||||
'port' => '766',
|
||||
]);
|
||||
});
|
||||
|
||||
test('convertGitUrlsForSourceAndSshUrlSchemeWithCustomPort', function () {
|
||||
$result = convertGitUrl('ssh://git@192.168.56.11:22222/User/Repo.git', 'source', null);
|
||||
expect($result)->toBe([
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\New\PublicGitRepository;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$team = Team::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$team->members()->attach($user->id, ['role' => 'owner']);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
});
|
||||
|
||||
test('converts scp-style ssh urls with custom usernames to https', function () {
|
||||
Livewire::test(PublicGitRepository::class, ['type' => 'public'])
|
||||
->set('repository_url', 'custom-user@git.example.com:organization/repository.git')
|
||||
->call('loadBranch')
|
||||
->assertSet('repository_url', 'https://git.example.com/organization/repository.git')
|
||||
->assertSet('branchFound', true);
|
||||
});
|
||||
|
||||
test('strips custom ports when converting scp-style ssh urls to https', function () {
|
||||
Livewire::test(PublicGitRepository::class, ['type' => 'public'])
|
||||
->set('repository_url', 'custom-user@git.example.com:2222/organization/repository.git')
|
||||
->call('loadBranch')
|
||||
->assertSet('repository_url', 'https://git.example.com/organization/repository.git')
|
||||
->assertSet('branchFound', true);
|
||||
});
|
||||
@@ -565,6 +565,29 @@ describe('Manual Webhook Repository Matching', function () {
|
||||
expect($response->getContent())->not->toContain('No applications found');
|
||||
});
|
||||
|
||||
test('github matches an ssh repository URL with a non-git username and custom port', function () {
|
||||
$app = createApplicationWithWebhook(overrides: [
|
||||
'git_repository' => 'custom-user@git.example.com:2222/test-org/test-repo.git',
|
||||
]);
|
||||
$secret = $app->manual_webhook_secret_github;
|
||||
|
||||
$payload = json_encode([
|
||||
'ref' => 'refs/heads/main',
|
||||
'repository' => ['full_name' => 'test-org/test-repo'],
|
||||
'after' => 'abc123',
|
||||
'commits' => [],
|
||||
]);
|
||||
|
||||
$response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [
|
||||
'HTTP_X-GitHub-Event' => 'push',
|
||||
'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $payload, $secret),
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], $payload);
|
||||
|
||||
$response->assertOk();
|
||||
expect($response->getContent())->not->toContain('No applications found');
|
||||
});
|
||||
|
||||
test('gitlab matches scp-style ssh repository URL with custom port', function () {
|
||||
$app = createApplicationWithWebhook(overrides: [
|
||||
'git_repository' => 'git@gitlab.example.com:2222/services/xyz.git',
|
||||
|
||||
@@ -17,6 +17,14 @@ it('generates commit links for direct repository remotes', function (string $rep
|
||||
'git@github.com:coollabsio/coolify.git',
|
||||
'https://github.com/coollabsio/coolify/commit/1234567890abcdef',
|
||||
],
|
||||
'SSH remote with custom username' => [
|
||||
'custom-user@git.example.com:coollabsio/coolify.git',
|
||||
'https://git.example.com/coollabsio/coolify/commit/1234567890abcdef',
|
||||
],
|
||||
'SSH remote with custom username and port' => [
|
||||
'custom-user@git.example.com:2222/coollabsio/coolify.git',
|
||||
'https://git.example.com/coollabsio/coolify/commit/1234567890abcdef',
|
||||
],
|
||||
'SSH URL' => [
|
||||
'ssh://git@gitlab.com/coollabsio/coolify.git',
|
||||
'https://gitlab.com/coollabsio/coolify/commit/1234567890abcdef',
|
||||
@@ -37,3 +45,34 @@ it('does not generate commit links from incomplete repository URLs', function (s
|
||||
'missing host' => 'https://',
|
||||
'missing scheme' => 'github.com/coollabsio/coolify',
|
||||
]);
|
||||
|
||||
it('converts scp-style remotes with generic usernames into https repository links', function (string $repository, string $expectedBranch, string $expectedCommits, string $expectedWebhook) {
|
||||
$application = new Application;
|
||||
$application->setRelation('source', null);
|
||||
$application->git_repository = $repository;
|
||||
$application->git_branch = 'main';
|
||||
$application->base_directory = '/';
|
||||
|
||||
expect($application->gitBranchLocation)->toBe($expectedBranch)
|
||||
->and($application->gitCommits)->toBe($expectedCommits)
|
||||
->and($application->gitWebhook)->toBe($expectedWebhook);
|
||||
})->with([
|
||||
'git username' => [
|
||||
'git@github.com:coollabsio/coolify.git',
|
||||
'https://github.com/coollabsio/coolify/tree/main/',
|
||||
'https://github.com/coollabsio/coolify/commits/main',
|
||||
'https://github.com/coollabsio/coolify/settings/hooks',
|
||||
],
|
||||
'custom username' => [
|
||||
'custom-user@git.example.com:organization/repository.git',
|
||||
'https://git.example.com/organization/repository/tree/main/',
|
||||
'https://git.example.com/organization/repository/commits/main',
|
||||
'https://git.example.com/organization/repository/settings/hooks',
|
||||
],
|
||||
'custom username and port' => [
|
||||
'custom-user@git.example.com:2222/organization/repository.git',
|
||||
'https://git.example.com/organization/repository/tree/main/',
|
||||
'https://git.example.com/organization/repository/commits/main',
|
||||
'https://git.example.com/organization/repository/settings/hooks',
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
it('parses scp-style ssh git urls including custom usernames and ports', function (string $url, array $expected) {
|
||||
expect(parseScpStyleGitUrl($url))->toBe($expected);
|
||||
})->with([
|
||||
'git username' => [
|
||||
'git@github.com:organization/repository.git',
|
||||
[
|
||||
'user' => 'git',
|
||||
'host' => 'github.com',
|
||||
'port' => null,
|
||||
'path' => 'organization/repository.git',
|
||||
],
|
||||
],
|
||||
'custom username' => [
|
||||
'custom-user@git.example.com:organization/repository.git',
|
||||
[
|
||||
'user' => 'custom-user',
|
||||
'host' => 'git.example.com',
|
||||
'port' => null,
|
||||
'path' => 'organization/repository.git',
|
||||
],
|
||||
],
|
||||
'custom username and port' => [
|
||||
'custom-user@git.example.com:2222/organization/repository.git',
|
||||
[
|
||||
'user' => 'custom-user',
|
||||
'host' => 'git.example.com',
|
||||
'port' => '2222',
|
||||
'path' => 'organization/repository.git',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
it('converts scp-style ssh git urls to https without embedding custom ports in the path', function (string $url, string $expected) {
|
||||
expect(scpStyleGitUrlToHttps($url))->toBe($expected);
|
||||
})->with([
|
||||
'git username' => [
|
||||
'git@github.com:organization/repository.git',
|
||||
'https://github.com/organization/repository.git',
|
||||
],
|
||||
'custom username' => [
|
||||
'custom-user@git.example.com:organization/repository.git',
|
||||
'https://git.example.com/organization/repository.git',
|
||||
],
|
||||
'custom username and port' => [
|
||||
'custom-user@git.example.com:2222/organization/repository.git',
|
||||
'https://git.example.com/organization/repository.git',
|
||||
],
|
||||
]);
|
||||
|
||||
it('rejects non-scp-style git urls', function (string $url) {
|
||||
expect(parseScpStyleGitUrl($url))->toBeNull()
|
||||
->and(scpStyleGitUrlToHttps($url))->toBeNull();
|
||||
})->with([
|
||||
'https' => 'https://github.com/organization/repository.git',
|
||||
'email without path' => 'custom-user@git.example.com',
|
||||
'ssh scheme' => 'ssh://git@github.com/organization/repository.git',
|
||||
'empty' => '',
|
||||
]);
|
||||
|
||||
it('normalizes github app repository slugs from scp-style ssh urls', function (string $url, string $expected) {
|
||||
expect(gitRepositorySlug($url))->toBe($expected);
|
||||
})->with([
|
||||
'https' => ['https://github.com/organization/repository.git', 'organization/repository'],
|
||||
'owner/repo' => ['organization/repository', 'organization/repository'],
|
||||
'git username' => ['git@github.com:organization/repository.git', 'organization/repository'],
|
||||
'custom username' => ['custom-user@git.example.com:organization/repository.git', 'organization/repository'],
|
||||
'custom username and port' => ['custom-user@git.example.com:2222/organization/repository.git', 'organization/repository'],
|
||||
]);
|
||||
@@ -108,6 +108,8 @@ it('validates SSH URLs when allowed', function () {
|
||||
'git@gitlab.com:user/repo.git',
|
||||
'git@bitbucket.org:user/repo.git',
|
||||
'custom-user@git.example.com:organization/repository.git',
|
||||
'custom-user@git.example.com:2222/organization/repository.git',
|
||||
'enterprise-user@enterprise.ghe.com:organization/repository.git',
|
||||
];
|
||||
|
||||
foreach ($validUrls as $url) {
|
||||
@@ -130,6 +132,7 @@ it('rejects SSH URLs when not allowed', function () {
|
||||
$invalidUrls = [
|
||||
'git@github.com:user/repo.git',
|
||||
'git@gitlab.com:user/repo.git',
|
||||
'custom-user@git.example.com:organization/repository.git',
|
||||
];
|
||||
|
||||
foreach ($invalidUrls as $url) {
|
||||
|
||||
Reference in New Issue
Block a user