541 lines
21 KiB
C#
541 lines
21 KiB
C#
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
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.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 System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Runtime.Loader;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
|
|
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<StellaOps.Router.Gateway.Authorization.IEffectiveClaimsStore>(sp =>
|
|
sp.GetRequiredService<IEffectiveClaimsStore>());
|
|
|
|
var authorityClaimsUrl = ResolveAuthorityClaimsUrl(bootstrapOptions.Auth.Authority);
|
|
StellaOps.Router.Gateway.Authorization.AuthorizationServiceCollectionExtensions.AddAuthorityIntegration(
|
|
builder.Services,
|
|
options =>
|
|
{
|
|
options.Enabled = !string.IsNullOrWhiteSpace(authorityClaimsUrl);
|
|
options.AuthorityUrl = authorityClaimsUrl ?? string.Empty;
|
|
options.RefreshInterval = TimeSpan.FromSeconds(30);
|
|
options.WaitForAuthorityOnStartup = false;
|
|
options.StartupTimeout = TimeSpan.FromSeconds(10);
|
|
options.UseAuthorityPushNotifications = false;
|
|
});
|
|
|
|
builder.Services.Replace(ServiceDescriptor.Singleton<StellaOps.Router.Gateway.Authorization.IEffectiveClaimsStore>(
|
|
sp => sp.GetRequiredService<IEffectiveClaimsStore>()));
|
|
|
|
builder.Services.AddSingleton<GatewayServiceStatus>();
|
|
builder.Services.AddSingleton<GatewayMetrics>();
|
|
|
|
// Load router transport plugins
|
|
var transportPluginLoader = new RouterTransportPluginLoader(
|
|
NullLoggerFactory.Instance.CreateLogger<RouterTransportPluginLoader>());
|
|
|
|
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()
|
|
.Where(assembly =>
|
|
assembly.GetName().Name?.StartsWith("StellaOps.Router.Transport.", StringComparison.OrdinalIgnoreCase) == true
|
|
&& AssemblyLoadContext.GetLoadContext(assembly) == AssemblyLoadContext.Default))
|
|
{
|
|
transportPluginLoader.LoadFromAssembly(assembly);
|
|
}
|
|
|
|
var pluginsPath = builder.Configuration["Gateway:TransportPlugins:Directory"];
|
|
if (string.IsNullOrWhiteSpace(pluginsPath))
|
|
{
|
|
pluginsPath = Path.Combine(AppContext.BaseDirectory, "plugins", "router", "transports");
|
|
}
|
|
|
|
var transportSearchPattern = builder.Configuration["Gateway:TransportPlugins:SearchPattern"];
|
|
if (string.IsNullOrWhiteSpace(transportSearchPattern))
|
|
{
|
|
transportSearchPattern = "StellaOps.Router.Transport.*.dll";
|
|
}
|
|
|
|
transportPluginLoader.LoadFromDirectory(pluginsPath, transportSearchPattern);
|
|
|
|
RegisterGatewayTransportIfEnabled("tcp", bootstrapOptions.Transports.Tcp.Enabled, "Gateway:Transports:Tcp");
|
|
RegisterGatewayTransportIfEnabled("tls", bootstrapOptions.Transports.Tls.Enabled, "Gateway:Transports:Tls");
|
|
RegisterGatewayTransportIfEnabled("messaging", bootstrapOptions.Transports.Messaging.Enabled, "Gateway:Transports:Messaging");
|
|
|
|
builder.Services.AddSingleton<GatewayTransportClient>();
|
|
builder.Services.AddSingleton<ITransportClient>(sp => sp.GetRequiredService<GatewayTransportClient>());
|
|
|
|
builder.Services.AddSingleton(new GatewayRouteCatalog(bootstrapOptions.Routes));
|
|
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,
|
|
EmitIdentityEnvelope = bootstrapOptions.Auth.EmitIdentityEnvelope,
|
|
IdentityEnvelopeSigningKey = bootstrapOptions.Auth.IdentityEnvelopeSigningKey,
|
|
IdentityEnvelopeIssuer = bootstrapOptions.Auth.IdentityEnvelopeIssuer,
|
|
IdentityEnvelopeTtl = TimeSpan.FromSeconds(Math.Max(1, bootstrapOptions.Auth.IdentityEnvelopeTtlSeconds)),
|
|
JwtPassthroughPrefixes = bootstrapOptions.Routes
|
|
.Where(r => r.PreserveAuthHeaders)
|
|
.Select(r => r.Path)
|
|
.ToList(),
|
|
ApprovedAuthPassthroughPrefixes = [.. bootstrapOptions.Auth.ApprovedAuthPassthroughPrefixes],
|
|
EnableTenantOverride = bootstrapOptions.Auth.EnableTenantOverride
|
|
});
|
|
|
|
// 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 routerEnabled = builder.Services.AddRouterMicroservice(
|
|
builder.Configuration,
|
|
serviceName: "gateway",
|
|
version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0",
|
|
routerOptionsSection: "Router");
|
|
|
|
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
|
|
|
if (ShouldApplyStellaOpsLocalBinding())
|
|
{
|
|
builder.TryAddStellaOpsLocalBinding("router");
|
|
}
|
|
else
|
|
{
|
|
ConfigureContainerFrontdoorBindings(builder);
|
|
}
|
|
var app = builder.Build();
|
|
app.LogStellaOpsLocalHostname("router");
|
|
|
|
// Force browser traffic onto HTTPS so auth (PKCE/DPoP/WebCrypto) always runs in a secure context.
|
|
app.Use(async (context, next) =>
|
|
{
|
|
var isWebSocketUpgrade =
|
|
context.WebSockets.IsWebSocketRequest ||
|
|
string.Equals(context.Request.Headers.Upgrade, "websocket", StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (!context.Request.IsHttps &&
|
|
context.Request.Host.HasValue &&
|
|
!GatewayRoutes.IsSystemPath(context.Request.Path) &&
|
|
!isWebSocketUpgrade)
|
|
{
|
|
var host = context.Request.Host.Host;
|
|
var redirect = $"https://{host}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}";
|
|
context.Response.Redirect(redirect, permanent: false);
|
|
return;
|
|
}
|
|
|
|
await next().ConfigureAwait(false);
|
|
});
|
|
|
|
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(routerEnabled);
|
|
|
|
// 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(routerEnabled);
|
|
|
|
await app.RunAsync();
|
|
|
|
void RegisterGatewayTransportIfEnabled(string transportName, bool enabled, string configurationSection)
|
|
{
|
|
if (!enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var plugin = transportPluginLoader.GetPlugin(transportName);
|
|
if (plugin is null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Gateway transport plugin '{transportName}' is not available. " +
|
|
$"Provide a plugin assembly in '{pluginsPath}' or add the transport plugin dependency.");
|
|
}
|
|
|
|
plugin.Register(new RouterTransportRegistrationContext(
|
|
builder.Services,
|
|
builder.Configuration,
|
|
RouterTransportMode.Server)
|
|
{
|
|
ConfigurationSection = configurationSection
|
|
});
|
|
}
|
|
|
|
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)
|
|
{
|
|
// Explicitly configure the named metadata client used by StellaOpsAuthorityConfigurationManager.
|
|
// ConfigureHttpClientDefaults may not apply to named clients in all .NET versions.
|
|
builder.Services.AddHttpClient("StellaOps.Auth.ServerIntegration.Metadata")
|
|
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
|
{
|
|
ServerCertificateCustomValidationCallback =
|
|
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
|
});
|
|
|
|
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.GlobalTimeoutCapMs = (int)GatewayValueParser.ParseDuration(routing.GlobalTimeoutCap, TimeSpan.FromSeconds(120)).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);
|
|
GatewayHealthThresholdPolicy.ApplyMinimums(options, gateway.Value.Transports.Messaging);
|
|
});
|
|
|
|
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;
|
|
});
|
|
|
|
}
|
|
|
|
static bool ShouldApplyStellaOpsLocalBinding()
|
|
{
|
|
var runningInContainer = string.Equals(
|
|
Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"),
|
|
"true",
|
|
StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (!runningInContainer)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Compose-published container runs already define the frontdoor port contract.
|
|
// Respect explicit container port settings instead of replacing them with 80/443.
|
|
var explicitUrls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS");
|
|
var explicitHttpPorts = Environment.GetEnvironmentVariable("ASPNETCORE_HTTP_PORTS");
|
|
var explicitHttpsPorts = Environment.GetEnvironmentVariable("ASPNETCORE_HTTPS_PORTS");
|
|
|
|
return string.IsNullOrWhiteSpace(explicitUrls)
|
|
&& string.IsNullOrWhiteSpace(explicitHttpPorts)
|
|
&& string.IsNullOrWhiteSpace(explicitHttpsPorts);
|
|
}
|
|
|
|
static void ConfigureContainerFrontdoorBindings(WebApplicationBuilder builder)
|
|
{
|
|
var currentUrls = ContainerFrontdoorBindingResolver.ResolveConfiguredUrls(
|
|
builder.WebHost.GetSetting(WebHostDefaults.ServerUrlsKey),
|
|
Environment.GetEnvironmentVariable("ASPNETCORE_URLS"),
|
|
Environment.GetEnvironmentVariable("ASPNETCORE_HTTP_PORTS"),
|
|
Environment.GetEnvironmentVariable("ASPNETCORE_HTTPS_PORTS"));
|
|
|
|
builder.WebHost.ConfigureKestrel((context, kestrel) =>
|
|
{
|
|
var defaultCert = LoadDefaultCertificate(context.Configuration);
|
|
|
|
foreach (var uri in currentUrls)
|
|
{
|
|
var address = ResolveListenAddress(uri.Host);
|
|
if (string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
kestrel.Listen(address, uri.Port, listenOptions =>
|
|
{
|
|
if (defaultCert is not null)
|
|
{
|
|
listenOptions.UseHttps(defaultCert);
|
|
}
|
|
else
|
|
{
|
|
listenOptions.UseHttps();
|
|
}
|
|
});
|
|
continue;
|
|
}
|
|
|
|
kestrel.Listen(address, uri.Port);
|
|
}
|
|
|
|
if (defaultCert is not null && IsPortAvailable(443, IPAddress.Any))
|
|
{
|
|
kestrel.ListenAnyIP(443, listenOptions => listenOptions.UseHttps(defaultCert));
|
|
}
|
|
});
|
|
}
|
|
|
|
static X509Certificate2? LoadDefaultCertificate(IConfiguration configuration)
|
|
{
|
|
var certPath = configuration["Kestrel:Certificates:Default:Path"];
|
|
var certPass = configuration["Kestrel:Certificates:Default:Password"];
|
|
if (string.IsNullOrWhiteSpace(certPath) || !File.Exists(certPath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return X509CertificateLoader.LoadPkcs12FromFile(certPath, certPass);
|
|
}
|
|
|
|
static IPAddress ResolveListenAddress(string host)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(host) ||
|
|
string.Equals(host, "*", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(host, "+", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(host, "0.0.0.0", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return IPAddress.Any;
|
|
}
|
|
|
|
if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return IPAddress.Loopback;
|
|
}
|
|
|
|
return IPAddress.Parse(host);
|
|
}
|
|
|
|
static bool IsPortAvailable(int port, IPAddress address)
|
|
{
|
|
try
|
|
{
|
|
using var listener = new TcpListener(address, port);
|
|
listener.Start();
|
|
listener.Stop();
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static string? ResolveAuthorityClaimsUrl(GatewayAuthorityOptions authorityOptions)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(authorityOptions.ClaimsOverridesUrl))
|
|
{
|
|
return authorityOptions.ClaimsOverridesUrl.TrimEnd('/');
|
|
}
|
|
|
|
var candidate = authorityOptions.Issuer;
|
|
if (string.IsNullOrWhiteSpace(candidate))
|
|
{
|
|
candidate = authorityOptions.MetadataAddress;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(candidate))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!Uri.TryCreate(candidate, UriKind.Absolute, out var uri))
|
|
{
|
|
return candidate.TrimEnd('/');
|
|
}
|
|
|
|
// Authority runs HTTP on the internal compose network by default.
|
|
var builder = new UriBuilder(uri)
|
|
{
|
|
Path = string.Empty,
|
|
Query = string.Empty,
|
|
Fragment = string.Empty
|
|
};
|
|
|
|
if (string.Equals(builder.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
builder.Scheme = Uri.UriSchemeHttp;
|
|
builder.Port = builder.Port == 443 ? 80 : builder.Port;
|
|
}
|
|
|
|
return builder.Uri.GetLeftPart(UriPartial.Authority).TrimEnd('/');
|
|
}
|