Files
git.stella-ops.org/src/Router/StellaOps.Gateway.WebService/Configuration/GatewayOptionsValidator.cs

175 lines
7.1 KiB
C#

using System.Text.RegularExpressions;
using StellaOps.Router.Gateway.Configuration;
namespace StellaOps.Gateway.WebService.Configuration;
public static class GatewayOptionsValidator
{
public static void Validate(GatewayOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (string.IsNullOrWhiteSpace(options.Node.Region))
{
throw new InvalidOperationException("Gateway node region is required.");
}
if (options.Transports.Tcp.Enabled && options.Transports.Tcp.Port <= 0)
{
throw new InvalidOperationException("TCP transport port must be greater than zero.");
}
if (options.Transports.Tls.Enabled)
{
if (options.Transports.Tls.Port <= 0)
{
throw new InvalidOperationException("TLS transport port must be greater than zero.");
}
if (string.IsNullOrWhiteSpace(options.Transports.Tls.CertificatePath))
{
throw new InvalidOperationException("TLS transport requires a certificate path when enabled.");
}
}
_ = GatewayValueParser.ParseDuration(options.Routing.DefaultTimeout, TimeSpan.FromSeconds(30));
_ = GatewayValueParser.ParseDuration(options.Routing.GlobalTimeoutCap, TimeSpan.FromSeconds(120));
_ = GatewayValueParser.ParseSizeBytes(options.Routing.MaxRequestBodySize, 0);
_ = GatewayValueParser.ParseDuration(options.Health.StaleThreshold, TimeSpan.FromSeconds(30));
_ = GatewayValueParser.ParseDuration(options.Health.DegradedThreshold, TimeSpan.FromSeconds(15));
_ = GatewayValueParser.ParseDuration(options.Health.CheckInterval, TimeSpan.FromSeconds(5));
if (options.Health.RequiredMicroservices.Any(service => string.IsNullOrWhiteSpace(service)))
{
throw new InvalidOperationException("Gateway health required microservices must not contain empty values.");
}
ValidateRoutes(options.Routes);
}
private static void ValidateRoutes(List<StellaOpsRoute> routes)
{
var exactPathIndices = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < routes.Count; i++)
{
var route = routes[i];
var prefix = $"Route[{i}]";
if (string.IsNullOrWhiteSpace(route.Path))
{
throw new InvalidOperationException($"{prefix}: Path must not be empty.");
}
if (route.IsRegex)
{
try
{
_ = new Regex(route.Path, RegexOptions.Compiled, TimeSpan.FromSeconds(1));
}
catch (ArgumentException ex)
{
throw new InvalidOperationException($"{prefix}: Path is not a valid regex pattern: {ex.Message}");
}
}
else
{
var normalizedPath = NormalizePath(route.Path);
if (exactPathIndices.TryGetValue(normalizedPath, out var existingIndex))
{
throw new InvalidOperationException(
$"{prefix}: Duplicate route path '{normalizedPath}' already defined by Route[{existingIndex}].");
}
exactPathIndices[normalizedPath] = i;
}
switch (route.Type)
{
case StellaOpsRouteType.ReverseProxy:
if (string.IsNullOrWhiteSpace(route.TranslatesTo) ||
!Uri.TryCreate(route.TranslatesTo, UriKind.Absolute, out var proxyUri) ||
(proxyUri.Scheme != "http" && proxyUri.Scheme != "https"))
{
throw new InvalidOperationException($"{prefix}: ReverseProxy requires a valid HTTP(S) URL in TranslatesTo.");
}
break;
case StellaOpsRouteType.StaticFiles:
if (string.IsNullOrWhiteSpace(route.TranslatesTo))
{
throw new InvalidOperationException($"{prefix}: StaticFiles requires a directory path in TranslatesTo.");
}
break;
case StellaOpsRouteType.StaticFile:
if (string.IsNullOrWhiteSpace(route.TranslatesTo))
{
throw new InvalidOperationException($"{prefix}: StaticFile requires a file path in TranslatesTo.");
}
break;
case StellaOpsRouteType.WebSocket:
if (string.IsNullOrWhiteSpace(route.TranslatesTo) ||
!Uri.TryCreate(route.TranslatesTo, UriKind.Absolute, out var wsUri) ||
(wsUri.Scheme != "ws" && wsUri.Scheme != "wss"))
{
throw new InvalidOperationException($"{prefix}: WebSocket requires a valid ws:// or wss:// URL in TranslatesTo.");
}
break;
case StellaOpsRouteType.NotFoundPage:
if (string.IsNullOrWhiteSpace(route.TranslatesTo))
{
throw new InvalidOperationException($"{prefix}: NotFoundPage requires a file path in TranslatesTo.");
}
break;
case StellaOpsRouteType.ServerErrorPage:
if (string.IsNullOrWhiteSpace(route.TranslatesTo))
{
throw new InvalidOperationException($"{prefix}: ServerErrorPage requires a file path in TranslatesTo.");
}
break;
case StellaOpsRouteType.Microservice:
if (!string.IsNullOrWhiteSpace(route.DefaultTimeout))
{
_ = GatewayValueParser.ParseDuration(route.DefaultTimeout, TimeSpan.FromSeconds(30));
}
if (route.IsRegex && !string.IsNullOrWhiteSpace(route.TranslatesTo))
{
var regex = new Regex(route.Path);
var groupCount = regex.GetGroupNumbers().Length;
var refs = Regex.Matches(route.TranslatesTo, @"\$(\d+)");
foreach (Match refMatch in refs)
{
var groupNum = int.Parse(refMatch.Groups[1].Value);
if (groupNum >= groupCount)
{
throw new InvalidOperationException(
$"{prefix}: TranslatesTo references ${groupNum} but regex only has {groupCount - 1} capture groups.");
}
}
}
break;
}
}
}
private static string NormalizePath(string value)
{
var normalized = value.Trim();
if (!normalized.StartsWith('/'))
{
normalized = "/" + normalized;
}
normalized = normalized.TrimEnd('/');
return string.IsNullOrEmpty(normalized) ? "/" : normalized;
}
}