ce9fa07ff8
The Kestrel re-bind read only urls/ASPNETCORE_URLS. The aspnet:8.0+ base images set ASPNETCORE_HTTP_PORTS=8080 (all interfaces) as the CONTAINER default in place of ASPNETCORE_URLS, so a driver node that never sets URLS fell straight through to Kestrel's localhost:5000 default — silently moving its health/metrics surface to loopback when the sync listener took over Kestrel. Now falls through URLS → HTTP_PORTS/HTTPS_PORTS (wildcard, all-interfaces) → localhost:5000, mirroring Kestrel's own source precedence. Found on the docker-dev live gate (central nodes were fine — they set ASPNETCORE_URLS=9000 explicitly).
103 lines
4.9 KiB
C#
103 lines
4.9 KiB
C#
using System.Net;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
|
|
|
/// <summary>
|
|
/// One HTTP endpoint the host was asked to serve, parsed out of the configured URL list so it
|
|
/// can be re-bound explicitly.
|
|
/// </summary>
|
|
/// <param name="Host">The host component as written — <c>+</c>, <c>*</c>, a hostname, or an IP.</param>
|
|
/// <param name="Port">The port.</param>
|
|
/// <param name="IsSecure">True when the URL used the <c>https</c> scheme.</param>
|
|
public sealed record KestrelHttpBinding(string Host, int Port, bool IsSecure)
|
|
{
|
|
/// <summary>
|
|
/// Parses a semicolon-separated URL list (the <c>ASPNETCORE_URLS</c> / <c>urls</c> format)
|
|
/// into bindings. Unparseable entries are skipped rather than throwing — a malformed URL
|
|
/// must not take the process down at startup.
|
|
/// </summary>
|
|
/// <param name="urls">The configured URL list, or null/empty when none is configured.</param>
|
|
/// <returns>The parsed bindings, in the order given; empty when nothing is configured.</returns>
|
|
public static IReadOnlyList<KestrelHttpBinding> Parse(string? urls)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(urls))
|
|
return [];
|
|
|
|
var result = new List<KestrelHttpBinding>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the bare-port list format of <c>ASPNETCORE_HTTP_PORTS</c> / <c>HTTP_PORTS</c> (and
|
|
/// the <c>HTTPS</c> variants) into wildcard (<c>+</c>, all-interfaces) bindings — the shape
|
|
/// Kestrel itself gives those variables. Modern .NET base images (aspnet:8.0+) set
|
|
/// <c>ASPNETCORE_HTTP_PORTS=8080</c> as the container default <b>instead of</b>
|
|
/// <c>ASPNETCORE_URLS</c>, so a node that never sets <c>URLS</c> explicitly still has a real
|
|
/// configured surface here that must be re-bound, not replaced with Kestrel's localhost:5000.
|
|
/// </summary>
|
|
/// <param name="ports">A <c>;</c>- or <c>,</c>-separated list of bare ports, or null/empty.</param>
|
|
/// <param name="isSecure">True to mark the parsed bindings <c>https</c> (the <c>HTTPS_PORTS</c> vars).</param>
|
|
/// <returns>One all-interfaces binding per parseable port; empty when nothing is configured.</returns>
|
|
public static IReadOnlyList<KestrelHttpBinding> ParsePorts(string? ports, bool isSecure)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(ports))
|
|
return [];
|
|
|
|
var result = new List<KestrelHttpBinding>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies this binding to Kestrel, choosing the narrowest listen call the host component
|
|
/// allows.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A hostname that is neither a literal IP nor <c>localhost</c> cannot be reliably mapped to
|
|
/// a local interface, so it falls back to <see cref="KestrelServerOptions.ListenAnyIP"/> —
|
|
/// the safe superset. Narrowing it and guessing wrong would silently stop serving.
|
|
/// </remarks>
|
|
/// <param name="options">The Kestrel options being configured.</param>
|
|
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);
|
|
}
|
|
}
|