fix(host): central https URLs get TLS from a node certificate or refuse to boot (ScadaBridge#108)
Central re-bound its HTTP surface from ASPNETCORE_URLS keeping only the port, so https://+:443 silently served plaintext HTTP. The parser now keeps the scheme (ParseHttpBindEndpoints); an https entry binds UseHttps with the node certificate from the new ScadaBridge:Node:Tls section (CertificatePath + CertificatePassword, or CertificateThumbprint + StoreName + StoreLocation; default unset) and the node refuses to boot, with a clear error, when https is requested without a certificate. http entries bind exactly as before. Tests: scheme parsing, refuse-to-boot, http endpoint config identical to the legacy inline binding (reflected effective Kestrel listen options), and a real Kestrel HTTPS listener with a generated .pfx serving h2 and HTTP/1.1 by ALPN while plaintext to the TLS port fails. Reverting the fix turns the refuse-to-boot and real-listener tests red. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -205,7 +205,11 @@ try
|
||||
var httpUrls = configuration["ASPNETCORE_URLS"]
|
||||
?? builder.WebHost.GetSetting("urls")
|
||||
?? "http://+:5000";
|
||||
var httpPorts = ParseHttpBindPorts(httpUrls);
|
||||
var httpEndpoints = ParseHttpBindEndpoints(httpUrls);
|
||||
var httpPorts = httpEndpoints.Select(e => e.Port).ToList();
|
||||
// ScadaBridge#108: an https URL needs a configured node certificate; without one the
|
||||
// node refuses to boot rather than silently serving plaintext on that port.
|
||||
var httpsCertificate = CentralHttpBinding.LoadCertificateIfNeeded(httpEndpoints, nodeOptions.Tls);
|
||||
|
||||
// Register row 56: central's dedicated Prometheus listener, OPT-IN.
|
||||
//
|
||||
@@ -233,13 +237,7 @@ try
|
||||
options.Limits.Http2.KeepAlivePingTimeout = SiteKestrelSetup.Http2KeepAlivePingTimeout;
|
||||
// The HTTP/1.1 (+ HTTP/2) surface from the configured URLs, re-declared so it
|
||||
// survives the explicit-endpoints switch above.
|
||||
foreach (var httpPort in httpPorts)
|
||||
{
|
||||
options.ListenAnyIP(httpPort, listenOptions =>
|
||||
{
|
||||
listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2;
|
||||
});
|
||||
}
|
||||
CentralHttpBinding.BindHttpSurface(options, httpEndpoints, httpsCertificate);
|
||||
|
||||
// The gRPC control plane, HTTP/2 h2c only, on its own port.
|
||||
options.ListenAnyIP(centralGrpcPort, listenOptions =>
|
||||
@@ -942,29 +940,41 @@ public partial class Program
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the distinct TCP ports from an ASP.NET Core server-URLs string (the
|
||||
/// <c>ASPNETCORE_URLS</c> / <c>--urls</c> value, e.g. <c>"http://+:5000"</c> or a
|
||||
/// <c>ASPNETCORE_URLS</c> / <c>--urls</c> value). Port-only view of
|
||||
/// <see cref="ParseHttpBindEndpoints"/>; kept for callers that only need the ports.
|
||||
/// </summary>
|
||||
/// <param name="serverUrls">The server-URLs string; may be null/empty.</param>
|
||||
/// <returns>The distinct ports, in first-seen order.</returns>
|
||||
internal static IReadOnlyList<int> ParseHttpBindPorts(string? serverUrls) =>
|
||||
ParseHttpBindEndpoints(serverUrls).Select(e => e.Port).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the distinct (port, scheme) endpoints from an ASP.NET Core server-URLs string
|
||||
/// (the <c>ASPNETCORE_URLS</c> / <c>--urls</c> value, e.g. <c>"http://+:5000"</c> or a
|
||||
/// semicolon-separated list). Used to re-declare central's HTTP surface after the gRPC
|
||||
/// listener switches Kestrel into explicit-endpoints mode — see the call site's warning.
|
||||
/// listener switches Kestrel into explicit-endpoints mode - see the call site's warning.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Bind hosts (<c>+</c>, <c>*</c>, <c>0.0.0.0</c>, <c>[::]</c>, a hostname) are irrelevant
|
||||
/// here because the caller re-binds via <c>ListenAnyIP</c>; only the port matters. A URL
|
||||
/// with no explicit port falls back to the scheme default (80/443). Unparseable entries
|
||||
/// are skipped rather than throwing — a bad URL should not take the node down at boot.
|
||||
/// here because the caller re-binds via <c>ListenAnyIP</c>. The scheme IS kept (ScadaBridge#108):
|
||||
/// dropping it used to make <c>https://+:443</c> serve plaintext. A URL with no explicit port
|
||||
/// falls back to the scheme default (80/443). Unparseable entries are skipped rather than
|
||||
/// throwing. The same port listed with two schemes is a configuration error and throws.
|
||||
/// </remarks>
|
||||
/// <param name="serverUrls">The server-URLs string; may be null/empty.</param>
|
||||
/// <returns>The distinct ports, in first-seen order.</returns>
|
||||
internal static IReadOnlyList<int> ParseHttpBindPorts(string? serverUrls)
|
||||
/// <returns>The distinct endpoints, in first-seen order.</returns>
|
||||
internal static IReadOnlyList<HttpBindEndpoint> ParseHttpBindEndpoints(string? serverUrls)
|
||||
{
|
||||
var ports = new List<int>();
|
||||
var endpoints = new List<HttpBindEndpoint>();
|
||||
if (string.IsNullOrWhiteSpace(serverUrls))
|
||||
{
|
||||
return ports;
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
foreach (var raw in serverUrls.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
int port;
|
||||
var isHttps = raw.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
|
||||
// Uri can't parse the wildcard hosts Kestrel accepts (+, *), so normalize them
|
||||
// to a placeholder host before parsing; the host is discarded anyway.
|
||||
var normalized = raw.Replace("://+", "://placeholder").Replace("://*", "://placeholder");
|
||||
@@ -982,12 +992,21 @@ public partial class Program
|
||||
}
|
||||
}
|
||||
|
||||
if (port is > 0 and <= 65535 && !ports.Contains(port))
|
||||
if (port is > 0 and <= 65535)
|
||||
{
|
||||
ports.Add(port);
|
||||
var existing = endpoints.FindIndex(e => e.Port == port);
|
||||
if (existing < 0)
|
||||
{
|
||||
endpoints.Add(new HttpBindEndpoint(port, isHttps));
|
||||
}
|
||||
else if (endpoints[existing].IsHttps != isHttps)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"ASPNETCORE_URLS lists port {port} with both http and https schemes; a port is one or the other.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ports;
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user