using System.Net; using Microsoft.AspNetCore.HttpOverrides; using Tiku.Api.Background; using Tiku.Api.Options; using Tiku.Application.PlatformAdmin; using Tiku.Application.Tenancy; namespace Tiku.Api.Configuration; internal static class NetworkConfigurationExtensions { internal static IServiceCollection AddNetworkConfiguration( this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment) { services.AddOptions() .Bind(configuration.GetSection(TenantResolutionOptions.SectionName)) .Validate( options => OptionsValidation.BeValidTenantResolutionOptions(options, configuration, environment.IsProduction()), "Production requires formal platform hosts, non-wildcard AllowedHosts, and trusted proxy addresses.") .ValidateOnStart(); services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto; options.ForwardLimit = 1; options.KnownProxies.Clear(); options.KnownIPNetworks.Clear(); var resolution = configuration .GetSection(TenantResolutionOptions.SectionName) .Get() ?? new TenantResolutionOptions(); foreach (var address in resolution.TrustedProxyAddresses) if (IPAddress.TryParse(address, out var proxy)) options.KnownProxies.Add(proxy); }); services.AddOptions() .Bind(configuration.GetSection("TenantDomains")) .Validate(options => environment.IsDevelopment() || !options.EnableDevelopmentLocalhostBypass, "The .localhost domain lifecycle bypass can only be enabled in Development.") .Validate(options => options.PollSeconds is >= 1 and <= 3600 && options.MaxIdlePollSeconds is >= 1 and <= 3600 && options.MaxIdlePollSeconds >= options.PollSeconds, "Tenant domain polling intervals are invalid.") .ValidateOnStart(); if (environment.IsDevelopment()) services.AddHostedService(); services.AddOptions() .Bind(configuration.GetSection(TenantProvisioningOptions.SectionName)) .Validate(options => !string.IsNullOrWhiteSpace(options.DefaultBaseOfferingCode) && options.DefaultTrialDays is >= 1 and <= 365 && options.OwnerActivationMinutes is >= 5 and <= 1440 && options.OwnerActivationUrlTemplate.Contains("{host}", StringComparison.Ordinal) && Uri.TryCreate( options.OwnerActivationUrlTemplate.Replace("{host}", "tenant.example.com", StringComparison.Ordinal), UriKind.Absolute, out var activationOrigin) && activationOrigin.Scheme == Uri.UriSchemeHttps && ValidateDevelopmentActivationTemplate(options, environment.IsDevelopment()), "Tenant provisioning requires a default offering code, valid trial/activation durations, an HTTPS owner activation URL template, and permits an HTTP template only for Development .localhost sites.") .ValidateOnStart(); if (environment.IsProduction()) services.AddHostedService(); services.AddOptions() .Bind(configuration.GetSection(CorsOptions.SectionName)) .ValidateDataAnnotations() .Validate( OptionsValidation.BeValidCorsOptions, "CORS origins must be absolute HTTP/HTTPS origins, and credentials require explicit origins.") .ValidateOnStart(); var corsOptions = configuration .GetSection(CorsOptions.SectionName) .Get() ?? new CorsOptions(); services.AddCors(options => { options.AddPolicy(CorsOptions.PolicyName, policy => { var origins = corsOptions.AllowedOrigins .Where(origin => !string.IsNullOrWhiteSpace(origin)) .Select(origin => origin.Trim().TrimEnd('/')) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); if (origins.Length > 0) policy.WithOrigins(origins); policy .WithHeaders(corsOptions.AllowedHeaders) .WithMethods(corsOptions.AllowedMethods); if (corsOptions.AllowCredentials) policy.AllowCredentials(); }); }); return services; } private static bool ValidateDevelopmentActivationTemplate( TenantProvisioningOptions options, bool isDevelopment) { if (string.IsNullOrWhiteSpace(options.DevelopmentLocalhostOwnerActivationUrlTemplate)) return true; return isDevelopment && options.DevelopmentLocalhostOwnerActivationUrlTemplate.Contains("{host}", StringComparison.Ordinal) && Uri.TryCreate( options.DevelopmentLocalhostOwnerActivationUrlTemplate.Replace( "{host}", "tenant.localhost", StringComparison.Ordinal), UriKind.Absolute, out var developmentOrigin) && (developmentOrigin.Scheme == Uri.UriSchemeHttp || developmentOrigin.Scheme == Uri.UriSchemeHttps); } }