Files
coolify/app/Services/ProxyPortParser.php
T

153 lines
4.3 KiB
PHP

<?php
namespace App\Services;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
class ProxyPortParser
{
/**
* @return list<int>
*/
public static function fromConfiguration(string $configuration): array
{
try {
$parsed = Yaml::parse($configuration);
} catch (ParseException $exception) {
throw new \InvalidArgumentException('The proxy configuration must contain valid YAML.', previous: $exception);
}
if (! is_array($parsed)) {
return [];
}
$ports = [];
foreach (['traefik', 'caddy'] as $proxyService) {
$path = "services.{$proxyService}.ports";
if (! data_has($parsed, $path)) {
continue;
}
$configuredPorts = data_get($parsed, $path);
if (! is_array($configuredPorts) || ! array_is_list($configuredPorts)) {
self::invalid();
}
foreach ($configuredPorts as $configuredPort) {
$ports[] = self::publishedPort($configuredPort);
}
}
return array_values(array_unique($ports));
}
private static function publishedPort(mixed $configuredPort): int
{
if (is_array($configuredPort)) {
if (array_is_list($configuredPort) || ! array_key_exists('target', $configuredPort)) {
self::invalid();
}
self::validateProtocol($configuredPort['protocol'] ?? null);
if (array_key_exists('host_ip', $configuredPort)) {
self::validateHostIp($configuredPort['host_ip']);
}
$target = self::portNumber($configuredPort['target']);
return array_key_exists('published', $configuredPort)
? self::portNumber($configuredPort['published'])
: $target;
}
if (! is_int($configuredPort) && ! is_string($configuredPort)) {
self::invalid();
}
if (is_int($configuredPort)) {
return self::portNumber($configuredPort);
}
$portDefinition = $configuredPort;
$protocolSeparator = strrpos($portDefinition, '/');
if ($protocolSeparator !== false) {
self::validateProtocol(substr($portDefinition, $protocolSeparator + 1));
$portDefinition = substr($portDefinition, 0, $protocolSeparator);
}
if (str_starts_with($portDefinition, '[')) {
if (! preg_match('/^\[([^]]+)]:(\d+):(\d+)$/D', $portDefinition, $matches)) {
self::invalid();
}
self::validateHostIp($matches[1]);
self::portNumber($matches[3]);
return self::portNumber($matches[2]);
}
$parts = explode(':', $portDefinition);
if (count($parts) < 1 || count($parts) > 3) {
self::invalid();
}
if (count($parts) === 3 && $parts[0] === '') {
self::invalid();
}
if (count($parts) === 3) {
self::validateHostIp($parts[0]);
}
$portParts = count($parts) === 3 ? array_slice($parts, 1) : $parts;
foreach ($portParts as $part) {
self::portNumber($part);
}
return self::portNumber($portParts[0]);
}
private static function portNumber(mixed $port): int
{
if (is_int($port)) {
if ($port < 1 || $port > 65535) {
self::invalid();
}
return $port;
}
if (! is_string($port) || preg_match('/^\d+$/D', $port) !== 1) {
self::invalid();
}
$normalized = (int) $port;
if ($normalized < 1 || $normalized > 65535) {
self::invalid();
}
return $normalized;
}
private static function validateProtocol(mixed $protocol): void
{
if ($protocol !== null && (! is_string($protocol) || ! in_array($protocol, ['tcp', 'udp'], true))) {
self::invalid();
}
}
private static function validateHostIp(mixed $hostIp): void
{
if (! is_string($hostIp) || filter_var($hostIp, FILTER_VALIDATE_IP) === false) {
self::invalid();
}
}
private static function invalid(): never
{
throw new \InvalidArgumentException('Proxy ports must be integers from 1 through 65535.');
}
}