Support local domains and bundled client patching

This commit is contained in:
Alexa
2026-05-27 18:27:09 +03:00
parent ecbf192e47
commit 676dcd09a7
10 changed files with 243 additions and 51 deletions
@@ -15,5 +15,5 @@ public class ServiceUrlsController(DomainConfig domain) : ControllerBase
{
[HttpGet("services")]
public ActionResult<Dictionary<string, string>> Get()
=> Ok(ConfigService.BuildServiceUrlMap(domain.Apex));
=> Ok(ConfigService.BuildServiceUrlMap(domain));
}
@@ -37,13 +37,13 @@ public class NsController : ControllerBase
// code: locker)" in BootSequence.
[HttpGet("/", Order = -1000)]
public ActionResult<Dictionary<string, string>> Root() =>
Ok(ConfigService.BuildServiceUrlMap(_domain.Apex));
Ok(ConfigService.BuildServiceUrlMap(_domain));
// Some builds fetch /v1 or /v1/services directly
[HttpGet("/v1")]
[HttpGet("/v1/services")]
public ActionResult<Dictionary<string, string>> Services() =>
Ok(ConfigService.BuildServiceUrlMap(_domain.Apex));
Ok(ConfigService.BuildServiceUrlMap(_domain));
// Ping / health used by the client keep-alive loop
[HttpGet("/v1/ping")]
+60 -19
View File
@@ -87,14 +87,21 @@ var apex =
builder.Configuration["Domain:Apex"]
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
?? "localhost";
builder.Services.AddSingleton(new DomainConfig(apex));
var domainScheme =
builder.Configuration["Domain:Scheme"]
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN_SCHEME")
?? "https";
var domainPort =
builder.Configuration["Domain:Port"]
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN_PORT");
builder.Services.AddSingleton(new DomainConfig(apex, domainScheme, domainPort));
builder.Services.Configure<Microsoft.AspNetCore.HostFiltering.HostFilteringOptions>(opt =>
{
opt.AllowedHosts = new[] { apex, $"*.{apex}", "localhost", "127.0.0.1" };
opt.AllowEmptyHosts = true;
opt.IncludeFailureMessage = true;
});
Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{apex}, *.{apex}, localhost]");
Console.WriteLine($"[domain] apex={apex}, scheme={domainScheme}, port={domainPort}, allowedHosts=[{apex}, *.{apex}, localhost]");
// ── Services ──────────────────────────────────────────────────────────────────
builder.Services.AddScoped<PlayerService>();
@@ -441,28 +448,18 @@ app.Use(async (ctx, next) =>
}
});
// ── Migrate DB ────────────────────────────────────────────────────────────────
// EF Core migrations under Migrations/. On boot we apply any pending ones —
// `Migrate()` is a no-op when the DB is already up to date and applies the
// missing migrations in order otherwise. Schema evolution is now exclusively
// done via `dotnet ef migrations add <Name>`; never call `EnsureCreated` (it
// bypasses the migration history table and leaves the db in a broken
// "exists but no __EFMigrationsHistory entries" state that future Migrate
// calls can't reason about).
//
// One-time upgrade path: DBs created by the previous EnsureCreated boot
// path already contain the Initial schema but have no __EFMigrationsHistory
// entries. The baseline detector below catches that case and inserts a
// row marking Initial as already applied, so Migrate() will only apply the
// Phase1+Phase2 deltas (column adds) instead of trying to re-CREATE TABLE.
// ── Create/repair DB ──────────────────────────────────────────────────────────
// Local launcher installs use SQLite as an appliance database. For that path,
// create the current EF model directly instead of replaying the migration chain;
// then run small idempotent repair patches for DBs made by older launcher builds.
// Hosted Postgres keeps its existing EnsureCreated + explicit patch path below.
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
if (db.Database.IsSqlite())
{
// Sqlite-only legacy baseline path — sqlite_master probes inside.
BaselineExistingSchemaIfNeeded(db);
db.Database.Migrate();
db.Database.EnsureCreated();
ApplySqliteCompatibilityPatches(db);
}
else
{
@@ -1185,3 +1182,47 @@ static void BaselineExistingSchemaIfNeeded(DorkNetDbContext db)
Log.Information(
"[migrations] Detected legacy EnsureCreated DB; baselined as 20260508092637_Initial. Subsequent migrations will apply normally.");
}
static void ApplySqliteCompatibilityPatches(DorkNetDbContext db)
{
if (!db.Database.IsSqlite()) return;
AddSqliteColumnIfMissing(db, "Rooms", "HiddenFromBrowse",
@"""HiddenFromBrowse"" INTEGER NOT NULL DEFAULT 0");
}
static void AddSqliteColumnIfMissing(DorkNetDbContext db, string table, string column, string definition)
{
var conn = db.Database.GetDbConnection();
var shouldClose = conn.State == System.Data.ConnectionState.Closed;
if (shouldClose) conn.Open();
try
{
var exists = false;
using var check = conn.CreateCommand();
check.CommandText = $"PRAGMA table_info(\"{table.Replace("\"", "\"\"")}\");";
{
using var reader = check.ExecuteReader();
while (reader.Read())
{
if (!string.Equals(reader.GetString(1), column, StringComparison.OrdinalIgnoreCase)) continue;
exists = true;
break;
}
}
if (exists)
{
Log.Information("[sqlite-compat] {Table}.{Column} already exists", table, column);
return;
}
using var alter = conn.CreateCommand();
alter.CommandText = $"ALTER TABLE \"{table.Replace("\"", "\"\"")}\" ADD COLUMN {definition};";
alter.ExecuteNonQuery();
Log.Information("[sqlite-compat] Added {Table}.{Column}", table, column);
}
finally
{
if (shouldClose) conn.Close();
}
}
+13 -4
View File
@@ -20,14 +20,12 @@ public class ConfigService(IConfiguration config, DomainConfig domain)
// the client used to hit us. The baseUrl parameter is kept
// for API stability but is no longer dereferenced.
_ = baseUrl;
var apex = domain.Apex;
return new RecRoomConfig
{
MessageOfTheDay = config["Server:MOTD"] ?? "Welcome to the private server!",
CdnBaseUri = $"https://{domain.Sub("cdn")}",
CdnBaseUri = domain.SubUrl("cdn"),
PhotonConfig = photon,
ServiceUrls = BuildServiceUrlMap(apex),
ServiceUrls = BuildServiceUrlMap(domain),
ConfigTable = new Dictionary<string, string>
{
// Season keys the 2019/2020 client checks at startup
@@ -106,4 +104,15 @@ public class ConfigService(IConfiguration config, DomainConfig domain)
}
return map;
}
public static Dictionary<string, string> BuildServiceUrlMap(DomainConfig domain)
{
var map = new Dictionary<string, string>(ServiceSubdomains.Length);
foreach (var (service, sub) in ServiceSubdomains)
{
var host = string.IsNullOrEmpty(sub) ? domain.Apex : domain.Sub(sub);
map[service] = domain.Url(host);
}
return map;
}
}
+12 -1
View File
@@ -11,8 +11,19 @@ namespace DorkNet.Server.Services;
public sealed class DomainConfig
{
public string Apex { get; }
public DomainConfig(string apex) { Apex = apex; }
public string Scheme { get; }
public string? Port { get; }
public DomainConfig(string apex, string scheme = "https", string? port = null)
{
Apex = apex;
Scheme = string.IsNullOrWhiteSpace(scheme) ? "https" : scheme.TrimEnd(':', '/', '\\');
Port = string.IsNullOrWhiteSpace(port) ? null : port.Trim().TrimStart(':');
}
public string Sub(string prefix) => $"{prefix}.{Apex}";
public string Url(string host) => Port is null ? $"{Scheme}://{host}" : $"{Scheme}://{host}:{Port}";
public string SubUrl(string prefix) => Url(Sub(prefix));
public static bool MatchesSubdomain(string host, string prefix)
=> host.StartsWith(prefix + ".", StringComparison.OrdinalIgnoreCase);
}
+20 -7
View File
@@ -923,14 +923,27 @@ public class RoomService(DorkNetDbContext db)
// Name="BloodMoon" because SaveChanges hadn't fired yet, so
// the image binding silently no-op'd. Mutating the already-
// loaded entity sidesteps that race.
var bloodmoon = await db.Rooms.FirstOrDefaultAsync(r => r.Name == "BloodMoon" || r.Name == "Crescendo");
if (bloodmoon is not null)
var crescendoRooms = await db.Rooms
.Where(r => r.Name.ToLower() == "bloodmoon" || r.Name.ToLower() == "crescendo")
.ToListAsync();
var crescendo = crescendoRooms.FirstOrDefault(r =>
string.Equals(r.Name, "Crescendo", StringComparison.OrdinalIgnoreCase));
var bloodMoon = crescendoRooms.FirstOrDefault(r =>
string.Equals(r.Name, "BloodMoon", StringComparison.OrdinalIgnoreCase));
var shouldRenameBloodMoon = crescendo is null;
crescendo ??= bloodMoon;
if (crescendo is not null)
{
if (bloodmoon.Name != "Crescendo") bloodmoon.Name = "Crescendo";
if (string.IsNullOrWhiteSpace(bloodmoon.Description))
bloodmoon.Description = "Brave the haunted halls of Castle Dracula and survive the night.";
if (bloodmoon.ImageName != "image_Crescendo.png")
bloodmoon.ImageName = "image_Crescendo.png";
if (shouldRenameBloodMoon && crescendo.Name != "Crescendo")
crescendo.Name = "Crescendo";
if (string.IsNullOrWhiteSpace(crescendo.Description))
crescendo.Description = "Brave the haunted halls of Castle Dracula and survive the night.";
if (crescendo.ImageName != "image_Crescendo.png")
crescendo.ImageName = "image_Crescendo.png";
}
foreach (var duplicate in crescendoRooms.Where(r => r.Id != crescendo?.Id))
{
duplicate.HiddenFromBrowse = true;
}
var paintball = await db.Rooms.FirstOrDefaultAsync(r => r.Name == "Paintball");