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 routes) { var exactPathIndices = new Dictionary(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; } }