diff --git a/DorkNet.Server/Services/DomainConfig.cs b/DorkNet.Server/Services/DomainConfig.cs index 2b328b3..e93a01b 100644 --- a/DorkNet.Server/Services/DomainConfig.cs +++ b/DorkNet.Server/Services/DomainConfig.cs @@ -12,14 +12,22 @@ public sealed class DomainConfig { public string Apex { get; } public string Scheme { get; } + public string HostStyle { get; } + public bool UsesHyphenSubdomains => HostStyle == "hyphen"; - public DomainConfig(string apex, string scheme = "https") + public DomainConfig(string apex, string scheme = "https", string hostStyle = "dot") { Apex = apex; Scheme = NormalizeScheme(scheme); + HostStyle = NormalizeHostStyle(hostStyle); + } + + public string Sub(string prefix) + { + if (string.IsNullOrWhiteSpace(prefix)) return Apex; + return UsesHyphenSubdomains ? $"{prefix}-{Apex}" : $"{prefix}.{Apex}"; } - public string Sub(string prefix) => $"{prefix}.{Apex}"; public string Url(string prefix, string path = "") { var host = string.IsNullOrEmpty(prefix) ? Apex : Sub(prefix); @@ -28,11 +36,18 @@ public sealed class DomainConfig } public static bool MatchesSubdomain(string host, string prefix) - => host.StartsWith(prefix + ".", StringComparison.OrdinalIgnoreCase); + => host.StartsWith(prefix + ".", StringComparison.OrdinalIgnoreCase) + || host.StartsWith(prefix + "-", StringComparison.OrdinalIgnoreCase); private static string NormalizeScheme(string? value) { var scheme = string.IsNullOrWhiteSpace(value) ? "https" : value.Trim().TrimEnd(':', '/', '\\').ToLowerInvariant(); return scheme is "http" or "https" ? scheme : "https"; } + + private static string NormalizeHostStyle(string? value) + { + var style = string.IsNullOrWhiteSpace(value) ? "dot" : value.Trim().ToLowerInvariant(); + return style is "hyphen" or "dash" or "flat" ? "hyphen" : "dot"; + } } diff --git a/DorkNet.Server/Startup/MiddlewarePipelineExtensions.cs b/DorkNet.Server/Startup/MiddlewarePipelineExtensions.cs index 4313c8a..3f0fa86 100644 --- a/DorkNet.Server/Startup/MiddlewarePipelineExtensions.cs +++ b/DorkNet.Server/Startup/MiddlewarePipelineExtensions.cs @@ -54,7 +54,7 @@ public static class MiddlewarePipelineExtensions // Public-facing site at the apex (e.g. localhost + www variant). // React-router routes are client-side so spaFallback must be true. app.MountStaticHost(domainCfg.Apex, "site", spaFallback: true); - app.MountStaticHost($"www.{domainCfg.Apex}", "site", spaFallback: true); + app.MountStaticHost(domainCfg.Sub("www"), "site", spaFallback: true); // Old feed.* subdomain kept as legacy; new visitors land on the apex. app.MountStaticHost(domainCfg.Sub("feed"), "feed"); diff --git a/DorkNet.Server/Startup/ServiceCollectionExtensions.cs b/DorkNet.Server/Startup/ServiceCollectionExtensions.cs index a28743a..8e6013d 100644 --- a/DorkNet.Server/Startup/ServiceCollectionExtensions.cs +++ b/DorkNet.Server/Startup/ServiceCollectionExtensions.cs @@ -115,7 +115,23 @@ public static class ServiceCollectionExtensions ?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN") ?? "localhost"; var scheme = builder.Configuration["Domain:Scheme"] ?? "https"; - var allowedHosts = new List { apex, $"*.{apex}", "localhost", "127.0.0.1" }; + var hostStyle = + builder.Configuration["Domain:HostStyle"] + ?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN_STYLE") + ?? "dot"; + var domain = new DomainConfig(apex, scheme, hostStyle); + var allowedHosts = new List { apex, "localhost", "127.0.0.1" }; + if (domain.UsesHyphenSubdomains) + { + allowedHosts.AddRange(KnownServiceSubdomains + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(domain.Sub)); + allowedHosts.Add(domain.Sub("www")); + } + else + { + allowedHosts.Add($"*.{apex}"); + } var bootstrapApex = builder.Configuration["Domain:BootstrapApex"]; if (!string.IsNullOrWhiteSpace(bootstrapApex)) { @@ -123,16 +139,57 @@ public static class ServiceCollectionExtensions allowedHosts.Add($"*.{bootstrapApex}"); } - builder.Services.AddSingleton(new DomainConfig(apex, scheme)); + builder.Services.AddSingleton(domain); builder.Services.Configure(opt => { opt.AllowedHosts = allowedHosts.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); opt.AllowEmptyHosts = true; opt.IncludeFailureMessage = true; }); - Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{string.Join(", ", allowedHosts)}]"); + Console.WriteLine($"[domain] apex={apex}, style={domain.HostStyle}, allowedHosts=[{string.Join(", ", allowedHosts)}]"); } + private static readonly string[] KnownServiceSubdomains = + { + "admin", + "api", + "accounts", + "auth", + "bugreporting", + "cards", + "cdn", + "chat", + "clubs", + "cms", + "commerce", + "data", + "datacollection", + "discovery", + "econ", + "feed", + "gamelogs", + "geo", + "img", + "leaderboard", + "link", + "lists", + "match", + "moderation", + "notify", + "ns", + "platformnotifications", + "playersettings", + "roomcomments", + "roomieintegrations", + "rooms", + "storage", + "strings", + "strings-cdn", + "studio", + "thorn", + "videos", + }; + // ── Services ────────────────────────────────────────────────────────────── private static void AddCoreServices(IServiceCollection services) { diff --git a/DorkNet.Server/admin-ui/src/lib/types.ts b/DorkNet.Server/admin-ui/src/lib/types.ts index 0269899..ba0797f 100644 --- a/DorkNet.Server/admin-ui/src/lib/types.ts +++ b/DorkNet.Server/admin-ui/src/lib/types.ts @@ -54,11 +54,18 @@ export function adminApex(): string { const host = window.location.hostname.toLowerCase(); if (host === 'localhost' || host.endsWith('.localhost')) return 'localhost'; + if (host.endsWith('-dev.example.com')) return 'dev.example.com'; const parts = host.split('.'); return parts.length > 2 ? parts.slice(1).join('.') : host; } +function serviceHost(subdomain: string, apex: string): string { + return apex === 'dev.example.com' + ? `${subdomain}-${apex}` + : `${subdomain}.${apex}`; +} + function subdomainUrl(subdomain: string, path: string | null | undefined, query?: string, preserveSlashes = false): string | null { if (!path) return null; const apex = adminApex(); @@ -67,7 +74,7 @@ function subdomainUrl(subdomain: string, path: string | null | undefined, query? const encodedPath = preserveSlashes ? path.split('/').map(encodeURIComponent).join('/') : encodeURIComponent(path); - return `${scheme}://${subdomain}.${apex}/${encodedPath}${suffix}`; + return `${scheme}://${serviceHost(subdomain, apex)}/${encodedPath}${suffix}`; } export interface Room {