mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-28 17:44:36 +08:00
Store server IPs in one form (no brackets, short lower-case IPv6) and write each address in the form its context needs: - URLs and host:port (Sentinel, database URLs, SFTP backup download, DNS servers, SMTP, webhook base URLs, links, health checks) get brackets through formatHostForUrl(). - Traefik Host() rules use the address without brackets, so IPv6 domains no longer return 404; saved labels are fixed at deploy. Caddy keeps the brackets. - SSH keeps the plain address. - Hetzner gives an IPv6 /64 network; IPv6-only servers now get the server address (prefix::1) instead of the unusable prefix::64. - sslip labels, registry image names with an IPv6 host, the API domain list, allowlist matching and duplicate checks handle IPv6. A migration normalizes stored IPv6 server addresses, repairs Hetzner servers saved as ::64 that are not reachable, and adds brackets to saved Sentinel URLs.
72 lines
2.3 KiB
PHP
72 lines
2.3 KiB
PHP
<?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);
|
|
}
|
|
|
|
// sslip.io writes an IPv6 address with dashes: fd00--1.sslip.io is fd00::1.
|
|
if (preg_match('/(?:^|\.)([0-9a-f]*-[0-9a-f-]*)\.(?:sslip|nip)\.io$/', $host, $matches) === 1) {
|
|
$embeddedIp = str_replace('-', ':', $matches[1]);
|
|
|
|
return filter_var($embeddedIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false
|
|
&& ! DnsRecordHints::isPublicAddress($embeddedIp);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|