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);
}
}