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.SectionName, (opts, _) => GatewayOptionsValidator.Validate(opts)); builder.Services.AddOptions() .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(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Load router transport plugins var transportPluginLoader = new RouterTransportPluginLoader( NullLoggerFactory.Instance.CreateLogger()); // 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(builder.Configuration, "Gateway:Transports:Messaging"); builder.Services.AddMessagingTransportServer(); } builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // 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>( 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(); 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(); app.UseStellaOpsCors(); app.UseAuthentication(); app.UseMiddleware(); // IdentityHeaderPolicyMiddleware replaces TenantMiddleware and ClaimsPropagationMiddleware // It strips reserved identity headers and overwrites them from validated claims (security fix) app.UseMiddleware(); app.UseMiddleware(); app.TryUseStellaRouter(routerOptions); // WebSocket support (before route dispatch) app.UseWebSockets(); // Route dispatch for configured routes (static files, reverse proxy, websocket) app.UseMiddleware(); if (bootstrapOptions.OpenApi.Enabled) { app.MapRouterOpenApi(); } app.UseWhen( context => !GatewayRoutes.IsSystemPath(context.Request.Path), branch => { branch.UseMiddleware(); branch.UseMiddleware(); branch.UseMiddleware(); branch.UseMiddleware(); branch.UseMiddleware(); branch.UseRateLimiting(); branch.UseMiddleware(); branch.UseMiddleware(); }); // Error page fallback (after all other middleware) app.UseMiddleware(); // 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( AllowAllAuthenticationHandler.SchemeName, _ => { }); return; } throw new InvalidOperationException("Gateway authentication requires an Authority issuer or AllowAnonymous."); } static void ConfigureGatewayOptionsMapping(WebApplicationBuilder builder, GatewayOptions gatewayOptions) { builder.Services.AddOptions() .Configure>((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() .Configure>((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() .Configure>((options, gateway) => { var routing = gateway.Value.Routing; options.MaxRequestBytesPerCall = GatewayValueParser.ParseSizeBytes(routing.MaxRequestBodySize, options.MaxRequestBytesPerCall); }); builder.Services.AddOptions() .Configure>((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() .Configure>((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() .Configure>((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() .Configure>((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() .Configure>((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() .Configure>((options, gateway) => { var messaging = gateway.Value.Transports.Messaging; options.ConnectionString = messaging.ConnectionString; options.Database = messaging.Database; }); }