mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
- database import: add keep_owners (restore PostgreSQL owners and privileges) and sqlite_database (pick the target SQLite file) to the API, OpenAPI spec, and import form - oauth: require password confirmation and support team selection on OAuth login - remote process: send team broadcast events to the team of the server that ran the process instead of the causer's user id - servers API: handle the legacy build server setting - compose: support parent bind paths and volume driver options - dns: improve record hints and provider management - notifications: fix throttling and missing backup notifications - ui: rework the confirmation modal - add feature and unit tests for the new behavior
197 lines
5.8 KiB
PHP
197 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
/**
|
|
* Provider-agnostic DNS records to point hostnames at a Coolify server.
|
|
*/
|
|
class DnsRecordHints
|
|
{
|
|
public const NO_PUBLIC_ADDRESS_MESSAGE = 'The server has no public IP address; add the DNS record manually.';
|
|
|
|
/**
|
|
* Whether an address may be published as a public A/AAAA record: private, reserved, loopback, link-local,
|
|
* unique local, CGNAT (100.64.0.0/10, used by Tailscale), and multicast addresses are rejected.
|
|
*/
|
|
public static function isPublicAddress(string $address): bool
|
|
{
|
|
$binary = @inet_pton($address);
|
|
if ($binary === false) {
|
|
return false;
|
|
}
|
|
|
|
if (strlen($binary) === 16 && str_starts_with($binary, str_repeat("\0", 10)."\xff\xff")) {
|
|
$address = (string) inet_ntop(substr($binary, 12));
|
|
$binary = substr($binary, 12);
|
|
}
|
|
|
|
if (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
|
return false;
|
|
}
|
|
|
|
if (strlen($binary) === 4) {
|
|
$firstOctet = ord($binary[0]);
|
|
$isCgnat = $firstOctet === 100 && (ord($binary[1]) & 0xC0) === 64;
|
|
|
|
return ! $isCgnat && ($firstOctet & 0xF0) !== 224;
|
|
}
|
|
|
|
return ord($binary[0]) !== 0xFF;
|
|
}
|
|
|
|
/**
|
|
* Build A/AAAA entries for every hostname (deduped).
|
|
*
|
|
* @param array<int, string|null> $hostnames
|
|
* @return array<int, array{type: string, name: string, value: string}>
|
|
*/
|
|
public static function forHostnames(array $hostnames, ?string $ipv4, ?string $ipv6 = null): array
|
|
{
|
|
$records = [];
|
|
$seen = [];
|
|
|
|
foreach ($hostnames as $hostname) {
|
|
if (! is_string($hostname) || trim($hostname) === '') {
|
|
continue;
|
|
}
|
|
|
|
foreach (self::forTarget($hostname, $ipv4, $ipv6) as $record) {
|
|
$key = strtolower($record['type'].'|'.$record['name'].'|'.$record['value']);
|
|
if (isset($seen[$key])) {
|
|
continue;
|
|
}
|
|
$seen[$key] = true;
|
|
$records[] = $record;
|
|
}
|
|
}
|
|
|
|
usort($records, function (array $a, array $b): int {
|
|
return [$a['name'], $a['type'], $a['value']] <=> [$b['name'], $b['type'], $b['value']];
|
|
});
|
|
|
|
return $records;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array{type: string, name: string, value: string}>
|
|
*/
|
|
public static function forTarget(?string $hostname, ?string $ipv4, ?string $ipv6 = null): array
|
|
{
|
|
$records = [];
|
|
$fqdn = self::normalizeHostname($hostname);
|
|
if ($fqdn === null) {
|
|
return [];
|
|
}
|
|
|
|
if (filled($ipv4) && filter_var($ipv4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) {
|
|
$records[] = [
|
|
'type' => 'A',
|
|
'name' => $fqdn,
|
|
'value' => $ipv4,
|
|
];
|
|
}
|
|
|
|
if (filled($ipv6) && filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) {
|
|
$records[] = [
|
|
'type' => 'AAAA',
|
|
'name' => $fqdn,
|
|
'value' => $ipv6,
|
|
];
|
|
}
|
|
|
|
return $records;
|
|
}
|
|
|
|
public static function normalizeHostname(?string $hostname): ?string
|
|
{
|
|
if (blank($hostname)) {
|
|
return null;
|
|
}
|
|
|
|
$hostname = strtolower(trim($hostname));
|
|
$hostname = preg_replace('#^https?://#i', '', $hostname) ?? $hostname;
|
|
$hostname = explode('/', $hostname)[0] ?? $hostname;
|
|
$hostname = explode(':', $hostname)[0] ?? $hostname;
|
|
$hostname = rtrim($hostname, '.');
|
|
|
|
if ($hostname === '' || $hostname === '@' || ! str_contains($hostname, '.')) {
|
|
return null;
|
|
}
|
|
|
|
// Strip path-like noise; host only.
|
|
if (filter_var($hostname, FILTER_VALIDATE_IP)) {
|
|
return null;
|
|
}
|
|
|
|
return $hostname;
|
|
}
|
|
|
|
/**
|
|
* Relative name for a zone (e.g. app for app.example.com, @ for example.com).
|
|
*/
|
|
public static function relativeName(?string $hostname): string
|
|
{
|
|
$fqdn = self::normalizeHostname($hostname);
|
|
if ($fqdn === null) {
|
|
return '@';
|
|
}
|
|
|
|
$labels = array_values(array_filter(explode('.', $fqdn), fn (string $p) => $p !== ''));
|
|
if (count($labels) <= 2) {
|
|
return '@';
|
|
}
|
|
|
|
return implode('.', array_slice($labels, 0, -2));
|
|
}
|
|
|
|
/**
|
|
* BIND-compatible zone snippet for clipboard (absolute names with trailing dots).
|
|
*
|
|
* Example:
|
|
* asd.hu. IN A 172.16.0.2
|
|
* www.asd.hu. IN A 172.16.0.2
|
|
*
|
|
* @param array<int, array{type: string, name: string, value: string}> $records
|
|
*/
|
|
public static function toCopyText(array $records): string
|
|
{
|
|
if ($records === []) {
|
|
return '';
|
|
}
|
|
|
|
$lines = [];
|
|
$nameWidth = 0;
|
|
|
|
foreach ($records as $record) {
|
|
$name = self::bindAbsoluteName((string) $record['name']);
|
|
$nameWidth = max($nameWidth, strlen($name));
|
|
}
|
|
|
|
foreach ($records as $record) {
|
|
$name = self::bindAbsoluteName((string) $record['name']);
|
|
$type = strtoupper((string) $record['type']);
|
|
$value = (string) $record['value'];
|
|
// AAAA values may be IPv6; leave as-is (no quotes needed for A/AAAA).
|
|
$format = '%-'.$nameWidth.'s IN %-5s %s';
|
|
$lines[] = sprintf($format, $name, $type, $value);
|
|
}
|
|
|
|
return implode("\n", $lines)."\n";
|
|
}
|
|
|
|
/**
|
|
* Absolute BIND name (trailing dot). Leaves @ as-is.
|
|
*/
|
|
public static function bindAbsoluteName(string $name): string
|
|
{
|
|
$name = trim($name);
|
|
if ($name === '' || $name === '@') {
|
|
return '@';
|
|
}
|
|
|
|
$name = rtrim($name, '.');
|
|
|
|
return $name.'.';
|
|
}
|
|
}
|