From 46e2bb15e4e76028ece7cba512df3d5b6e62e9f7 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Thu, 28 May 2026 01:39:54 +0300 Subject: [PATCH] Single-origin URL rewriting; configurable SQLite path; default InjectAuthValues=false - ClientMod: add SingleOriginBaseUrl config + URI rewrite path so the client can talk to one https:///__dn//... base instead of demanding wildcard subdomains. ServerHost rewrite still the fallback when SingleOriginBaseUrl is empty. - ClientMod config: InjectAuthValues now defaults to false. The server's PhotonCustomAuthController already fail-opens (Photon:RequireCustomAuth=false), so anonymous bootstrap is the cleaner default and avoids Photon's CustomAuthFailed loop when a WAF returns 403 to Photon's probe. - Server: SQLite path now reads Database:SqlitePath config (Database__SqlitePath env). Falls back to /data/dorknet.db so existing Docker / standalone deploys are unchanged. Lets the Windows launcher put the db under %APPDATA%\DorkNet. - Server: DomainConfig + ConfigService updates for the single-origin apex. - .gitignore: exclude local-releases.zip. --- .gitignore | 1 + DorkNet.ClientMod/Mod.cs | 41 ++++++++++++- .../dorknet-clientmod.json.template | 3 +- DorkNet.Server/Program.cs | 57 ++++++++++++++++++- DorkNet.Server/Services/ConfigService.cs | 10 +++- DorkNet.Server/Services/DomainConfig.cs | 7 ++- 6 files changed, 111 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 147ac2f..b845db2 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ DorkNet.Server/site/node_modules/ DorkNet.Server/site/dist/ DorkNet.Server/site/.vite/ DorkNet.Server/site/tsconfig.tsbuildinfo +local-releases.zip diff --git a/DorkNet.ClientMod/Mod.cs b/DorkNet.ClientMod/Mod.cs index 5479861..66d0755 100644 --- a/DorkNet.ClientMod/Mod.cs +++ b/DorkNet.ClientMod/Mod.cs @@ -55,6 +55,7 @@ public class Mod : MelonMod public static class Cfg { public static string ServerHost = "localhost"; + public static string SingleOriginBaseUrl = ""; public static string PhotonAppId = ""; public static string PhotonVoiceAppId = ""; // Forced PhotonServerSettings.PreferredRegion (with HostType @@ -281,12 +282,13 @@ public class Mod : MelonMod var doc = JsonDocument.Parse(File.ReadAllText(path)); var r = doc.RootElement; if (r.TryGetProperty("ServerHost", out var v)) Cfg.ServerHost = v.GetString() ?? Cfg.ServerHost; + if (r.TryGetProperty("SingleOriginBaseUrl", out v)) Cfg.SingleOriginBaseUrl = (v.GetString() ?? "").TrimEnd('/'); if (r.TryGetProperty("PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId; if (r.TryGetProperty("PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId; if (r.TryGetProperty("PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion; if (r.TryGetProperty("InjectAuthValues", out v)) Cfg.InjectAuthValues = v.GetBoolean(); if (r.TryGetProperty("EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean(); - Log.Msg($"[config] loaded: ServerHost={Cfg.ServerHost}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "" : "")}, " + + Log.Msg($"[config] loaded: ServerHost={Cfg.ServerHost}, SingleOrigin={(string.IsNullOrEmpty(Cfg.SingleOriginBaseUrl) ? "" : Cfg.SingleOriginBaseUrl)}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "" : "")}, " + $"InjectAuthValues={Cfg.InjectAuthValues}, EnableTlsTrustBypass={Cfg.EnableTlsTrustBypass}"); } catch (Exception ex) @@ -554,9 +556,22 @@ internal static class UriPatches private static void RewriteString(ref string uriString, string label) { if (string.IsNullOrEmpty(uriString)) return; + if (uriString.IndexOf(".rec.net", StringComparison.OrdinalIgnoreCase) < 0) return; + + if (!string.IsNullOrEmpty(Mod.Cfg.SingleOriginBaseUrl) && + Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) + { + var rewrittenUri = RewriteUri(uri); + if (rewrittenUri is null) return; + var rewrittenText = rewrittenUri.ToString(); + if (rewrittenText == uriString) return; + Mod.Log.Msg($"[{label}] {uriString} → {rewrittenText}"); + uriString = rewrittenText; + return; + } + var host = Mod.Cfg.ServerHost; if (string.IsNullOrEmpty(host)) return; - if (uriString.IndexOf(".rec.net", StringComparison.OrdinalIgnoreCase) < 0) return; var rewritten = uriString.Replace(".rec.net", "." + host); if (rewritten == uriString) return; Mod.Log.Msg($"[{label}] {uriString} → {rewritten}"); @@ -572,11 +587,33 @@ internal static class UriPatches var service = uri.Host[..^".rec.net".Length]; if (string.IsNullOrWhiteSpace(service)) return null; + if (!string.IsNullOrEmpty(Mod.Cfg.SingleOriginBaseUrl) && + Uri.TryCreate(Mod.Cfg.SingleOriginBaseUrl, UriKind.Absolute, out var baseUri)) + { + var path = CombinePath(baseUri.AbsolutePath, "__dn/" + service, uri.AbsolutePath); + return new UriBuilder(baseUri) + { + Path = path, + Query = uri.Query.TrimStart('?'), + }.Uri; + } + return new UriBuilder(uri) { Host = service + "." + host, }.Uri; } + + private static string CombinePath(params string[] parts) + { + var cleaned = new List(); + foreach (var part in parts) + { + var p = (part ?? "").Trim('/'); + if (p.Length > 0) cleaned.Add(p); + } + return "/" + string.Join("/", cleaned); + } } internal static class PhotonPatches diff --git a/DorkNet.ClientMod/dorknet-clientmod.json.template b/DorkNet.ClientMod/dorknet-clientmod.json.template index c3bc687..71390dc 100644 --- a/DorkNet.ClientMod/dorknet-clientmod.json.template +++ b/DorkNet.ClientMod/dorknet-clientmod.json.template @@ -1,8 +1,9 @@ { "ServerHost": "{HOST}", + "SingleOriginBaseUrl": "{SINGLE_ORIGIN_BASE_URL}", "PhotonAppId": "{PHOTON_APPID}", "PhotonVoiceAppId": "{PHOTON_VOICE_APPID}", "PhotonCloudRegion": "{PHOTON_REGION}", - "InjectAuthValues": true, + "InjectAuthValues": false, "EnableTlsTrustBypass": true } diff --git a/DorkNet.Server/Program.cs b/DorkNet.Server/Program.cs index 29e3190..5c518cd 100644 --- a/DorkNet.Server/Program.cs +++ b/DorkNet.Server/Program.cs @@ -65,7 +65,15 @@ builder.Services.AddDbContext(opt => } else { - var dbPath = Path.Combine(AppContext.BaseDirectory, "data", "dorknet.db"); + // Honor an explicit path the host can set via env var + // (Database__SqlitePath) or appsettings (Database:SqlitePath). + // The Windows launcher sets this to %APPDATA%\DorkNet\dorknet.db + // so server installs don't accumulate state under \data\. + // Existing Docker / standalone deploys that mounted \data\ + // keep working because that's still the fallback. + var dbPath = builder.Configuration["Database:SqlitePath"]; + if (string.IsNullOrWhiteSpace(dbPath)) + dbPath = Path.Combine(AppContext.BaseDirectory, "data", "dorknet.db"); Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); opt.UseSqlite($"Data Source={dbPath}", lite => lite.MigrationsAssembly("DorkNet.Server")); } @@ -94,14 +102,17 @@ var domainScheme = var domainPort = builder.Configuration["Domain:Port"] ?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN_PORT"); -builder.Services.AddSingleton(new DomainConfig(apex, domainScheme, domainPort)); +var singleOriginBaseUrl = + builder.Configuration["Domain:SingleOriginBaseUrl"] + ?? Environment.GetEnvironmentVariable("DORKNET_SINGLE_ORIGIN_BASE_URL"); +builder.Services.AddSingleton(new DomainConfig(apex, domainScheme, domainPort, singleOriginBaseUrl)); builder.Services.Configure(opt => { opt.AllowedHosts = new[] { apex, $"*.{apex}", "localhost", "127.0.0.1" }; opt.AllowEmptyHosts = true; opt.IncludeFailureMessage = true; }); -Console.WriteLine($"[domain] apex={apex}, scheme={domainScheme}, port={domainPort}, allowedHosts=[{apex}, *.{apex}, localhost]"); +Console.WriteLine($"[domain] apex={apex}, scheme={domainScheme}, port={domainPort}, singleOrigin={singleOriginBaseUrl}, allowedHosts=[{apex}, *.{apex}, localhost]"); // ── Services ────────────────────────────────────────────────────────────────── builder.Services.AddScoped(); @@ -317,6 +328,30 @@ var app = builder.Build(); // hardcoded rec.net/localhost literals. var domainCfg = app.Services.GetRequiredService(); +// Easy Launcher single-origin mode. localtunnel gives us one public +// hostname, so the launcher name-server emits URLs like +// https://abc.loca.lt/__dn/api/...; this middleware maps those path +// prefixes back into the same internal host layout used by advanced +// wildcard deployments. If Domain:SingleOriginBaseUrl is unset this is +// inert, preserving the normal subdomain behavior. +app.Use(async (ctx, next) => +{ + if (domainCfg.SingleOriginEnabled && + TryMapSingleOriginPath(ctx.Request.Path, out var service, out var rest)) + { + ctx.Request.Host = new HostString(service == "www" ? domainCfg.Apex : domainCfg.Sub(service)); + ctx.Request.Path = string.IsNullOrEmpty(rest) ? "/" : rest; + } + + await next(); +}); + +// Keep endpoint selection after the single-origin path rewrite. Without an +// explicit routing point here, WebApplication can auto-insert routing before +// the rewrite middleware, causing /__dn/api/... requests to stay route misses +// even though the trace logs show the rewritten /api/... path. +app.UseRouting(); + // Request/response tracing — emits one structured log per non-health request, // with the response body included for any 4xx/5xx so we can see exactly what // the game is asking for. This runs early enough to catch route misses, @@ -1071,6 +1106,22 @@ app.MapHub("/hub/v1", opts => app.Run(); +static bool TryMapSingleOriginPath(PathString path, out string service, out PathString rest) +{ + service = ""; + rest = PathString.Empty; + if (!path.StartsWithSegments("/__dn", out var tail)) return false; + + var value = tail.Value ?? ""; + if (value.Length <= 1) return false; + var slash = value.IndexOf('/', 1); + service = slash < 0 ? value[1..] : value[1..slash]; + if (string.IsNullOrWhiteSpace(service)) return false; + + rest = slash < 0 ? "/" : value[slash..]; + return true; +} + static bool IsHealthProbe(HttpRequest request) { return request.Path.Equals("/healthz", StringComparison.OrdinalIgnoreCase); diff --git a/DorkNet.Server/Services/ConfigService.cs b/DorkNet.Server/Services/ConfigService.cs index b6d7783..858d386 100644 --- a/DorkNet.Server/Services/ConfigService.cs +++ b/DorkNet.Server/Services/ConfigService.cs @@ -23,7 +23,9 @@ public class ConfigService(IConfiguration config, DomainConfig domain) return new RecRoomConfig { MessageOfTheDay = config["Server:MOTD"] ?? "Welcome to the private server!", - CdnBaseUri = domain.SubUrl("cdn"), + CdnBaseUri = domain.SingleOriginEnabled + ? $"{domain.SingleOriginBaseUrl}/__dn/cdn" + : domain.SubUrl("cdn"), PhotonConfig = photon, ServiceUrls = BuildServiceUrlMap(domain), ConfigTable = new Dictionary @@ -110,6 +112,12 @@ public class ConfigService(IConfiguration config, DomainConfig domain) var map = new Dictionary(ServiceSubdomains.Length); foreach (var (service, sub) in ServiceSubdomains) { + if (domain.SingleOriginEnabled) + { + var prefix = string.IsNullOrEmpty(sub) ? "www" : sub; + map[service] = $"{domain.SingleOriginBaseUrl}/__dn/{prefix}"; + continue; + } var host = string.IsNullOrEmpty(sub) ? domain.Apex : domain.Sub(sub); map[service] = domain.Url(host); } diff --git a/DorkNet.Server/Services/DomainConfig.cs b/DorkNet.Server/Services/DomainConfig.cs index 4d6332b..1bed6e8 100644 --- a/DorkNet.Server/Services/DomainConfig.cs +++ b/DorkNet.Server/Services/DomainConfig.cs @@ -13,12 +13,17 @@ public sealed class DomainConfig public string Apex { get; } public string Scheme { get; } public string? Port { get; } + public string? SingleOriginBaseUrl { get; } + public bool SingleOriginEnabled => !string.IsNullOrWhiteSpace(SingleOriginBaseUrl); - public DomainConfig(string apex, string scheme = "https", string? port = null) + public DomainConfig(string apex, string scheme = "https", string? port = null, string? singleOriginBaseUrl = null) { Apex = apex; Scheme = string.IsNullOrWhiteSpace(scheme) ? "https" : scheme.TrimEnd(':', '/', '\\'); Port = string.IsNullOrWhiteSpace(port) ? null : port.Trim().TrimStart(':'); + SingleOriginBaseUrl = string.IsNullOrWhiteSpace(singleOriginBaseUrl) + ? null + : singleOriginBaseUrl.Trim().TrimEnd('/'); } public string Sub(string prefix) => $"{prefix}.{Apex}";