Files
git.stella-ops.org/src/Router/StellaOps.Gateway.WebService/Program.cs
2026-02-17 00:51:35 +02:00

372 lines
15 KiB
C#

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Security.Dpop;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Configuration;
using StellaOps.Gateway.WebService.Authorization;
using StellaOps.Gateway.WebService.Configuration;
using StellaOps.Gateway.WebService.Middleware;
using StellaOps.Gateway.WebService.Routing;
using StellaOps.Gateway.WebService.Security;
using StellaOps.Gateway.WebService.Services;
using StellaOps.Messaging.DependencyInjection;
using StellaOps.Messaging.Transport.Valkey;
using StellaOps.Router.AspNet;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Models;
using StellaOps.Router.Common.Plugins;
using StellaOps.Router.Gateway;
using StellaOps.Router.Gateway.Configuration;
using StellaOps.Router.Gateway.DependencyInjection;
using StellaOps.Router.Gateway.Middleware;
using StellaOps.Router.Gateway.OpenApi;
using StellaOps.Router.Gateway.RateLimit;
using StellaOps.Router.Gateway.Routing;
using StellaOps.Router.Transport.Messaging;
using StellaOps.Router.Transport.Messaging.Options;
using StellaOps.Router.Transport.Tcp;
using StellaOps.Router.Transport.Tls;
using System.Net;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddStellaOpsDefaults(options =>
{
options.BasePath = builder.Environment.ContentRootPath;
options.EnvironmentPrefix = "GATEWAY_";
});
var bootstrapOptions = builder.Configuration.BindOptions<GatewayOptions>(
GatewayOptions.SectionName,
(opts, _) => GatewayOptionsValidator.Validate(opts));
builder.Services.AddOptions<GatewayOptions>()
.Bind(builder.Configuration.GetSection(GatewayOptions.SectionName))
.PostConfigure(GatewayOptionsValidator.Validate)
.ValidateOnStart();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddRouterGatewayCore();
builder.Services.AddRouterRateLimiting(builder.Configuration);
builder.Services.AddSingleton<IEffectiveClaimsStore, EffectiveClaimsStore>();
builder.Services.AddSingleton<GatewayServiceStatus>();
builder.Services.AddSingleton<GatewayMetrics>();
// Load router transport plugins
var transportPluginLoader = new RouterTransportPluginLoader(
NullLoggerFactory.Instance.CreateLogger<RouterTransportPluginLoader>());
// Try to load from plugins directory, fallback to direct registration if not found
var pluginsPath = Path.Combine(AppContext.BaseDirectory, "plugins", "router", "transports");
if (Directory.Exists(pluginsPath))
{
transportPluginLoader.LoadFromDirectory(pluginsPath);
}
// Register TCP and TLS transports (from plugins or fallback to compile-time references)
var tcpPlugin = transportPluginLoader.GetPlugin("tcp");
var tlsPlugin = transportPluginLoader.GetPlugin("tls");
if (tcpPlugin is not null)
{
tcpPlugin.Register(new RouterTransportRegistrationContext(
builder.Services, builder.Configuration, RouterTransportMode.Server)
{
ConfigurationSection = "Gateway:Transports:Tcp"
});
}
else
{
// Fallback to compile-time registration
builder.Services.AddTcpTransportServer();
}
if (tlsPlugin is not null)
{
tlsPlugin.Register(new RouterTransportRegistrationContext(
builder.Services, builder.Configuration, RouterTransportMode.Server)
{
ConfigurationSection = "Gateway:Transports:Tls"
});
}
else
{
// Fallback to compile-time registration
builder.Services.AddTlsTransportServer();
}
// Messaging transport (Valkey)
if (bootstrapOptions.Transports.Messaging.Enabled)
{
builder.Services.AddMessagingTransport<ValkeyTransportPlugin>(builder.Configuration, "Gateway:Transports:Messaging");
builder.Services.AddMessagingTransportServer();
}
builder.Services.AddSingleton<GatewayTransportClient>();
builder.Services.AddSingleton<ITransportClient>(sp => sp.GetRequiredService<GatewayTransportClient>());
builder.Services.AddSingleton<IOpenApiDocumentGenerator, OpenApiDocumentGenerator>();
builder.Services.AddSingleton<IRouterOpenApiDocumentCache, RouterOpenApiDocumentCache>();
builder.Services.AddHostedService<GatewayHostedService>();
builder.Services.AddHostedService<GatewayHealthMonitorService>();
builder.Services.AddSingleton<IDpopReplayCache, InMemoryDpopReplayCache>();
builder.Services.AddSingleton<IDpopProofValidator, DpopProofValidator>();
// Identity header policy options
builder.Services.AddSingleton(new IdentityHeaderPolicyOptions
{
EnableLegacyHeaders = bootstrapOptions.Auth.EnableLegacyHeaders,
AllowScopeHeaderOverride = bootstrapOptions.Auth.AllowScopeHeader,
JwtPassthroughPrefixes = bootstrapOptions.Routes
.Where(r => r.PreserveAuthHeaders)
.Select(r => r.Path)
.ToList()
});
// Route table: resolver + error routes + HTTP client for reverse proxy
builder.Services.AddSingleton(new StellaOpsRouteResolver(bootstrapOptions.Routes));
builder.Services.AddSingleton<IEnumerable<StellaOpsRoute>>(
bootstrapOptions.Routes.Where(r =>
r.Type == StellaOpsRouteType.NotFoundPage ||
r.Type == StellaOpsRouteType.ServerErrorPage).ToList());
builder.Services.AddHttpClient("RouteDispatch")
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
AllowAutoRedirect = false,
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
});
ConfigureAuthentication(builder, bootstrapOptions);
ConfigureGatewayOptionsMapping(builder, bootstrapOptions);
// Stella Router integration
var routerOptions = builder.Configuration.GetSection("Gateway:Router").Get<StellaRouterOptionsBase>();
builder.Services.TryAddStellaRouter(
serviceName: "gateway",
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
routerOptions: routerOptions);
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
builder.TryAddStellaOpsLocalBinding("router");
var app = builder.Build();
app.LogStellaOpsLocalHostname("router");
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseStellaOpsCors();
app.UseAuthentication();
app.UseMiddleware<SenderConstraintMiddleware>();
// IdentityHeaderPolicyMiddleware replaces TenantMiddleware and ClaimsPropagationMiddleware
// It strips reserved identity headers and overwrites them from validated claims (security fix)
app.UseMiddleware<IdentityHeaderPolicyMiddleware>();
app.UseMiddleware<HealthCheckMiddleware>();
app.TryUseStellaRouter(routerOptions);
// WebSocket support (before route dispatch)
app.UseWebSockets();
// Route dispatch for configured routes (static files, reverse proxy, websocket)
app.UseMiddleware<RouteDispatchMiddleware>();
if (bootstrapOptions.OpenApi.Enabled)
{
app.MapRouterOpenApi();
}
app.UseWhen(
context => !GatewayRoutes.IsSystemPath(context.Request.Path),
branch =>
{
branch.UseMiddleware<RequestLoggingMiddleware>();
branch.UseMiddleware<GlobalErrorHandlerMiddleware>();
branch.UseMiddleware<PayloadLimitsMiddleware>();
branch.UseMiddleware<EndpointResolutionMiddleware>();
branch.UseMiddleware<AuthorizationMiddleware>();
branch.UseRateLimiting();
branch.UseMiddleware<RoutingDecisionMiddleware>();
branch.UseMiddleware<RequestRoutingMiddleware>();
});
// Error page fallback (after all other middleware)
app.UseMiddleware<ErrorPageFallbackMiddleware>();
// Refresh Router endpoint cache
app.TryRefreshStellaRouterEndpoints(routerOptions);
await app.RunAsync();
static void ConfigureAuthentication(WebApplicationBuilder builder, GatewayOptions options)
{
var authOptions = options.Auth;
if (!string.IsNullOrWhiteSpace(authOptions.Authority.Issuer))
{
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configurationSection: null,
configure: resourceOptions =>
{
resourceOptions.Authority = authOptions.Authority.Issuer;
resourceOptions.RequireHttpsMetadata = authOptions.Authority.RequireHttpsMetadata;
resourceOptions.MetadataAddress = authOptions.Authority.MetadataAddress;
resourceOptions.Audiences.Clear();
foreach (var audience in authOptions.Authority.Audiences)
{
resourceOptions.Audiences.Add(audience);
}
});
// Configure the OIDC metadata HTTP client to accept self-signed certificates
// (Authority uses a dev cert in Docker)
if (!authOptions.Authority.RequireHttpsMetadata)
{
builder.Services.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
});
});
}
if (authOptions.Authority.RequiredScopes.Count > 0)
{
builder.Services.AddAuthorization(config =>
{
config.AddPolicy("gateway.default", policy =>
{
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new StellaOpsScopeRequirement(authOptions.Authority.RequiredScopes));
policy.AddAuthenticationSchemes(StellaOpsAuthenticationDefaults.AuthenticationScheme);
});
});
}
return;
}
if (authOptions.AllowAnonymous)
{
builder.Services.AddAuthentication(authConfig =>
{
authConfig.DefaultAuthenticateScheme = AllowAllAuthenticationHandler.SchemeName;
authConfig.DefaultChallengeScheme = AllowAllAuthenticationHandler.SchemeName;
}).AddScheme<AuthenticationSchemeOptions, AllowAllAuthenticationHandler>(
AllowAllAuthenticationHandler.SchemeName,
_ => { });
return;
}
throw new InvalidOperationException("Gateway authentication requires an Authority issuer or AllowAnonymous.");
}
static void ConfigureGatewayOptionsMapping(WebApplicationBuilder builder, GatewayOptions gatewayOptions)
{
builder.Services.AddOptions<RouterNodeConfig>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
options.Region = gateway.Value.Node.Region;
options.NodeId = gateway.Value.Node.NodeId;
options.Environment = gateway.Value.Node.Environment;
options.NeighborRegions = gateway.Value.Node.NeighborRegions;
});
builder.Services.AddOptions<RoutingOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var routing = gateway.Value.Routing;
options.RoutingTimeoutMs = (int)GatewayValueParser.ParseDuration(routing.DefaultTimeout, TimeSpan.FromSeconds(30)).TotalMilliseconds;
options.PreferLocalRegion = routing.PreferLocalRegion;
options.AllowDegradedInstances = routing.AllowDegradedInstances;
options.StrictVersionMatching = routing.StrictVersionMatching;
});
builder.Services.AddOptions<PayloadLimits>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var routing = gateway.Value.Routing;
options.MaxRequestBytesPerCall = GatewayValueParser.ParseSizeBytes(routing.MaxRequestBodySize, options.MaxRequestBytesPerCall);
});
builder.Services.AddOptions<HealthOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var health = gateway.Value.Health;
options.StaleThreshold = GatewayValueParser.ParseDuration(health.StaleThreshold, options.StaleThreshold);
options.DegradedThreshold = GatewayValueParser.ParseDuration(health.DegradedThreshold, options.DegradedThreshold);
options.CheckInterval = GatewayValueParser.ParseDuration(health.CheckInterval, options.CheckInterval);
});
builder.Services.AddOptions<OpenApiAggregationOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var openApi = gateway.Value.OpenApi;
options.Enabled = openApi.Enabled;
options.CacheTtlSeconds = openApi.CacheTtlSeconds;
options.Title = openApi.Title;
options.Description = openApi.Description;
options.Version = openApi.Version;
options.ServerUrl = openApi.ServerUrl;
options.TokenUrl = openApi.TokenUrl;
});
builder.Services.AddOptions<TcpTransportOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var tcp = gateway.Value.Transports.Tcp;
options.Port = tcp.Port;
options.ReceiveBufferSize = tcp.ReceiveBufferSize;
options.SendBufferSize = tcp.SendBufferSize;
options.MaxFrameSize = tcp.MaxFrameSize;
options.BindAddress = IPAddress.Parse(tcp.BindAddress);
});
builder.Services.AddOptions<TlsTransportOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var tls = gateway.Value.Transports.Tls;
options.Port = tls.Port;
options.ReceiveBufferSize = tls.ReceiveBufferSize;
options.SendBufferSize = tls.SendBufferSize;
options.MaxFrameSize = tls.MaxFrameSize;
options.BindAddress = IPAddress.Parse(tls.BindAddress);
options.ServerCertificatePath = tls.CertificatePath;
options.ServerCertificateKeyPath = tls.CertificateKeyPath;
options.ServerCertificatePassword = tls.CertificatePassword;
options.RequireClientCertificate = tls.RequireClientCertificate;
options.AllowSelfSigned = tls.AllowSelfSigned;
});
builder.Services.AddOptions<MessagingTransportOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var messaging = gateway.Value.Transports.Messaging;
options.RequestQueueTemplate = messaging.RequestQueueTemplate;
options.ResponseQueueName = messaging.ResponseQueueName;
options.ConsumerGroup = messaging.ConsumerGroup;
options.RequestTimeout = GatewayValueParser.ParseDuration(messaging.RequestTimeout, TimeSpan.FromSeconds(30));
options.LeaseDuration = GatewayValueParser.ParseDuration(messaging.LeaseDuration, TimeSpan.FromMinutes(5));
options.BatchSize = messaging.BatchSize;
options.HeartbeatInterval = GatewayValueParser.ParseDuration(messaging.HeartbeatInterval, TimeSpan.FromSeconds(10));
});
builder.Services.AddOptions<ValkeyTransportOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var messaging = gateway.Value.Transports.Messaging;
options.ConnectionString = messaging.ConnectionString;
options.Database = messaging.Database;
});
}