mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}";
|
||||||
|
|||||||
Reference in New Issue
Block a user