using System.Net; using Microsoft.AspNetCore.Server.Kestrel.Core; namespace ZB.MOM.WW.OtOpcUa.Host.Configuration; /// /// One HTTP endpoint the host was asked to serve, parsed out of the configured URL list so it /// can be re-bound explicitly. /// /// The host component as written — +, *, a hostname, or an IP. /// The port. /// True when the URL used the https scheme. public sealed record KestrelHttpBinding(string Host, int Port, bool IsSecure) { /// /// Parses a semicolon-separated URL list (the ASPNETCORE_URLS / urls format) /// into bindings. Unparseable entries are skipped rather than throwing — a malformed URL /// must not take the process down at startup. /// /// The configured URL list, or null/empty when none is configured. /// The parsed bindings, in the order given; empty when nothing is configured. public static IReadOnlyList Parse(string? urls) { if (string.IsNullOrWhiteSpace(urls)) return []; var result = new List(); foreach (var raw in urls.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { // Uri cannot parse the "+"/"*" wildcard hosts Kestrel accepts, so substitute a // placeholder host purely to get scheme/port out, then keep the original host token. var isWildcard = raw.Contains("://+", StringComparison.Ordinal) || raw.Contains("://*", StringComparison.Ordinal); var probe = isWildcard ? raw.Replace("://+", "://placeholder", StringComparison.Ordinal) .Replace("://*", "://placeholder", StringComparison.Ordinal) : raw; if (!Uri.TryCreate(probe, UriKind.Absolute, out var uri)) continue; var host = isWildcard ? raw.Contains("://+", StringComparison.Ordinal) ? "+" : "*" : uri.Host; result.Add(new KestrelHttpBinding(host, uri.Port, uri.Scheme == Uri.UriSchemeHttps)); } return result; } /// /// Parses the bare-port list format of ASPNETCORE_HTTP_PORTS / HTTP_PORTS (and /// the HTTPS variants) into wildcard (+, all-interfaces) bindings — the shape /// Kestrel itself gives those variables. Modern .NET base images (aspnet:8.0+) set /// ASPNETCORE_HTTP_PORTS=8080 as the container default instead of /// ASPNETCORE_URLS, so a node that never sets URLS explicitly still has a real /// configured surface here that must be re-bound, not replaced with Kestrel's localhost:5000. /// /// A ;- or ,-separated list of bare ports, or null/empty. /// True to mark the parsed bindings https (the HTTPS_PORTS vars). /// One all-interfaces binding per parseable port; empty when nothing is configured. public static IReadOnlyList ParsePorts(string? ports, bool isSecure) { if (string.IsNullOrWhiteSpace(ports)) return []; var result = new List(); foreach (var token in ports.Split([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { // Skip a malformed port rather than throwing — a bad env var must not down startup. if (int.TryParse(token, out var port) && port is > 0 and <= 65535) result.Add(new KestrelHttpBinding("+", port, isSecure)); } return result; } /// /// Applies this binding to Kestrel, choosing the narrowest listen call the host component /// allows. /// /// /// A hostname that is neither a literal IP nor localhost cannot be reliably mapped to /// a local interface, so it falls back to — /// the safe superset. Narrowing it and guessing wrong would silently stop serving. /// /// The Kestrel options being configured. public void Apply(KestrelServerOptions options) { ArgumentNullException.ThrowIfNull(options); if (Host is "+" or "*") options.ListenAnyIP(Port); else if (IPAddress.TryParse(Host, out var address)) options.Listen(address, Port); else if (string.Equals(Host, "localhost", StringComparison.OrdinalIgnoreCase)) options.ListenLocalhost(Port); else options.ListenAnyIP(Port); } }