mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24cc13084a | |||
| 193d9b343c | |||
| 97f6522497 | |||
| e3bbcbe4a4 | |||
| 12b1a7f1e2 | |||
| 157cc457a9 | |||
| fa7486915b | |||
| 1794d4e777 | |||
| 8426091e91 | |||
| fa7dac9ec1 |
@@ -11,6 +11,9 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- 'v1-december-*'
|
- 'v1-december-*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # create the GitHub release + upload assets
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|||||||
@@ -53,12 +53,14 @@ WORKDIR /src
|
|||||||
# DorkNet.Models project ref, so its csproj needs to be present too.
|
# DorkNet.Models project ref, so its csproj needs to be present too.
|
||||||
COPY DorkNet.Server/DorkNet.Server.csproj DorkNet.Server/
|
COPY DorkNet.Server/DorkNet.Server.csproj DorkNet.Server/
|
||||||
COPY DorkNet.Models/DorkNet.Models.csproj DorkNet.Models/
|
COPY DorkNet.Models/DorkNet.Models.csproj DorkNet.Models/
|
||||||
|
COPY DorkNet.Contracts/DorkNet.Contracts.csproj DorkNet.Contracts/
|
||||||
RUN dotnet restore DorkNet.Server/DorkNet.Server.csproj
|
RUN dotnet restore DorkNet.Server/DorkNet.Server.csproj
|
||||||
|
|
||||||
# Now copy the rest of the .NET source, then drop the prebuilt admin
|
# 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.
|
# SPA into wwwroot/admin so the publish step ships it as static content.
|
||||||
COPY DorkNet.Server/ DorkNet.Server/
|
COPY DorkNet.Server/ DorkNet.Server/
|
||||||
COPY DorkNet.Models/ DorkNet.Models/
|
COPY DorkNet.Models/ DorkNet.Models/
|
||||||
|
COPY DorkNet.Contracts/ DorkNet.Contracts/
|
||||||
COPY --from=admin-ui /wwwroot/admin/ DorkNet.Server/wwwroot/admin/
|
COPY --from=admin-ui /wwwroot/admin/ DorkNet.Server/wwwroot/admin/
|
||||||
COPY --from=site /wwwroot/site/ DorkNet.Server/wwwroot/site/
|
COPY --from=site /wwwroot/site/ DorkNet.Server/wwwroot/site/
|
||||||
|
|
||||||
|
|||||||
+57
-29
@@ -610,17 +610,7 @@ public class Mod : MelonMod
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Compute the UserData path ourselves rather than calling
|
var userData = ResolveUserDataDirectory();
|
||||||
// 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");
|
|
||||||
Directory.CreateDirectory(userData);
|
Directory.CreateDirectory(userData);
|
||||||
DiagnosticsLogPath = Path.Combine(userData, "dorknet-diagnostics.log");
|
DiagnosticsLogPath = Path.Combine(userData, "dorknet-diagnostics.log");
|
||||||
var path = Path.Combine(userData, "dorknet-clientmod.json");
|
var path = Path.Combine(userData, "dorknet-clientmod.json");
|
||||||
@@ -631,21 +621,21 @@ 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 (TryGetConfigValue(r, "ServerHost", out var v)) Cfg.ServerHost = v.GetString() ?? Cfg.ServerHost;
|
||||||
if (r.TryGetProperty("PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
|
if (TryGetConfigValue(r, "PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
|
||||||
if (r.TryGetProperty("PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
|
if (TryGetConfigValue(r, "PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
|
||||||
if (r.TryGetProperty("PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
|
if (TryGetConfigValue(r, "PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
|
||||||
if (r.TryGetProperty("EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
|
if (TryGetConfigValue(r, "EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
|
||||||
if (r.TryGetProperty("TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
|
if (TryGetConfigValue(r, "TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
|
||||||
if (r.TryGetProperty("EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
if (TryGetConfigValue(r, "EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
||||||
if (r.TryGetProperty("DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
|
if (TryGetConfigValue(r, "DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
|
||||||
if (r.TryGetProperty("DesktopScreenShareFps", out v)) Cfg.DesktopScreenShareFps = (float)v.GetDouble();
|
if (TryGetConfigValue(r, "DesktopScreenShareFps", out v)) Cfg.DesktopScreenShareFps = (float)v.GetDouble();
|
||||||
if (r.TryGetProperty("DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean();
|
if (TryGetConfigValue(r, "DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean();
|
||||||
if (r.TryGetProperty("DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
|
if (TryGetConfigValue(r, "DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
|
||||||
if (r.TryGetProperty("DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
|
if (TryGetConfigValue(r, "DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
|
||||||
if (r.TryGetProperty("DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
if (TryGetConfigValue(r, "DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
||||||
if (r.TryGetProperty("QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
|
if (TryGetConfigValue(r, "QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
|
||||||
if (r.TryGetProperty("QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array)
|
if (TryGetConfigValue(r, "QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
var rooms = new List<string>();
|
var rooms = new List<string>();
|
||||||
foreach (var e in v.EnumerateArray())
|
foreach (var e in v.EnumerateArray())
|
||||||
@@ -653,8 +643,8 @@ public class Mod : MelonMod
|
|||||||
rooms.Add(e.GetString()!);
|
rooms.Add(e.GetString()!);
|
||||||
Cfg.QuestMaxTeamSizeRooms = rooms.ToArray(); // explicit (incl. empty = none)
|
Cfg.QuestMaxTeamSizeRooms = rooms.ToArray(); // explicit (incl. empty = none)
|
||||||
}
|
}
|
||||||
if (r.TryGetProperty("EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
if (TryGetConfigValue(r, "EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
||||||
if (r.TryGetProperty("DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
if (TryGetConfigValue(r, "DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
var list = new List<string>();
|
var list = new List<string>();
|
||||||
foreach (var e in v.EnumerateArray())
|
foreach (var e in v.EnumerateArray())
|
||||||
@@ -666,7 +656,7 @@ public class Mod : MelonMod
|
|||||||
}
|
}
|
||||||
// "InjectAuthValues" key in the template is now ignored —
|
// "InjectAuthValues" key in the template is now ignored —
|
||||||
// see attic/AuthValuesInjector.cs.attic.
|
// 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}");
|
$"EnableTlsTrustBypass={Cfg.EnableTlsTrustBypass}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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 ────────────────────────────────────────
|
// ── Patch dispatch helpers ────────────────────────────────────────
|
||||||
private bool TryPatch(string label, Type target, string methodName,
|
private bool TryPatch(string label, Type target, string methodName,
|
||||||
Type[]? args, string? prefix = null, string? postfix = null,
|
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<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)
|
public static bool IsInfrastructurePath(string path)
|
||||||
{
|
{
|
||||||
return path.Equals("/healthz", StringComparison.OrdinalIgnoreCase)
|
return path.Equals("/healthz", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|||||||
@@ -26,6 +26,20 @@ var app = builder.Build();
|
|||||||
|
|
||||||
app.MapDorkNetServiceDefaults();
|
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) =>
|
app.MapGet("/internal/services", (IOptions<DorkNetServiceMapOptions> options) =>
|
||||||
Results.Ok(options.Value.Endpoints()));
|
Results.Ok(options.Value.Endpoints()));
|
||||||
app.MapGet("/internal/routes", () =>
|
app.MapGet("/internal/routes", () =>
|
||||||
@@ -190,6 +204,33 @@ static string[] WebBrowserHosts(string apexDomain)
|
|||||||
return [apexDomain, SubdomainHost("www", 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(
|
static void AddHostRoute(
|
||||||
List<RouteConfig> routes,
|
List<RouteConfig> routes,
|
||||||
ref int order,
|
ref int order,
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ public class ServiceUrlsController(DomainConfig domain) : ControllerBase
|
|||||||
{
|
{
|
||||||
[HttpGet("services")]
|
[HttpGet("services")]
|
||||||
public ActionResult<Dictionary<string, string>> Get()
|
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
|
public IActionResult Connect() => StatusCode(StatusCodes.Status410Gone, new
|
||||||
{
|
{
|
||||||
error = "use_signalr_hub",
|
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
|
/// <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")]
|
[HttpGet("/api/notification/hub/v1")]
|
||||||
public IActionResult HubInfo() => Ok(new
|
public IActionResult HubInfo() => Ok(new
|
||||||
{
|
{
|
||||||
Url = $"https://{domain.Sub("notify")}/hub/v1",
|
Url = domain.Url("notify", "/hub/v1"),
|
||||||
Protocol = "signalr",
|
Protocol = "signalr",
|
||||||
Version = 1,
|
Version = 1,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ namespace DorkNet.Server.Controllers.Match;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
public class MatchPlayerController(
|
public class MatchPlayerController(
|
||||||
PlayerPresenceService presence,
|
PlayerPresenceService presence,
|
||||||
NotificationService notifications,
|
|
||||||
RoomService rooms,
|
RoomService rooms,
|
||||||
PrivateInstanceService privateInstances,
|
PrivateInstanceService privateInstances,
|
||||||
IConfiguration config,
|
IConfiguration config,
|
||||||
@@ -37,7 +36,7 @@ public class MatchPlayerController(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("/player/login")]
|
[HttpPost("/player/login")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
||||||
public async Task<IActionResult> PlayerLogin([FromForm(Name = "LoginLock")] string? loginLock)
|
public IActionResult PlayerLogin([FromForm(Name = "LoginLock")] string? loginLock)
|
||||||
{
|
{
|
||||||
var playerId = TryGetCurrentPlayerId();
|
var playerId = TryGetCurrentPlayerId();
|
||||||
if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock))
|
if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock))
|
||||||
@@ -46,17 +45,15 @@ public class MatchPlayerController(
|
|||||||
if (replaced)
|
if (replaced)
|
||||||
{
|
{
|
||||||
log.LogWarning(
|
log.LogWarning(
|
||||||
"[player-login] concurrent session for player={PlayerId}; kicking old session(s) via ModerationKick push",
|
"[player-login] concurrent session marker replaced for player={PlayerId}; stale SignalR connections will be evicted on hub connect",
|
||||||
playerId);
|
playerId);
|
||||||
// The new session hasn't opened its SignalR socket yet
|
// Do not broadcast ModerationKick to the whole player group
|
||||||
// (negotiate fires AFTER /player/login on the watch's
|
// here. During reconnects the active watch can already have a
|
||||||
// boot sequence — verified in the live trace), so the
|
// SignalR connection in that group, and the 2020 client crashes
|
||||||
// ModerationKick push hits only the OLDER session's
|
// hard if it receives a malformed/stale moderation payload.
|
||||||
// existing connections. They'll show the "you've been
|
// NotifyHub.OnConnectedAsync snapshots existing connection ids
|
||||||
// kicked" dialog and gracefully disconnect via the
|
// before adding the new connection and sends a targeted kick to
|
||||||
// watch's LocalModerationKickSelf path.
|
// those ids only.
|
||||||
await notifications.KickStaleSession(playerId,
|
|
||||||
"Your account was signed in on another device.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Ok(Array.Empty<object>());
|
return Ok(Array.Empty<object>());
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ public class NsController : ControllerBase
|
|||||||
// code: locker)" in BootSequence.
|
// code: locker)" in BootSequence.
|
||||||
[HttpGet("/", Order = -1000)]
|
[HttpGet("/", Order = -1000)]
|
||||||
public ActionResult<Dictionary<string, string>> Root() =>
|
public ActionResult<Dictionary<string, string>> Root() =>
|
||||||
Ok(ConfigService.BuildServiceUrlMap(_domain.Apex));
|
Ok(ConfigService.BuildServiceUrlMap(_domain));
|
||||||
|
|
||||||
// Some builds fetch /v1 or /v1/services directly
|
// Some builds fetch /v1 or /v1/services directly
|
||||||
[HttpGet("/v1")]
|
[HttpGet("/v1")]
|
||||||
[HttpGet("/v1/services")]
|
[HttpGet("/v1/services")]
|
||||||
public ActionResult<Dictionary<string, string>> 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
|
// Ping / health used by the client keep-alive loop
|
||||||
[HttpGet("/v1/ping")]
|
[HttpGet("/v1/ping")]
|
||||||
|
|||||||
@@ -180,8 +180,13 @@ public static class LegacyUpgrades
|
|||||||
// either no-op (Crescendo now exists) or the only writer.
|
// either no-op (Crescendo now exists) or the only writer.
|
||||||
for (var attempt = 1; attempt <= 2; attempt++)
|
for (var attempt = 1; attempt <= 2; attempt++)
|
||||||
{
|
{
|
||||||
var crescendoExists = await db.Rooms.AnyAsync(r => r.Name == "Crescendo", ct);
|
var renameCandidates = await db.Rooms
|
||||||
var bloodMoon = await db.Rooms.FirstOrDefaultAsync(r => r.Name == "BloodMoon", ct);
|
.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)
|
if (bloodMoon is null && crescendoExists)
|
||||||
{
|
{
|
||||||
@@ -217,15 +222,15 @@ public static class LegacyUpgrades
|
|||||||
{
|
{
|
||||||
// Both exist — the rename ran on a different row at
|
// Both exist — the rename ran on a different row at
|
||||||
// some point, and the original BloodMoon row is now
|
// some point, and the original BloodMoon row is now
|
||||||
// a stale duplicate. Park it under CrescendoLegacy
|
// a stale duplicate. Park it under a unique legacy
|
||||||
// and hide from browse so /goto/name/... still
|
// name and hide from browse so /goto/name/... still
|
||||||
// resolves Crescendo to the right row.
|
// resolves Crescendo to the right row.
|
||||||
bloodMoon.Name = "CrescendoLegacy";
|
bloodMoon.Name = await PickUniqueRoomNameAsync(db, "CrescendoLegacy", bloodMoon.Id, ct);
|
||||||
bloodMoon.HiddenFromBrowse = true;
|
bloodMoon.HiddenFromBrowse = true;
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"[legacy-upgrade] RenameBloodMoonToCrescendo: applied — Rooms.Id={Id} parked as CrescendoLegacy (Crescendo already existed on a different row)",
|
"[legacy-upgrade] RenameBloodMoonToCrescendo: applied — Rooms.Id={Id} parked as {Name} (Crescendo already existed on a different row)",
|
||||||
bloodMoon.Id);
|
bloodMoon.Id, bloodMoon.Name);
|
||||||
return;
|
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>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DorkNet.Models\DorkNet.Models.csproj" />
|
<ProjectReference Include="..\DorkNet.Models\DorkNet.Models.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Service-slice containers reference DorkNet.Server for shared controllers
|
<!-- Service-slice containers reference DorkNet.Server for shared controllers
|
||||||
|
|||||||
@@ -25,9 +25,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 = $"https://{domain.Sub("cdn")}",
|
CdnBaseUri = domain.Url("cdn"),
|
||||||
PhotonConfig = photon,
|
PhotonConfig = photon,
|
||||||
ServiceUrls = BuildServiceUrlMap(apex),
|
ServiceUrls = BuildServiceUrlMap(domain),
|
||||||
ConfigTable = new Dictionary<string, string>
|
ConfigTable = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
// Season keys the 2019/2020 client checks at startup
|
// 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
|
/// <see cref="DomainConfig.Apex"/>) so the map stays a single
|
||||||
/// source of truth.</summary>
|
/// source of truth.</summary>
|
||||||
public static Dictionary<string, string> BuildServiceUrlMap(string apex)
|
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);
|
var map = new Dictionary<string, string>(ServiceSubdomains.Length);
|
||||||
foreach (var (service, sub) in ServiceSubdomains)
|
foreach (var (service, sub) in ServiceSubdomains)
|
||||||
{
|
{
|
||||||
var host = string.IsNullOrEmpty(sub) ? apex : $"{sub}.{apex}";
|
map[service] = domain.Url(sub);
|
||||||
map[service] = $"https://{host}";
|
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,28 @@ namespace DorkNet.Server.Services;
|
|||||||
public sealed class DomainConfig
|
public sealed class DomainConfig
|
||||||
{
|
{
|
||||||
public string Apex { get; }
|
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 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)
|
public static bool MatchesSubdomain(string host, string prefix)
|
||||||
=> host.StartsWith(prefix + ".", StringComparison.OrdinalIgnoreCase);
|
=> 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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,12 @@ public class GameSessionService(DorkNetDbContext db)
|
|||||||
// in, so we save once with a placeholder, then patch and save
|
// in, so we save once with a placeholder, then patch and save
|
||||||
// again. Two SaveChangesAsync calls is cheap at our QPS.
|
// again. Two SaveChangesAsync calls is cheap at our QPS.
|
||||||
PhotonRoomName = "pending",
|
PhotonRoomName = "pending",
|
||||||
MaxCapacity = 8,
|
// Track the room's "Max Player Count in This Subroom" slider
|
||||||
|
// instead of a hardcoded 8, so matchmade joiners fill the room to
|
||||||
|
// the host's chosen limit rather than fragmenting into a new
|
||||||
|
// instance every 8 players. (Existing sessions keep whatever cap
|
||||||
|
// they were created with.)
|
||||||
|
MaxCapacity = await ResolveRoomMaxCapacityAsync(effectiveRoom),
|
||||||
PlayerCount = 1,
|
PlayerCount = 1,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
};
|
};
|
||||||
@@ -75,6 +80,28 @@ public class GameSessionService(DorkNetDbContext db)
|
|||||||
return ToDto(session);
|
return ToDto(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default session cap when we can't resolve a room-specific value (system
|
||||||
|
// rooms like the dorm use a GUID roomId that doesn't map to a RoomEntity).
|
||||||
|
private const int DefaultMaxCapacity = 8;
|
||||||
|
|
||||||
|
/// <summary>Resolve a new session's capacity from the room's entry
|
||||||
|
/// subroom (lowest <c>OrderIndex</c>) <see cref="RoomSceneEntity.MaxPlayers"/>
|
||||||
|
/// — the value the in-game "Max Player Count in This Subroom" slider
|
||||||
|
/// writes. Matchmade sessions land in the entry subroom, so its cap is the
|
||||||
|
/// right ceiling. GameSession roomIds are the room's numeric Id as a
|
||||||
|
/// string for user rooms; non-numeric (dorm GUID, etc.) → default. Clamped
|
||||||
|
/// to a sane Photon ceiling.</summary>
|
||||||
|
private async Task<int> ResolveRoomMaxCapacityAsync(string roomId)
|
||||||
|
{
|
||||||
|
if (!long.TryParse(roomId, out var roomLongId)) return DefaultMaxCapacity;
|
||||||
|
var max = await db.RoomScenes
|
||||||
|
.Where(s => s.RoomId == roomLongId)
|
||||||
|
.OrderBy(s => s.OrderIndex)
|
||||||
|
.Select(s => (int?)s.MaxPlayers)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
return max is int v && v > 0 ? Math.Clamp(v, 1, 80) : DefaultMaxCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<GameSession?> GetByIdAsync(long id)
|
public async Task<GameSession?> GetByIdAsync(long id)
|
||||||
{
|
{
|
||||||
var s = await db.GameSessions.AsNoTracking()
|
var s = await db.GameSessions.AsNoTracking()
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ public static class MiddlewarePipelineExtensions
|
|||||||
if (ctx.Request.Host.Host.Equals(domainCfg.Sub("api"), StringComparison.OrdinalIgnoreCase) &&
|
if (ctx.Request.Host.Host.Equals(domainCfg.Sub("api"), StringComparison.OrdinalIgnoreCase) &&
|
||||||
(ctx.Request.Path == "/admin" || ctx.Request.Path.StartsWithSegments("/admin")))
|
(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;
|
return;
|
||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using DorkNet.Contracts;
|
||||||
using DorkNet.Server.Compat;
|
using DorkNet.Server.Compat;
|
||||||
using DorkNet.Server.Data;
|
using DorkNet.Server.Data;
|
||||||
using DorkNet.Server.Services;
|
using DorkNet.Server.Services;
|
||||||
@@ -106,14 +107,19 @@ public static class ServiceCollectionExtensions
|
|||||||
builder.Configuration["Domain:Apex"]
|
builder.Configuration["Domain:Apex"]
|
||||||
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
|
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
|
||||||
?? "localhost";
|
?? "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 =>
|
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.AllowEmptyHosts = true;
|
||||||
opt.IncludeFailureMessage = true;
|
opt.IncludeFailureMessage = true;
|
||||||
});
|
});
|
||||||
Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{apex}, *.{apex}, localhost]");
|
Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{string.Join(", ", allowedHosts)}]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Services ──────────────────────────────────────────────────────────────
|
// ── Services ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ x-dorknet-service-env: &dorknet-service-env
|
|||||||
|
|
||||||
x-dorknet-app-networks: &dorknet-app-networks
|
x-dorknet-app-networks: &dorknet-app-networks
|
||||||
networks:
|
networks:
|
||||||
|
# Internal only — backends are reached by the gateway over `default`,
|
||||||
|
# never directly by Traefik, so keep them off dokploy-network.
|
||||||
- default
|
- default
|
||||||
- dokploy-network
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
gateway:
|
gateway:
|
||||||
@@ -42,6 +43,18 @@ services:
|
|||||||
DorkNet__Services__Monolith: http://monolith:8080
|
DorkNet__Services__Monolith: http://monolith:8080
|
||||||
expose:
|
expose:
|
||||||
- "8080"
|
- "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:
|
depends_on:
|
||||||
- identity
|
- identity
|
||||||
- rooms
|
- rooms
|
||||||
@@ -57,6 +70,10 @@ services:
|
|||||||
cloudflared:
|
cloudflared:
|
||||||
image: cloudflare/cloudflared:latest
|
image: cloudflare/cloudflared:latest
|
||||||
command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN:?Set CLOUDFLARE_TUNNEL_TOKEN in Dokploy}
|
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:
|
depends_on:
|
||||||
- gateway
|
- gateway
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# DorkNet Cloudflare Tunnel config — Coolify production target.
|
# DorkNet Cloudflare Tunnel config — Coolify production target.
|
||||||
# ────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────
|
||||||
# Routes every localhost subdomain through the tunnel into the server
|
# Routes only DorkNet's known service subdomains through the tunnel
|
||||||
# container running on Coolify. The tunnel runs as its own Coolify
|
# 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
|
# service (or as a sidecar in the same project) and reaches the
|
||||||
# server over the internal Docker network.
|
# server over the internal Docker network.
|
||||||
#
|
#
|
||||||
@@ -49,24 +49,93 @@ originRequest:
|
|||||||
keepAliveTimeout: 10m
|
keepAliveTimeout: 10m
|
||||||
noHappyEyeballs: false
|
noHappyEyeballs: false
|
||||||
|
|
||||||
# Ingress rules are evaluated top-down. The wildcard catches every
|
# Ingress rules are evaluated top-down. Replace localhost below with
|
||||||
# subdomain we don't list explicitly, so when the 2020 Rec Room client
|
# your DORKNET_DOMAIN when installing this as a static cloudflared
|
||||||
# (post-domain-patch) hits a niche subdomain like roomieintegrations.
|
# config. Do not use a wildcard hostname: only these known client-facing
|
||||||
# localhost, the request still reaches the server.
|
# hosts should reach the gateway.
|
||||||
ingress:
|
ingress:
|
||||||
- hostname: localhost
|
- hostname: localhost
|
||||||
service: {{UPSTREAM}}
|
service: {{UPSTREAM}}
|
||||||
originRequest:
|
originRequest:
|
||||||
httpHostHeader: localhost
|
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}}
|
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
|
# Catch-all rejects anything that didn't match (defensive — the
|
||||||
# tunnel only routes traffic for hostnames we've added DNS records
|
# tunnel only routes traffic for hostnames listed above).
|
||||||
# for, so this is rarely reached).
|
|
||||||
- service: http_status:404
|
- service: http_status:404
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# DorkNet Cloudflare Tunnel config
|
# DorkNet Cloudflare Tunnel config
|
||||||
# ────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────
|
||||||
# Routes every localhost subdomain through the tunnel into the local
|
# Routes only DorkNet's known localhost service subdomains through the
|
||||||
# server (HTTPS on port 443). Generated by tools/setup-cloudflared.ps1
|
# tunnel into the local server (HTTPS on port 443). Generated by tools/setup-cloudflared.ps1
|
||||||
# — copy to %USERPROFILE%\.cloudflared\config.yml and replace the
|
# — copy to %USERPROFILE%\.cloudflared\config.yml and replace the
|
||||||
# {{TUNNEL_ID}} / {{CREDENTIALS_FILE}} placeholders.
|
# {{TUNNEL_ID}} / {{CREDENTIALS_FILE}} placeholders.
|
||||||
#
|
#
|
||||||
@@ -14,27 +14,96 @@
|
|||||||
tunnel: {{TUNNEL_ID}}
|
tunnel: {{TUNNEL_ID}}
|
||||||
credentials-file: {{CREDENTIALS_FILE}}
|
credentials-file: {{CREDENTIALS_FILE}}
|
||||||
|
|
||||||
# Ingress rules are evaluated top-down. The wildcard catches every
|
originRequest:
|
||||||
# subdomain we don't list explicitly, so when the 2020 Rec Room client
|
noTLSVerify: true
|
||||||
# (post-domain-patch) hits a niche subdomain like roomieintegrations.
|
|
||||||
# localhost, the request still reaches the server.
|
# 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:
|
ingress:
|
||||||
- hostname: localhost
|
- hostname: localhost
|
||||||
service: https://localhost:443
|
service: https://localhost:443
|
||||||
originRequest:
|
originRequest:
|
||||||
noTLSVerify: true
|
|
||||||
httpHostHeader: localhost
|
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
|
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
|
# Catch-all rejects anything that didn't match (defensive — the
|
||||||
# tunnel only routes traffic for hostnames we've added DNS records
|
# tunnel only routes traffic for hostnames listed above).
|
||||||
# for, so this is rarely reached).
|
|
||||||
- service: http_status:404
|
- service: http_status:404
|
||||||
|
|||||||
@@ -78,19 +78,15 @@ $Template = Join-Path $PSScriptRoot 'cloudflared-config.yml.template'
|
|||||||
$CfDir = Join-Path $env:USERPROFILE '.cloudflared'
|
$CfDir = Join-Path $env:USERPROFILE '.cloudflared'
|
||||||
$ConfigPath = Join-Path $CfDir 'config.yml'
|
$ConfigPath = Join-Path $CfDir 'config.yml'
|
||||||
|
|
||||||
# Subdomains we want routed through the tunnel. The wildcard route in
|
# Subdomains routed through the tunnel. Keep this list aligned with
|
||||||
# config.yml catches everything; this list drives the per-subdomain
|
# DorkNetRouteOwnership.PublicSubdomains and the cloudflared config
|
||||||
# DNS CNAMEs Cloudflare needs (no automatic wildcard CNAME via the
|
# template. We intentionally create explicit DNS routes instead of a
|
||||||
# CLI — each one is created explicitly via `cloudflared tunnel route
|
# wildcard so unrelated subdomains do not reach the gateway.
|
||||||
# dns`).
|
|
||||||
$Subdomains = @(
|
$Subdomains = @(
|
||||||
'@', # apex (localhost itself)
|
'@', # apex (localhost itself)
|
||||||
'admin', 'api', 'accounts', 'auth', 'cdn', 'chat', 'commerce',
|
'admin', 'api', 'accounts', 'auth', 'cdn', 'chat', 'commerce',
|
||||||
'discovery', 'econ', 'feed', 'geo', 'img', 'match', 'notify',
|
'discovery', 'econ', 'feed', 'geo', 'img', 'match', 'notify',
|
||||||
'ns', 'playersettings', 'rooms', 'storage', 'strings', 'strings-cdn',
|
'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',
|
'bugreporting', 'cards', 'clubs', 'cms', 'data', 'datacollection',
|
||||||
'gamelogs', 'leaderboard', 'link', 'lists', 'moderation',
|
'gamelogs', 'leaderboard', 'link', 'lists', 'moderation',
|
||||||
'platformnotifications', 'roomcomments', 'roomieintegrations',
|
'platformnotifications', 'roomcomments', 'roomieintegrations',
|
||||||
|
|||||||
Reference in New Issue
Block a user