mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(domains): skip favicons for private network hosts (#12107)
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* Builds the favicon URL that the browser loads for a domain row.
|
||||
*
|
||||
* Domains on a private network get no favicon: loading it from a public Coolify page triggers the
|
||||
* Chrome "access other devices on your local network" permission prompt.
|
||||
*/
|
||||
class DomainFavicon
|
||||
{
|
||||
/** @var list<string> */
|
||||
private const LOCAL_SUFFIXES = ['.localhost', '.local', '.lan', '.internal', '.home.arpa', '.test'];
|
||||
|
||||
public static function url(string $publicUrl, ?string $dnsStatus = null, ?string $expectedIp = null): ?string
|
||||
{
|
||||
$parts = parse_url($publicUrl);
|
||||
if (! is_array($parts) || ! isset($parts['scheme'], $parts['host'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (self::isPrivateHost($parts['host'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($dnsStatus === 'ok' && filled($expectedIp) && ! DnsRecordHints::isPublicAddress($expectedIp)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$port = isset($parts['port']) ? ':'.$parts['port'] : '';
|
||||
|
||||
return $parts['scheme'].'://'.$parts['host'].$port.'/favicon.ico';
|
||||
}
|
||||
|
||||
private static function isPrivateHost(string $host): bool
|
||||
{
|
||||
$host = strtolower(trim($host, '[]'));
|
||||
|
||||
if (filter_var($host, FILTER_VALIDATE_IP) !== false) {
|
||||
return ! DnsRecordHints::isPublicAddress($host);
|
||||
}
|
||||
|
||||
if ($host === 'localhost') {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (self::LOCAL_SUFFIXES as $suffix) {
|
||||
if (str_ends_with($host, $suffix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/(?:^|\.)(\d{1,3}(?:[.-]\d{1,3}){3})\.(?:sslip|nip)\.io$/', $host, $matches) === 1) {
|
||||
$embeddedIp = str_replace('-', '.', $matches[1]);
|
||||
|
||||
return filter_var($embeddedIp, FILTER_VALIDATE_IP) === false
|
||||
|| ! DnsRecordHints::isPublicAddress($embeddedIp);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,7 @@
|
||||
};
|
||||
$gridClass = 'service-domains-overview-grid';
|
||||
$publicUrl = getFqdnWithoutPort($row['url']);
|
||||
$domainParts = $isSuggested ? null : parse_url($publicUrl);
|
||||
$faviconUrl = is_array($domainParts) && isset($domainParts['scheme'], $domainParts['host'])
|
||||
? $domainParts['scheme'].'://'.$domainParts['host'].'/favicon.ico'
|
||||
: null;
|
||||
$faviconUrl = $isSuggested ? null : \App\Support\DomainFavicon::url($publicUrl, $row['dns_status'] ?? null, $row['expected_ip'] ?? null);
|
||||
$rowDirection = $isCompose
|
||||
? ($serviceRedirects[$this->serviceRedirectWireKey($row['service'])] ?? 'both')
|
||||
: $redirect;
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
? \Illuminate\Support\Str::headline($row['service_name'])
|
||||
: '-';
|
||||
$publicUrl = getFqdnWithoutPort($row['url']);
|
||||
$domainParts = $isSuggested ? null : parse_url($publicUrl);
|
||||
$isNoindexed = $service->applications->firstWhere('id', $row['service_application_id'])?->isDomainNoindexed($row['url']);
|
||||
$rowDirection = $serviceRedirects[$row['service_application_id']] ?? 'both';
|
||||
$directionLabel = match ($rowDirection) {
|
||||
@@ -53,9 +52,7 @@
|
||||
'non-www' => 'Redirect to non-www',
|
||||
default => 'Both www and non-www',
|
||||
};
|
||||
$faviconUrl = is_array($domainParts) && isset($domainParts['scheme'], $domainParts['host'])
|
||||
? $domainParts['scheme'].'://'.$domainParts['host'].(isset($domainParts['port']) ? ':'.$domainParts['port'] : '').'/favicon.ico'
|
||||
: null;
|
||||
$faviconUrl = $isSuggested ? null : \App\Support\DomainFavicon::url($publicUrl, $row['dns_status'] ?? null, $row['expected_ip'] ?? null);
|
||||
$domainKey = hash('sha256', $row['url'].'|'.($row['service_application_id'] ?? ''));
|
||||
$editingParts = \App\Support\DomainUrlParts::split($row['url']);
|
||||
if ($row['has_port_override'] ?? false) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use App\Support\DomainFavicon;
|
||||
|
||||
it('builds the favicon URL for a public domain', function (string $url, string $expected) {
|
||||
expect(DomainFavicon::url($url))->toBe($expected);
|
||||
})->with([
|
||||
'domain' => ['https://app.example.com', 'https://app.example.com/favicon.ico'],
|
||||
'domain with port' => ['https://app.example.com:8443', 'https://app.example.com:8443/favicon.ico'],
|
||||
'public IP' => ['http://8.8.8.8', 'http://8.8.8.8/favicon.ico'],
|
||||
'public sslip.io' => ['http://app.1.2.3.4.sslip.io', 'http://app.1.2.3.4.sslip.io/favicon.ico'],
|
||||
]);
|
||||
|
||||
it('skips the favicon for a private network host', function (string $url) {
|
||||
expect(DomainFavicon::url($url))->toBeNull();
|
||||
})->with([
|
||||
'private IPv4' => ['http://192.168.1.10'],
|
||||
'unique local IPv6' => ['http://[fd00::1]'],
|
||||
'tailscale CGNAT' => ['http://100.101.102.103'],
|
||||
'loopback' => ['http://127.0.0.1:8000'],
|
||||
'localhost' => ['http://localhost'],
|
||||
'mDNS name' => ['http://nas.local'],
|
||||
'internal name' => ['http://app.internal'],
|
||||
'private sslip.io' => ['http://app.10.0.0.5.sslip.io'],
|
||||
'private nip.io with dashes' => ['http://10-0-0-5.nip.io'],
|
||||
'invalid URL' => ['not a url'],
|
||||
]);
|
||||
|
||||
it('skips the favicon when the DNS check matched a private server IP', function () {
|
||||
expect(DomainFavicon::url('https://app.example.com', 'ok', '10.0.0.5'))->toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the favicon when the DNS check did not confirm a private server IP', function (?string $status, ?string $expectedIp) {
|
||||
expect(DomainFavicon::url('https://app.example.com', $status, $expectedIp))
|
||||
->toBe('https://app.example.com/favicon.ico');
|
||||
})->with([
|
||||
'mismatch' => ['failed', '10.0.0.5'],
|
||||
'public server IP' => ['ok', '8.8.8.8'],
|
||||
'no server IP' => ['ok', null],
|
||||
]);
|
||||
Reference in New Issue
Block a user