119 lines
5.8 KiB
C#
119 lines
5.8 KiB
C#
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<TenantResolutionOptions>()
|
|
.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<ForwardedHeadersOptions>(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<TenantResolutionOptions>() ?? new TenantResolutionOptions();
|
|
foreach (var address in resolution.TrustedProxyAddresses)
|
|
if (IPAddress.TryParse(address, out var proxy))
|
|
options.KnownProxies.Add(proxy);
|
|
});
|
|
services.AddOptions<DomainLifecycleOptions>()
|
|
.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<DevelopmentTenantDomainLifecycleHostedService>();
|
|
services.AddOptions<TenantProvisioningOptions>()
|
|
.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<TenantProvisioningStartupValidator>();
|
|
|
|
services.AddOptions<CorsOptions>()
|
|
.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<CorsOptions>() ?? 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);
|
|
}
|
|
}
|