stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -21,3 +21,8 @@
|
||||
|
||||
## Sprint Discipline
|
||||
- Record decisions and risks for security-sensitive changes in the sprint file.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10020, http://localhost:10021
|
||||
- Local alias: https://authority.stella-ops.local, http://authority.stella-ops.local
|
||||
- Env var: STELLAOPS_AUTHORITY_URL
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<ProjectReference Include="..\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.AspNet.Extensions/StellaOps.AspNet.Extensions.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under BUSL-1.1. See LICENSE in the project root.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Auth.ServerIntegration;
|
||||
|
||||
/// <summary>
|
||||
/// Provides two extension methods for the <c>.stella-ops.local</c> hostname convention:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <see cref="TryAddStellaOpsLocalBinding"/> — called on <see cref="WebApplicationBuilder"/>
|
||||
/// before <c>Build()</c>; binds both <c>https://{serviceName}.stella-ops.local</c> (port 443)
|
||||
/// and <c>http://{serviceName}.stella-ops.local</c> (port 80).
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <see cref="LogStellaOpsLocalHostname"/> — called on <see cref="WebApplication"/>
|
||||
/// after <c>Build()</c>; checks DNS for the friendly hostname and logs the result.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static class StellaOpsLocalHostnameExtensions
|
||||
{
|
||||
private const string DomainSuffix = ".stella-ops.local";
|
||||
private const int HttpsPort = 443;
|
||||
private const int HttpPort = 80;
|
||||
private const string SetupDocPath = "docs/technical/architecture/port-registry.md";
|
||||
|
||||
/// <summary>
|
||||
/// Configuration key used to communicate local-binding status
|
||||
/// from the builder phase to the app phase.
|
||||
/// </summary>
|
||||
internal const string LocalBindingBoundKey = "StellaOps:LocalBindingBound";
|
||||
|
||||
/// <summary>
|
||||
/// Configuration key storing the service name for use in the app phase.
|
||||
/// </summary>
|
||||
internal const string LocalBindingServiceKey = "StellaOps:LocalBindingService";
|
||||
|
||||
/// <summary>
|
||||
/// Resolves <c>{serviceName}.stella-ops.local</c> to its dedicated loopback IP
|
||||
/// (from the hosts file), then binds <c>https://{hostname}</c> (port 443) and
|
||||
/// <c>http://{hostname}</c> (port 80) on that IP. Each service uses a unique
|
||||
/// loopback address (e.g. 127.1.0.2) so ports never collide.
|
||||
/// </summary>
|
||||
public static WebApplicationBuilder TryAddStellaOpsLocalBinding(
|
||||
this WebApplicationBuilder builder, string serviceName)
|
||||
{
|
||||
builder.Configuration[LocalBindingServiceKey] = serviceName;
|
||||
var hostname = serviceName + DomainSuffix;
|
||||
|
||||
// Resolve the hostname to find its dedicated loopback IP from the hosts file.
|
||||
IPAddress? resolvedIp = null;
|
||||
try
|
||||
{
|
||||
var addresses = Dns.GetHostAddresses(hostname);
|
||||
resolvedIp = addresses.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Hostname does not resolve; skip binding (will warn at startup).
|
||||
}
|
||||
|
||||
if (resolvedIp == null)
|
||||
{
|
||||
builder.Configuration[LocalBindingBoundKey] = "false";
|
||||
return builder;
|
||||
}
|
||||
|
||||
var httpsAvailable = IsPortAvailable(HttpsPort, resolvedIp);
|
||||
var httpAvailable = IsPortAvailable(HttpPort, resolvedIp);
|
||||
|
||||
if (!httpsAvailable && !httpAvailable)
|
||||
{
|
||||
builder.Configuration[LocalBindingBoundKey] = "false";
|
||||
return builder;
|
||||
}
|
||||
|
||||
builder.Configuration[LocalBindingBoundKey] = "true";
|
||||
|
||||
// Bind to the specific loopback IP (not hostname) so Kestrel uses only
|
||||
// this address, leaving other 127.1.0.x IPs available for other services.
|
||||
// UseUrls("https://hostname") would bind to [::]:443 (all interfaces).
|
||||
//
|
||||
// When ConfigureKestrel uses explicit Listen() calls, Kestrel ignores UseUrls.
|
||||
// So we must also re-add the dev-port bindings from launchSettings.json.
|
||||
var currentUrls = builder.WebHost.GetSetting(WebHostDefaults.ServerUrlsKey) ?? "";
|
||||
var ip = resolvedIp;
|
||||
builder.WebHost.ConfigureKestrel((context, kestrel) =>
|
||||
{
|
||||
// Re-add dev-port bindings from launchSettings.json / ASPNETCORE_URLS
|
||||
foreach (var rawUrl in currentUrls.Split(';', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (Uri.TryCreate(rawUrl.Trim(), UriKind.Absolute, out var uri))
|
||||
{
|
||||
var isHttps = string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase);
|
||||
var addr = uri.Host == "localhost"
|
||||
? IPAddress.Loopback
|
||||
: IPAddress.Parse(uri.Host);
|
||||
|
||||
if (isHttps)
|
||||
{
|
||||
kestrel.Listen(addr, uri.Port, lo => lo.UseHttps());
|
||||
}
|
||||
else
|
||||
{
|
||||
kestrel.Listen(addr, uri.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add .stella-ops.local bindings on the dedicated loopback IP
|
||||
if (httpsAvailable)
|
||||
{
|
||||
kestrel.Listen(ip, HttpsPort, listenOptions =>
|
||||
{
|
||||
listenOptions.UseHttps();
|
||||
});
|
||||
}
|
||||
|
||||
if (httpAvailable)
|
||||
{
|
||||
kestrel.Listen(ip, HttpPort);
|
||||
}
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backwards-compatible overload — reads the service name from configuration
|
||||
/// set by <see cref="TryAddStellaOpsLocalBinding"/>.
|
||||
/// </summary>
|
||||
[System.Obsolete("Use TryAddStellaOpsLocalBinding(builder, serviceName) instead of TryAddStellaOpsSharedPort(). This overload will be removed.")]
|
||||
public static WebApplicationBuilder TryAddStellaOpsSharedPort(
|
||||
this WebApplicationBuilder builder)
|
||||
{
|
||||
// No-op fallback for any callers not yet migrated; the binding happens
|
||||
// in TryAddStellaOpsLocalBinding which should be called instead.
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a startup callback that checks DNS for
|
||||
/// <c>{serviceName}.stella-ops.local</c> and logs the result.
|
||||
/// Also warns if the local bindings were skipped.
|
||||
/// </summary>
|
||||
public static WebApplication LogStellaOpsLocalHostname(
|
||||
this WebApplication app, string serviceName)
|
||||
{
|
||||
var hostname = serviceName + DomainSuffix;
|
||||
var localBindingBound = string.Equals(
|
||||
app.Configuration[LocalBindingBoundKey], "true", StringComparison.OrdinalIgnoreCase);
|
||||
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
|
||||
|
||||
lifetime.ApplicationStarted.Register(() =>
|
||||
{
|
||||
var logger = app.Services.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger("StellaOps.LocalHostname");
|
||||
|
||||
if (!localBindingBound)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Ports {HttpsPort}/{HttpPort} are already in use; skipping {Hostname} bindings. " +
|
||||
"This is expected when running multiple services locally.",
|
||||
HttpsPort, HttpPort, hostname);
|
||||
}
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||
var resolved = await Dns.GetHostAddressesAsync(hostname, cts.Token);
|
||||
|
||||
if (resolved.Length > 0)
|
||||
{
|
||||
if (localBindingBound)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Also accessible at https://{Hostname} and http://{Hostname}",
|
||||
hostname, hostname);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Hostname {Hostname} resolves but ports {HttpsPort}/{HttpPort} are unavailable; " +
|
||||
"use the dev port instead.",
|
||||
hostname, HttpsPort, HttpPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
var hostsPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? @"C:\Windows\System32\drivers\etc\hosts"
|
||||
: "/etc/hosts";
|
||||
|
||||
logger.LogWarning(
|
||||
"Hostname {Hostname} does not resolve. " +
|
||||
"To enable friendly .stella-ops.local URLs, add hosts-file entries " +
|
||||
"as described in {SetupDoc}. " +
|
||||
"Edit {HostsFile} and add a unique 127.1.0.x entry for {Hostname}",
|
||||
hostname, SetupDocPath, hostsPath, hostname);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static bool IsPortAvailable(int port, IPAddress address)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var listener = new TcpListener(address, port);
|
||||
listener.Start();
|
||||
listener.Stop();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,6 +290,7 @@ var pluginRegistrationSummary = AuthorityPluginLoader.RegisterPlugins(
|
||||
|
||||
builder.Services.AddSingleton(pluginRegistrationSummary);
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddAuthentication();
|
||||
@@ -414,7 +415,9 @@ builder.Services.Configure<OpenIddictServerOptions>(options =>
|
||||
options.DisableRollingRefreshTokens = false;
|
||||
});
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("authority");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("authority");
|
||||
|
||||
var serviceAccountStore = app.Services.GetRequiredService<IAuthorityServiceAccountStore>();
|
||||
if (authorityOptions.Delegation.ServiceAccounts.Count > 0)
|
||||
@@ -1716,6 +1719,7 @@ app.UseLegacyAuthDeprecation();
|
||||
app.UseRouting();
|
||||
app.UseAuthorityRateLimiterContext();
|
||||
app.UseRateLimiter();
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5165",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7182;http://localhost:5165",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:10021",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:10020;http://localhost:10021",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography.Kms/StellaOps.Cryptography.Kms.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration.AuthorityPlugin/StellaOps.Configuration.AuthorityPlugin.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="../../../Attestor/StellaOps.Attestation/StellaOps.Attestation.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user