mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 15:38:45 -07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05099cce91 | |||
| 97f6522497 | |||
| e3bbcbe4a4 | |||
| 12b1a7f1e2 | |||
| 157cc457a9 | |||
| fa7486915b | |||
| 1794d4e777 | |||
| 8426091e91 | |||
| fa7dac9ec1 |
@@ -11,6 +11,9 @@ on:
|
||||
tags:
|
||||
- 'v1-december-*'
|
||||
|
||||
permissions:
|
||||
contents: write # create the GitHub release + upload assets
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: windows-latest
|
||||
|
||||
@@ -53,12 +53,14 @@ WORKDIR /src
|
||||
# DorkNet.Models project ref, so its csproj needs to be present too.
|
||||
COPY DorkNet.Server/DorkNet.Server.csproj DorkNet.Server/
|
||||
COPY DorkNet.Models/DorkNet.Models.csproj DorkNet.Models/
|
||||
COPY DorkNet.Contracts/DorkNet.Contracts.csproj DorkNet.Contracts/
|
||||
RUN dotnet restore DorkNet.Server/DorkNet.Server.csproj
|
||||
|
||||
# Now copy the rest of the .NET source, then drop the prebuilt admin
|
||||
# SPA into wwwroot/admin so the publish step ships it as static content.
|
||||
COPY DorkNet.Server/ DorkNet.Server/
|
||||
COPY DorkNet.Models/ DorkNet.Models/
|
||||
COPY DorkNet.Contracts/ DorkNet.Contracts/
|
||||
COPY --from=admin-ui /wwwroot/admin/ DorkNet.Server/wwwroot/admin/
|
||||
COPY --from=site /wwwroot/site/ DorkNet.Server/wwwroot/site/
|
||||
|
||||
|
||||
+57
-29
@@ -610,17 +610,7 @@ public class Mod : MelonMod
|
||||
{
|
||||
try
|
||||
{
|
||||
// Compute the UserData path ourselves rather than calling
|
||||
// MelonLoader.MelonUtils.UserDataDirectory (which is renamed
|
||||
// to MelonEnvironment.UserDataDirectory in newer 0.6.x builds
|
||||
// and shifts again in 0.7.x). Anchoring off the mod DLL's
|
||||
// location keeps this stable across versions: the DLL lives
|
||||
// in <game>/Mods/, so two dirname()'s reach the game root,
|
||||
// then MelonLoader/UserData/ is right next to it.
|
||||
var modDll = Assembly.GetExecutingAssembly().Location;
|
||||
var modsDir = Path.GetDirectoryName(modDll) ?? string.Empty;
|
||||
var gameDir = Path.GetDirectoryName(modsDir) ?? string.Empty;
|
||||
var userData = Path.Combine(gameDir, "MelonLoader", "UserData");
|
||||
var userData = ResolveUserDataDirectory();
|
||||
Directory.CreateDirectory(userData);
|
||||
DiagnosticsLogPath = Path.Combine(userData, "dorknet-diagnostics.log");
|
||||
var path = Path.Combine(userData, "dorknet-clientmod.json");
|
||||
@@ -631,21 +621,21 @@ 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("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("EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
|
||||
if (r.TryGetProperty("TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
|
||||
if (r.TryGetProperty("EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
||||
if (r.TryGetProperty("DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
|
||||
if (r.TryGetProperty("DesktopScreenShareFps", out v)) Cfg.DesktopScreenShareFps = (float)v.GetDouble();
|
||||
if (r.TryGetProperty("DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean();
|
||||
if (r.TryGetProperty("DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
|
||||
if (r.TryGetProperty("DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
|
||||
if (r.TryGetProperty("DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
||||
if (r.TryGetProperty("QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
|
||||
if (r.TryGetProperty("QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array)
|
||||
if (TryGetConfigValue(r, "ServerHost", out var v)) Cfg.ServerHost = v.GetString() ?? Cfg.ServerHost;
|
||||
if (TryGetConfigValue(r, "PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
|
||||
if (TryGetConfigValue(r, "PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
|
||||
if (TryGetConfigValue(r, "PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
|
||||
if (TryGetConfigValue(r, "EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
|
||||
if (TryGetConfigValue(r, "DesktopScreenShareFps", out v)) Cfg.DesktopScreenShareFps = (float)v.GetDouble();
|
||||
if (TryGetConfigValue(r, "DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
|
||||
if (TryGetConfigValue(r, "DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
|
||||
if (TryGetConfigValue(r, "DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
|
||||
if (TryGetConfigValue(r, "QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var rooms = new List<string>();
|
||||
foreach (var e in v.EnumerateArray())
|
||||
@@ -653,8 +643,8 @@ public class Mod : MelonMod
|
||||
rooms.Add(e.GetString()!);
|
||||
Cfg.QuestMaxTeamSizeRooms = rooms.ToArray(); // explicit (incl. empty = none)
|
||||
}
|
||||
if (r.TryGetProperty("EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
||||
if (r.TryGetProperty("DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
||||
if (TryGetConfigValue(r, "EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
||||
if (TryGetConfigValue(r, "DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var list = new List<string>();
|
||||
foreach (var e in v.EnumerateArray())
|
||||
@@ -666,7 +656,7 @@ public class Mod : MelonMod
|
||||
}
|
||||
// "InjectAuthValues" key in the template is now ignored —
|
||||
// see attic/AuthValuesInjector.cs.attic.
|
||||
Log.Msg($"[config] loaded: ServerHost={Cfg.ServerHost}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "<unset>" : "<set>")}, " +
|
||||
Log.Msg($"[config] loaded from {path}: ServerHost={Cfg.ServerHost}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "<unset>" : "<set>")}, " +
|
||||
$"EnableTlsTrustBypass={Cfg.EnableTlsTrustBypass}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -675,6 +665,44 @@ public class Mod : MelonMod
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveUserDataDirectory()
|
||||
{
|
||||
foreach (var typeName in new[]
|
||||
{
|
||||
"MelonLoader.MelonEnvironment, MelonLoader",
|
||||
"MelonLoader.MelonUtils, MelonLoader",
|
||||
})
|
||||
{
|
||||
var type = Type.GetType(typeName);
|
||||
var prop = type?.GetProperty("UserDataDirectory", BindingFlags.Public | BindingFlags.Static);
|
||||
if (prop?.GetValue(null) is string value && !string.IsNullOrWhiteSpace(value))
|
||||
return value;
|
||||
}
|
||||
|
||||
var modDll = Assembly.GetExecutingAssembly().Location;
|
||||
var modsDir = Path.GetDirectoryName(modDll) ?? string.Empty;
|
||||
var gameDir = Path.GetDirectoryName(modsDir) ?? AppContext.BaseDirectory;
|
||||
return Path.Combine(gameDir, "MelonLoader", "UserData");
|
||||
}
|
||||
|
||||
private static bool TryGetConfigValue(JsonElement root, string name, out JsonElement value)
|
||||
{
|
||||
if (root.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var prop in root.EnumerateObject())
|
||||
{
|
||||
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = prop.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Patch dispatch helpers ────────────────────────────────────────
|
||||
private bool TryPatch(string label, Type target, string methodName,
|
||||
Type[]? args, string? prefix = null, string? postfix = null,
|
||||
|
||||
@@ -164,6 +164,20 @@ public static class DorkNetRouteOwnership
|
||||
|
||||
public static IReadOnlyList<DorkNetRouteGroup> RouteGroups => ServiceRouteGroups;
|
||||
|
||||
public static IReadOnlyList<string> PublicSubdomains =>
|
||||
ServiceRouteGroups
|
||||
.SelectMany(group => group.HostSubdomains)
|
||||
// api.* is a shared public host: it owns no service by host and is
|
||||
// dispatched by path (see ResolvePublicService's path-prefix pass),
|
||||
// so it never appears in a group's HostSubdomains. It must still be
|
||||
// whitelisted here or HostFiltering rejects every api.{apex} request.
|
||||
.Append("api")
|
||||
.Append(AdminHost)
|
||||
.Append("feed")
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Order(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
public static bool IsInfrastructurePath(string path)
|
||||
{
|
||||
return path.Equals("/healthz", StringComparison.OrdinalIgnoreCase)
|
||||
|
||||
@@ -26,6 +26,20 @@ var app = builder.Build();
|
||||
|
||||
app.MapDorkNetServiceDefaults();
|
||||
|
||||
var publicHosts = BuildPublicHosts(apexDomain);
|
||||
app.Use(async (ctx, next) =>
|
||||
{
|
||||
if (DorkNetRouteOwnership.IsInfrastructurePath(ctx.Request.Path.Value ?? "/")
|
||||
|| IsAllowedPublicHost(ctx.Request.Host.Host, publicHosts))
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
await ctx.Response.WriteAsync("Unknown DorkNet host.");
|
||||
});
|
||||
|
||||
app.MapGet("/internal/services", (IOptions<DorkNetServiceMapOptions> options) =>
|
||||
Results.Ok(options.Value.Endpoints()));
|
||||
app.MapGet("/internal/routes", () =>
|
||||
@@ -190,6 +204,33 @@ static string[] WebBrowserHosts(string apexDomain)
|
||||
return [apexDomain, SubdomainHost("www", apexDomain)];
|
||||
}
|
||||
|
||||
static HashSet<string> BuildPublicHosts(string apexDomain)
|
||||
{
|
||||
var hosts = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
apexDomain,
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
};
|
||||
|
||||
foreach (var subdomain in DorkNetRouteOwnership.PublicSubdomains)
|
||||
{
|
||||
hosts.Add(SubdomainHost(subdomain, apexDomain));
|
||||
}
|
||||
|
||||
return hosts;
|
||||
}
|
||||
|
||||
static bool IsAllowedPublicHost(string? host, HashSet<string> publicHosts)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return publicHosts.Contains(host.Split(':', 2)[0].Trim());
|
||||
}
|
||||
|
||||
static void AddHostRoute(
|
||||
List<RouteConfig> routes,
|
||||
ref int order,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class NotificationController(DomainConfig domain) : ControllerBase
|
||||
public IActionResult Connect() => StatusCode(StatusCodes.Status410Gone, new
|
||||
{
|
||||
error = "use_signalr_hub",
|
||||
hint = $"connect to {domain.Sub("notify")}/hub/v1 instead",
|
||||
hint = $"connect to {domain.Url("notify", "/hub/v1")} instead",
|
||||
});
|
||||
|
||||
/// <summary>GET <c>api/notification/hub/v1</c> — returns the
|
||||
@@ -27,7 +27,7 @@ public class NotificationController(DomainConfig domain) : ControllerBase
|
||||
[HttpGet("/api/notification/hub/v1")]
|
||||
public IActionResult HubInfo() => Ok(new
|
||||
{
|
||||
Url = $"https://{domain.Sub("notify")}/hub/v1",
|
||||
Url = domain.Url("notify", "/hub/v1"),
|
||||
Protocol = "signalr",
|
||||
Version = 1,
|
||||
});
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -180,8 +180,13 @@ public static class LegacyUpgrades
|
||||
// either no-op (Crescendo now exists) or the only writer.
|
||||
for (var attempt = 1; attempt <= 2; attempt++)
|
||||
{
|
||||
var crescendoExists = await db.Rooms.AnyAsync(r => r.Name == "Crescendo", ct);
|
||||
var bloodMoon = await db.Rooms.FirstOrDefaultAsync(r => r.Name == "BloodMoon", ct);
|
||||
var renameCandidates = await db.Rooms
|
||||
.Where(r => r.Name.ToLower() == "bloodmoon" || r.Name.ToLower() == "crescendo")
|
||||
.ToListAsync(ct);
|
||||
var crescendoExists = renameCandidates.Any(r =>
|
||||
string.Equals(r.Name, "Crescendo", StringComparison.OrdinalIgnoreCase));
|
||||
var bloodMoon = renameCandidates.FirstOrDefault(r =>
|
||||
string.Equals(r.Name, "BloodMoon", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (bloodMoon is null && crescendoExists)
|
||||
{
|
||||
@@ -217,15 +222,15 @@ public static class LegacyUpgrades
|
||||
{
|
||||
// Both exist — the rename ran on a different row at
|
||||
// some point, and the original BloodMoon row is now
|
||||
// a stale duplicate. Park it under CrescendoLegacy
|
||||
// and hide from browse so /goto/name/... still
|
||||
// a stale duplicate. Park it under a unique legacy
|
||||
// name and hide from browse so /goto/name/... still
|
||||
// resolves Crescendo to the right row.
|
||||
bloodMoon.Name = "CrescendoLegacy";
|
||||
bloodMoon.Name = await PickUniqueRoomNameAsync(db, "CrescendoLegacy", bloodMoon.Id, ct);
|
||||
bloodMoon.HiddenFromBrowse = true;
|
||||
await db.SaveChangesAsync(ct);
|
||||
logger.LogInformation(
|
||||
"[legacy-upgrade] RenameBloodMoonToCrescendo: applied — Rooms.Id={Id} parked as CrescendoLegacy (Crescendo already existed on a different row)",
|
||||
bloodMoon.Id);
|
||||
"[legacy-upgrade] RenameBloodMoonToCrescendo: applied — Rooms.Id={Id} parked as {Name} (Crescendo already existed on a different row)",
|
||||
bloodMoon.Id, bloodMoon.Name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -241,4 +246,22 @@ public static class LegacyUpgrades
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> PickUniqueRoomNameAsync(
|
||||
DorkNetDbContext db, string preferred, long currentRoomId, CancellationToken ct)
|
||||
{
|
||||
var taken = await db.Rooms
|
||||
.Where(r => r.Id != currentRoomId && r.Name.ToLower().StartsWith(preferred.ToLower()))
|
||||
.Select(r => r.Name)
|
||||
.ToListAsync(ct);
|
||||
|
||||
if (!taken.Any(n => string.Equals(n, preferred, StringComparison.OrdinalIgnoreCase)))
|
||||
return preferred;
|
||||
|
||||
var suffix = 2;
|
||||
while (taken.Any(n => string.Equals(n, $"{preferred}{suffix}", StringComparison.OrdinalIgnoreCase)))
|
||||
suffix++;
|
||||
|
||||
return $"{preferred}{suffix}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DorkNet.Models\DorkNet.Models.csproj" />
|
||||
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Service-slice containers reference DorkNet.Server for shared controllers
|
||||
|
||||
@@ -25,9 +25,9 @@ public class ConfigService(IConfiguration config, DomainConfig domain)
|
||||
return new RecRoomConfig
|
||||
{
|
||||
MessageOfTheDay = config["Server:MOTD"] ?? "Welcome to the private server!",
|
||||
CdnBaseUri = $"https://{domain.Sub("cdn")}",
|
||||
CdnBaseUri = domain.Url("cdn"),
|
||||
PhotonConfig = photon,
|
||||
ServiceUrls = BuildServiceUrlMap(apex),
|
||||
ServiceUrls = BuildServiceUrlMap(domain),
|
||||
ConfigTable = new Dictionary<string, string>
|
||||
{
|
||||
// Season keys the 2019/2020 client checks at startup
|
||||
@@ -110,12 +110,14 @@ public class ConfigService(IConfiguration config, DomainConfig domain)
|
||||
/// <see cref="DomainConfig.Apex"/>) so the map stays a single
|
||||
/// source of truth.</summary>
|
||||
public static Dictionary<string, string> BuildServiceUrlMap(string apex)
|
||||
=> BuildServiceUrlMap(new DomainConfig(apex));
|
||||
|
||||
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) ? apex : $"{sub}.{apex}";
|
||||
map[service] = $"https://{host}";
|
||||
map[service] = domain.Url(sub);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,28 @@ namespace DorkNet.Server.Services;
|
||||
public sealed class DomainConfig
|
||||
{
|
||||
public string Apex { get; }
|
||||
public DomainConfig(string apex) { Apex = apex; }
|
||||
public string Scheme { get; }
|
||||
|
||||
public DomainConfig(string apex, string scheme = "https")
|
||||
{
|
||||
Apex = apex;
|
||||
Scheme = NormalizeScheme(scheme);
|
||||
}
|
||||
|
||||
public string Sub(string prefix) => $"{prefix}.{Apex}";
|
||||
public string Url(string prefix, string path = "")
|
||||
{
|
||||
var host = string.IsNullOrEmpty(prefix) ? Apex : Sub(prefix);
|
||||
if (string.IsNullOrEmpty(path)) return $"{Scheme}://{host}";
|
||||
return $"{Scheme}://{host}{(path.StartsWith('/') ? path : "/" + path)}";
|
||||
}
|
||||
|
||||
public static bool MatchesSubdomain(string host, string prefix)
|
||||
=> 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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ public static class MiddlewarePipelineExtensions
|
||||
if (ctx.Request.Host.Host.Equals(domainCfg.Sub("api"), StringComparison.OrdinalIgnoreCase) &&
|
||||
(ctx.Request.Path == "/admin" || ctx.Request.Path.StartsWithSegments("/admin")))
|
||||
{
|
||||
ctx.Response.Redirect($"https://{domainCfg.Sub("admin")}/", permanent: false);
|
||||
ctx.Response.Redirect(domainCfg.Url("admin", "/"), permanent: false);
|
||||
return;
|
||||
}
|
||||
await next();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using DorkNet.Contracts;
|
||||
using DorkNet.Server.Compat;
|
||||
using DorkNet.Server.Data;
|
||||
using DorkNet.Server.Services;
|
||||
@@ -106,14 +107,19 @@ public static class ServiceCollectionExtensions
|
||||
builder.Configuration["Domain:Apex"]
|
||||
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
|
||||
?? "localhost";
|
||||
builder.Services.AddSingleton(new DomainConfig(apex));
|
||||
var scheme = builder.Configuration["Domain:Scheme"] ?? "https";
|
||||
builder.Services.AddSingleton(new DomainConfig(apex, scheme));
|
||||
var allowedHosts = new[] { apex, "localhost", "127.0.0.1" }
|
||||
.Concat(DorkNetRouteOwnership.PublicSubdomains.Select(subdomain => $"{subdomain}.{apex}"))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
builder.Services.Configure<Microsoft.AspNetCore.HostFiltering.HostFilteringOptions>(opt =>
|
||||
{
|
||||
opt.AllowedHosts = new[] { apex, $"*.{apex}", "localhost", "127.0.0.1" };
|
||||
opt.AllowedHosts = allowedHosts;
|
||||
opt.AllowEmptyHosts = true;
|
||||
opt.IncludeFailureMessage = true;
|
||||
});
|
||||
Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{apex}, *.{apex}, localhost]");
|
||||
Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{string.Join(", ", allowedHosts)}]");
|
||||
}
|
||||
|
||||
// ── Services ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -17,8 +17,9 @@ x-dorknet-service-env: &dorknet-service-env
|
||||
|
||||
x-dorknet-app-networks: &dorknet-app-networks
|
||||
networks:
|
||||
# Internal only — backends are reached by the gateway over `default`,
|
||||
# never directly by Traefik, so keep them off dokploy-network.
|
||||
- default
|
||||
- dokploy-network
|
||||
|
||||
services:
|
||||
gateway:
|
||||
@@ -42,6 +43,18 @@ services:
|
||||
DorkNet__Services__Monolith: http://monolith:8080
|
||||
expose:
|
||||
- "8080"
|
||||
networks:
|
||||
- default
|
||||
- dokploy-network
|
||||
labels:
|
||||
# Cloudflare terminates TLS at the edge; the tunnel -> Traefik leg is
|
||||
# internal HTTP, so route on the `web` entrypoint with no certresolver.
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=dokploy-network
|
||||
# Scope routing to YOUR domain (apex + subdomains) instead of catch-all:
|
||||
- traefik.http.routers.dorknet-gw.rule=Host(`${DORKNET_DOMAIN}`) || HostRegexp(`^.+\.${DORKNET_DOMAIN}$`)
|
||||
- traefik.http.routers.dorknet-gw.entrypoints=web
|
||||
- traefik.http.services.dorknet-gw.loadbalancer.server.port=8080
|
||||
depends_on:
|
||||
- identity
|
||||
- rooms
|
||||
@@ -57,6 +70,10 @@ services:
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN:?Set CLOUDFLARE_TUNNEL_TOKEN in Dokploy}
|
||||
# Must share dokploy-network so the tunnel can reach the Traefik
|
||||
# container (dokploy-traefik) by name and hand traffic off to it.
|
||||
networks:
|
||||
- dokploy-network
|
||||
depends_on:
|
||||
- gateway
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# DorkNet Cloudflare Tunnel config — Coolify production target.
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
# Routes every localhost subdomain through the tunnel into the server
|
||||
# container running on Coolify. The tunnel runs as its own Coolify
|
||||
# Routes only DorkNet's known service subdomains through the tunnel
|
||||
# into the server container running on Coolify. The tunnel runs as its own Coolify
|
||||
# service (or as a sidecar in the same project) and reaches the
|
||||
# server over the internal Docker network.
|
||||
#
|
||||
@@ -49,24 +49,93 @@ originRequest:
|
||||
keepAliveTimeout: 10m
|
||||
noHappyEyeballs: false
|
||||
|
||||
# Ingress rules are evaluated top-down. The wildcard catches every
|
||||
# subdomain we don't list explicitly, so when the 2020 Rec Room client
|
||||
# (post-domain-patch) hits a niche subdomain like roomieintegrations.
|
||||
# localhost, the request still reaches the server.
|
||||
# Ingress rules are evaluated top-down. Replace localhost below with
|
||||
# your DORKNET_DOMAIN when installing this as a static cloudflared
|
||||
# config. Do not use a wildcard hostname: only these known client-facing
|
||||
# hosts should reach the gateway.
|
||||
ingress:
|
||||
- hostname: localhost
|
||||
service: {{UPSTREAM}}
|
||||
originRequest:
|
||||
httpHostHeader: localhost
|
||||
|
||||
- hostname: "*.localhost"
|
||||
- hostname: admin.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: api.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: accounts.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: auth.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: bugreporting.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: cards.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: cdn.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: chat.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: clubs.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: cms.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: commerce.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: data.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: datacollection.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: discovery.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: econ.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: feed.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: gamelogs.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: geo.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: img.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: leaderboard.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: link.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: lists.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: match.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: moderation.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: ns.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: notify.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: platformnotifications.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: playersettings.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: roomcomments.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: roomieintegrations.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: rooms.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: storage.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: strings.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: strings-cdn.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: studio.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: thorn.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: videos.localhost
|
||||
service: {{UPSTREAM}}
|
||||
- hostname: www.localhost
|
||||
service: {{UPSTREAM}}
|
||||
# cloudflared forwards the original Host header by default, which
|
||||
# is what the server's [Host("api.localhost")] etc. attributes match
|
||||
# against. No explicit httpHostHeader here so wildcards stay
|
||||
# transparent.
|
||||
|
||||
# Catch-all rejects anything that didn't match (defensive — the
|
||||
# tunnel only routes traffic for hostnames we've added DNS records
|
||||
# for, so this is rarely reached).
|
||||
# tunnel only routes traffic for hostnames listed above).
|
||||
- service: http_status:404
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# DorkNet Cloudflare Tunnel config
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
# Routes every localhost subdomain through the tunnel into the local
|
||||
# server (HTTPS on port 443). Generated by tools/setup-cloudflared.ps1
|
||||
# Routes only DorkNet's known localhost service subdomains through the
|
||||
# tunnel into the local server (HTTPS on port 443). Generated by tools/setup-cloudflared.ps1
|
||||
# — copy to %USERPROFILE%\.cloudflared\config.yml and replace the
|
||||
# {{TUNNEL_ID}} / {{CREDENTIALS_FILE}} placeholders.
|
||||
#
|
||||
@@ -14,27 +14,96 @@
|
||||
tunnel: {{TUNNEL_ID}}
|
||||
credentials-file: {{CREDENTIALS_FILE}}
|
||||
|
||||
# Ingress rules are evaluated top-down. The wildcard catches every
|
||||
# subdomain we don't list explicitly, so when the 2020 Rec Room client
|
||||
# (post-domain-patch) hits a niche subdomain like roomieintegrations.
|
||||
# localhost, the request still reaches the server.
|
||||
originRequest:
|
||||
noTLSVerify: true
|
||||
|
||||
# Ingress rules are evaluated top-down. Keep this list aligned with
|
||||
# DorkNetRouteOwnership.PublicSubdomains and ConfigService.ServiceSubdomains.
|
||||
# Do not use *.localhost here: only the subdomains the client actually
|
||||
# needs should reach the gateway/server.
|
||||
ingress:
|
||||
- hostname: localhost
|
||||
service: https://localhost:443
|
||||
originRequest:
|
||||
noTLSVerify: true
|
||||
httpHostHeader: localhost
|
||||
|
||||
- hostname: "*.localhost"
|
||||
- hostname: admin.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: api.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: accounts.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: auth.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: bugreporting.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: cards.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: cdn.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: chat.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: clubs.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: cms.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: commerce.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: data.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: datacollection.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: discovery.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: econ.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: feed.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: gamelogs.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: geo.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: img.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: leaderboard.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: link.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: lists.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: match.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: moderation.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: ns.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: notify.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: platformnotifications.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: playersettings.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: roomcomments.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: roomieintegrations.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: rooms.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: storage.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: strings.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: strings-cdn.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: studio.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: thorn.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: videos.localhost
|
||||
service: https://localhost:443
|
||||
- hostname: www.localhost
|
||||
service: https://localhost:443
|
||||
originRequest:
|
||||
noTLSVerify: true
|
||||
# Forward the original Host header verbatim so the server's
|
||||
# [Host("api.localhost")] etc. attributes match correctly.
|
||||
# (cloudflared does this by default; setting it explicitly so
|
||||
# config.yml is self-documenting.)
|
||||
|
||||
# Catch-all rejects anything that didn't match (defensive — the
|
||||
# tunnel only routes traffic for hostnames we've added DNS records
|
||||
# for, so this is rarely reached).
|
||||
# tunnel only routes traffic for hostnames listed above).
|
||||
- service: http_status:404
|
||||
|
||||
@@ -78,19 +78,15 @@ $Template = Join-Path $PSScriptRoot 'cloudflared-config.yml.template'
|
||||
$CfDir = Join-Path $env:USERPROFILE '.cloudflared'
|
||||
$ConfigPath = Join-Path $CfDir 'config.yml'
|
||||
|
||||
# Subdomains we want routed through the tunnel. The wildcard route in
|
||||
# config.yml catches everything; this list drives the per-subdomain
|
||||
# DNS CNAMEs Cloudflare needs (no automatic wildcard CNAME via the
|
||||
# CLI — each one is created explicitly via `cloudflared tunnel route
|
||||
# dns`).
|
||||
# Subdomains routed through the tunnel. Keep this list aligned with
|
||||
# DorkNetRouteOwnership.PublicSubdomains and the cloudflared config
|
||||
# template. We intentionally create explicit DNS routes instead of a
|
||||
# wildcard so unrelated subdomains do not reach the gateway.
|
||||
$Subdomains = @(
|
||||
'@', # apex (localhost itself)
|
||||
'admin', 'api', 'accounts', 'auth', 'cdn', 'chat', 'commerce',
|
||||
'discovery', 'econ', 'feed', 'geo', 'img', 'match', 'notify',
|
||||
'ns', 'playersettings', 'rooms', 'storage', 'strings', 'strings-cdn',
|
||||
# Long-tail subdomains DorkNet's stub controllers ack — adding
|
||||
# CNAMEs for these means the patched client never hits a NXDOMAIN
|
||||
# for any of the URLs it might probe.
|
||||
'bugreporting', 'cards', 'clubs', 'cms', 'data', 'datacollection',
|
||||
'gamelogs', 'leaderboard', 'link', 'lists', 'moderation',
|
||||
'platformnotifications', 'roomcomments', 'roomieintegrations',
|
||||
|
||||
Reference in New Issue
Block a user