372 lines
15 KiB
C#
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;
|
|
});
|
|
}
|