diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 75826f2d4e..cd047b1f43 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -201,6 +201,31 @@ class Index extends Component } } + /** + * Look up the public IPs of this host again and save them. + */ + public function detectPublicIps() + { + try { + $this->authorize('update', $this->settings); + + $detected = detect_public_ips(); + if ($detected === []) { + $this->dispatch('error', 'Could not detect a public IP address.'); + + return; + } + + $this->settings->fill($detected); + $this->saveAndAudit(); + $this->public_ipv4 = $this->settings->public_ipv4; + $this->public_ipv6 = $this->settings->public_ipv6; + $this->dispatch('success', 'Detected '.implode(' and ', $detected).'.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function buildHelperImage() { try { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 136c34589e..db202da591 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2684,38 +2684,46 @@ function get_public_ips(): void { try { $settings = InstanceSettings::get(); - $versions = array_filter( - ['public_ipv4' => 4, 'public_ipv6' => 6], - fn (string $column) => blank($settings->$column), - ARRAY_FILTER_USE_KEY, - ); - if ($versions === []) { + $columns = array_values(array_filter(['public_ipv4', 'public_ipv6'], fn (string $column) => blank($settings->$column))); + if ($columns === []) { return; } - $results = Process::concurrently(function (Pool $pool) use ($versions) { - foreach ($versions as $column => $version) { - $pool->as($column)->path(__DIR__)->command("curl -{$version}s https://ifconfig.io"); - } - }); - - foreach ($versions as $column => $version) { - $ip = trim($results[$column]->output()); - if ($ip === '') { - continue; - } - if (filter_var($ip, FILTER_VALIDATE_IP, $version === 4 ? FILTER_FLAG_IPV4 : FILTER_FLAG_IPV6) === false) { - echo "Invalid ipv{$version}: $ip\n"; - - continue; - } - $settings->update([$column => $ip]); + $detected = detect_public_ips($columns); + if ($detected !== []) { + $settings->update($detected); } } catch (Throwable $e) { echo "Error: {$e->getMessage()}\n"; } } +/** + * Look up the public IPs of this host with ifconfig.io, without saving them. + * + * @param list<'public_ipv4'|'public_ipv6'> $columns + * @return array<'public_ipv4'|'public_ipv6', string> valid addresses only + */ +function detect_public_ips(array $columns = ['public_ipv4', 'public_ipv6']): array +{ + $versions = array_intersect_key(['public_ipv4' => 4, 'public_ipv6' => 6], array_flip($columns)); + $results = Process::concurrently(function (Pool $pool) use ($versions) { + foreach ($versions as $column => $version) { + $pool->as($column)->path(__DIR__)->command("curl -{$version}s https://ifconfig.io"); + } + }); + + $detected = []; + foreach ($versions as $column => $version) { + $ip = trim($results[$column]->output()); + if (filter_var($ip, FILTER_VALIDATE_IP, $version === 4 ? FILTER_FLAG_IPV4 : FILTER_FLAG_IPV6) !== false) { + $detected[$column] = $ip; + } + } + + return $detected; +} + function isAnyDeploymentInprogress(bool $showAll = false) { $runningJobs = ApplicationDeploymentQueue::where('horizon_job_worker', gethostname())->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)->get(); diff --git a/resources/views/livewire/settings/index.blade.php b/resources/views/livewire/settings/index.blade.php index d888145faa..6815fb9ee9 100644 --- a/resources/views/livewire/settings/index.blade.php +++ b/resources/views/livewire/settings/index.blade.php @@ -48,14 +48,20 @@ + + + Detect + +
diff --git a/tests/Feature/SettingsDetectPublicIpsTest.php b/tests/Feature/SettingsDetectPublicIpsTest.php new file mode 100644 index 0000000000..f2de861987 --- /dev/null +++ b/tests/Feature/SettingsDetectPublicIpsTest.php @@ -0,0 +1,86 @@ +set('constants.coolify.self_hosted', false); + + InstanceSettings::forceCreate(['id' => 0, 'public_ipv4' => '203.0.113.21', 'public_ipv6' => '2001:db8::21']); + Once::flush(); + + $this->rootTeam = Team::factory()->create(['id' => 0]); + $this->user = User::factory()->create(); + $this->rootTeam->members()->attach($this->user->id, ['role' => 'owner']); + $this->actingAs($this->user); + session(['currentTeam' => $this->rootTeam]); + + Process::fake([ + 'curl -4s https://ifconfig.io' => "203.0.113.1\n", + 'curl -6s https://ifconfig.io' => "2001:db8::1\n", + ]); +}); + +it('saves the detected public IPs and shows them in the fields', function () { + Livewire::test(SettingsIndex::class) + ->call('detectPublicIps') + ->assertSet('public_ipv4', '203.0.113.1') + ->assertSet('public_ipv6', '2001:db8::1') + ->assertDispatched('success'); + + $settings = InstanceSettings::find(0); + expect($settings->public_ipv4)->toBe('203.0.113.1') + ->and($settings->public_ipv6)->toBe('2001:db8::1'); +}); + +it('keeps the current value of an address family that cannot be detected', function () { + Process::fake([ + 'curl -4s https://ifconfig.io' => "203.0.113.1\n", + 'curl -6s https://ifconfig.io' => '', + ]); + + Livewire::test(SettingsIndex::class) + ->call('detectPublicIps') + ->assertSet('public_ipv4', '203.0.113.1') + ->assertSet('public_ipv6', '2001:db8::21'); + + expect(InstanceSettings::find(0)->public_ipv6)->toBe('2001:db8::21'); +}); + +it('shows an error when no address can be detected', function () { + Process::fake([ + 'curl -4s https://ifconfig.io' => '', + 'curl -6s https://ifconfig.io' => '', + ]); + + Livewire::test(SettingsIndex::class) + ->call('detectPublicIps') + ->assertSet('public_ipv4', '203.0.113.21') + ->assertSet('public_ipv6', '2001:db8::21') + ->assertDispatched('error') + ->assertNotDispatched('success'); +}); + +it('does not let a user who is no longer an instance admin detect public IPs', function () { + $component = Livewire::test(SettingsIndex::class); + + $this->rootTeam->members()->updateExistingPivot($this->user->id, ['role' => 'member']); + $this->actingAs($this->user->fresh()); + Cache::flush(); + Once::flush(); + + $component->call('detectPublicIps') + ->assertDispatched('error') + ->assertSet('public_ipv4', '203.0.113.21'); + expect(InstanceSettings::find(0)->public_ipv4)->toBe('203.0.113.21'); + Process::assertNothingRan(); +});