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://<apex>/__dn/<service>/... 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 <bin>/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.
This commit is contained in:
Alexa
2026-05-28 01:39:54 +03:00
parent 676dcd09a7
commit 46e2bb15e4
6 changed files with 111 additions and 8 deletions
+1
View File
@@ -64,3 +64,4 @@ DorkNet.Server/site/node_modules/
DorkNet.Server/site/dist/ DorkNet.Server/site/dist/
DorkNet.Server/site/.vite/ DorkNet.Server/site/.vite/
DorkNet.Server/site/tsconfig.tsbuildinfo DorkNet.Server/site/tsconfig.tsbuildinfo
local-releases.zip
+39 -2
View File
@@ -55,6 +55,7 @@ public class Mod : MelonMod
public static class Cfg public static class Cfg
{ {
public static string ServerHost = "localhost"; public static string ServerHost = "localhost";
public static string SingleOriginBaseUrl = "";
public static string PhotonAppId = ""; public static string PhotonAppId = "";
public static string PhotonVoiceAppId = ""; public static string PhotonVoiceAppId = "";
// Forced PhotonServerSettings.PreferredRegion (with HostType // Forced PhotonServerSettings.PreferredRegion (with HostType
@@ -281,12 +282,13 @@ public class Mod : MelonMod
var doc = JsonDocument.Parse(File.ReadAllText(path)); var doc = JsonDocument.Parse(File.ReadAllText(path));
var r = doc.RootElement; var r = doc.RootElement;
if (r.TryGetProperty("ServerHost", out var v)) Cfg.ServerHost = v.GetString() ?? Cfg.ServerHost; 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("PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
if (r.TryGetProperty("PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId; 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("PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
if (r.TryGetProperty("InjectAuthValues", out v)) Cfg.InjectAuthValues = v.GetBoolean(); if (r.TryGetProperty("InjectAuthValues", out v)) Cfg.InjectAuthValues = v.GetBoolean();
if (r.TryGetProperty("EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean(); if (r.TryGetProperty("EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
Log.Msg($"[config] loaded: ServerHost={Cfg.ServerHost}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "<unset>" : "<set>")}, " + Log.Msg($"[config] loaded: ServerHost={Cfg.ServerHost}, SingleOrigin={(string.IsNullOrEmpty(Cfg.SingleOriginBaseUrl) ? "<off>" : Cfg.SingleOriginBaseUrl)}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "<unset>" : "<set>")}, " +
$"InjectAuthValues={Cfg.InjectAuthValues}, EnableTlsTrustBypass={Cfg.EnableTlsTrustBypass}"); $"InjectAuthValues={Cfg.InjectAuthValues}, EnableTlsTrustBypass={Cfg.EnableTlsTrustBypass}");
} }
catch (Exception ex) catch (Exception ex)
@@ -554,9 +556,22 @@ internal static class UriPatches
private static void RewriteString(ref string uriString, string label) private static void RewriteString(ref string uriString, string label)
{ {
if (string.IsNullOrEmpty(uriString)) return; 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; var host = Mod.Cfg.ServerHost;
if (string.IsNullOrEmpty(host)) return; if (string.IsNullOrEmpty(host)) return;
if (uriString.IndexOf(".rec.net", StringComparison.OrdinalIgnoreCase) < 0) return;
var rewritten = uriString.Replace(".rec.net", "." + host); var rewritten = uriString.Replace(".rec.net", "." + host);
if (rewritten == uriString) return; if (rewritten == uriString) return;
Mod.Log.Msg($"[{label}] {uriString} → {rewritten}"); Mod.Log.Msg($"[{label}] {uriString} → {rewritten}");
@@ -572,11 +587,33 @@ internal static class UriPatches
var service = uri.Host[..^".rec.net".Length]; var service = uri.Host[..^".rec.net".Length];
if (string.IsNullOrWhiteSpace(service)) return null; 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) return new UriBuilder(uri)
{ {
Host = service + "." + host, Host = service + "." + host,
}.Uri; }.Uri;
} }
private static string CombinePath(params string[] parts)
{
var cleaned = new List<string>();
foreach (var part in parts)
{
var p = (part ?? "").Trim('/');
if (p.Length > 0) cleaned.Add(p);
}
return "/" + string.Join("/", cleaned);
}
} }
internal static class PhotonPatches internal static class PhotonPatches
@@ -1,8 +1,9 @@
{ {
"ServerHost": "{HOST}", "ServerHost": "{HOST}",
"SingleOriginBaseUrl": "{SINGLE_ORIGIN_BASE_URL}",
"PhotonAppId": "{PHOTON_APPID}", "PhotonAppId": "{PHOTON_APPID}",
"PhotonVoiceAppId": "{PHOTON_VOICE_APPID}", "PhotonVoiceAppId": "{PHOTON_VOICE_APPID}",
"PhotonCloudRegion": "{PHOTON_REGION}", "PhotonCloudRegion": "{PHOTON_REGION}",
"InjectAuthValues": true, "InjectAuthValues": false,
"EnableTlsTrustBypass": true "EnableTlsTrustBypass": true
} }
+54 -3
View File
@@ -65,7 +65,15 @@ builder.Services.AddDbContext<DorkNetDbContext>(opt =>
} }
else 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 <bin>\data\.
// Existing Docker / standalone deploys that mounted <bin>\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)!); Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
opt.UseSqlite($"Data Source={dbPath}", lite => lite.MigrationsAssembly("DorkNet.Server")); opt.UseSqlite($"Data Source={dbPath}", lite => lite.MigrationsAssembly("DorkNet.Server"));
} }
@@ -94,14 +102,17 @@ var domainScheme =
var domainPort = var domainPort =
builder.Configuration["Domain:Port"] builder.Configuration["Domain:Port"]
?? Environment.GetEnvironmentVariable("DORKNET_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<Microsoft.AspNetCore.HostFiltering.HostFilteringOptions>(opt => builder.Services.Configure<Microsoft.AspNetCore.HostFiltering.HostFilteringOptions>(opt =>
{ {
opt.AllowedHosts = new[] { apex, $"*.{apex}", "localhost", "127.0.0.1" }; opt.AllowedHosts = new[] { apex, $"*.{apex}", "localhost", "127.0.0.1" };
opt.AllowEmptyHosts = true; opt.AllowEmptyHosts = true;
opt.IncludeFailureMessage = 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 ────────────────────────────────────────────────────────────────── // ── Services ──────────────────────────────────────────────────────────────────
builder.Services.AddScoped<PlayerService>(); builder.Services.AddScoped<PlayerService>();
@@ -317,6 +328,30 @@ var app = builder.Build();
// hardcoded rec.net/localhost literals. // hardcoded rec.net/localhost literals.
var domainCfg = app.Services.GetRequiredService<DomainConfig>(); var domainCfg = app.Services.GetRequiredService<DomainConfig>();
// 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, // 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 // 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, // the game is asking for. This runs early enough to catch route misses,
@@ -1071,6 +1106,22 @@ app.MapHub<DorkNet.Server.Hubs.NotifyHub>("/hub/v1", opts =>
app.Run(); 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) static bool IsHealthProbe(HttpRequest request)
{ {
return request.Path.Equals("/healthz", StringComparison.OrdinalIgnoreCase); return request.Path.Equals("/healthz", StringComparison.OrdinalIgnoreCase);
+9 -1
View File
@@ -23,7 +23,9 @@ public class ConfigService(IConfiguration config, DomainConfig domain)
return new RecRoomConfig return new RecRoomConfig
{ {
MessageOfTheDay = config["Server:MOTD"] ?? "Welcome to the private server!", 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, PhotonConfig = photon,
ServiceUrls = BuildServiceUrlMap(domain), ServiceUrls = BuildServiceUrlMap(domain),
ConfigTable = new Dictionary<string, string> ConfigTable = new Dictionary<string, string>
@@ -110,6 +112,12 @@ public class ConfigService(IConfiguration config, DomainConfig domain)
var map = new Dictionary<string, string>(ServiceSubdomains.Length); var map = new Dictionary<string, string>(ServiceSubdomains.Length);
foreach (var (service, sub) in ServiceSubdomains) 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); var host = string.IsNullOrEmpty(sub) ? domain.Apex : domain.Sub(sub);
map[service] = domain.Url(host); map[service] = domain.Url(host);
} }
+6 -1
View File
@@ -13,12 +13,17 @@ public sealed class DomainConfig
public string Apex { get; } public string Apex { get; }
public string Scheme { get; } public string Scheme { get; }
public string? Port { 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; Apex = apex;
Scheme = string.IsNullOrWhiteSpace(scheme) ? "https" : scheme.TrimEnd(':', '/', '\\'); Scheme = string.IsNullOrWhiteSpace(scheme) ? "https" : scheme.TrimEnd(':', '/', '\\');
Port = string.IsNullOrWhiteSpace(port) ? null : port.Trim().TrimStart(':'); Port = string.IsNullOrWhiteSpace(port) ? null : port.Trim().TrimStart(':');
SingleOriginBaseUrl = string.IsNullOrWhiteSpace(singleOriginBaseUrl)
? null
: singleOriginBaseUrl.Trim().TrimEnd('/');
} }
public string Sub(string prefix) => $"{prefix}.{Apex}"; public string Sub(string prefix) => $"{prefix}.{Apex}";