Split remaining service routes

This commit is contained in:
Alexa
2026-06-15 20:21:05 +03:00
parent bd4ceada65
commit 44f53e80b8
51 changed files with 2271 additions and 122 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ tmp/
## OS ## OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
.backups
## Reverse-engineered artifacts (Il2CppDumper / Cpp2IL output, not for commit) ## Reverse-engineered artifacts (Il2CppDumper / Cpp2IL output, not for commit)
dump.cs dump.cs
il2cpp.h il2cpp.h
+1 -1
View File
@@ -5,7 +5,7 @@ ARG PROJECT_PATH
WORKDIR /src WORKDIR /src
COPY . . COPY . .
RUN dotnet publish "${PROJECT_PATH}" -c Release -o /app/publish RUN dotnet publish "${PROJECT_PATH}" -c Release -p:SkipAdminUIBuild=true -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION} AS runtime FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION} AS runtime
ARG APP_DLL ARG APP_DLL
+298
View File
@@ -0,0 +1,298 @@
namespace DorkNet.Contracts;
public static class DorkNetRouteOwnership
{
private const string AdminHost = "admin";
private static readonly DorkNetRouteGroup[] ServiceRouteGroups =
[
new(
ServiceNames.Identity,
["auth", "accounts"],
[
"/.well-known",
"/account",
"/api/account",
"/api/auth",
"/api/createaccount",
"/api/platformlogin",
"/api/playerreputation",
"/api/players",
"/api/relationships",
"/api/settings",
"/cachedlogin",
"/connect",
"/eac",
"/parentalcontrol",
"/photon",
"/profileimage",
"/settings",
]),
new(
ServiceNames.Rooms,
["rooms", "match", "discovery", "lists", "roomcomments", "roomieintegrations", "studio"],
[
"/api/activities",
"/api/curatedroomplaylists",
"/api/gameconfigs",
"/api/gameconfigurations",
"/api/gamesessions",
"/api/leaderboard",
"/api/playerelo",
"/api/playlists",
"/api/roomkeys",
"/api/rooms",
"/api/royale",
"/discover",
"/featuredrooms",
"/goto",
"/hot_rooms",
"/hot_roomsandplaylists",
"/invite",
"/playlists",
"/player",
"/room",
"/roominstance",
"/rooms",
"/roomsandplaylists",
"/roomserver",
"/search_rooms",
"/search_roomsandplaylists",
"/v1/find",
"/v1/join",
"/v1/leave",
"/v1/session",
]),
new(
ServiceNames.Notify,
["notify", "chat", "platformnotifications"],
[
"/api/messages",
"/api/notification",
"/api/offlineinvite",
"/hub",
"/notify",
"/thread",
]),
new(
ServiceNames.Content,
["cdn", "data", "img", "storage", "strings-cdn", "videos"],
[
"/api/images",
"/api/photos",
"/config",
"/data",
"/img",
"/invention",
"/upload",
"/video",
]),
new(
ServiceNames.Social,
["clubs"],
[
"/announcements",
"/api/announcement",
"/api/clubreporting",
"/api/communityboard",
"/api/groups",
"/api/playercheer",
"/api/playerevents",
"/api/playersubscriptions",
"/club",
"/members",
"/subscription",
]),
new(
ServiceNames.Commerce,
["commerce", "econ"],
[
"/api/avatar",
"/api/catalog",
"/api/compatibility",
"/api/consumables",
"/api/equipment",
"/api/gamerewards",
"/api/inventions",
"/api/purchase",
"/api/storefronts",
"/commerce",
"/econ",
]),
new(
ServiceNames.Platform,
["cards", "cms", "datacollection", "gamelogs", "geo", "leaderboard", "link", "ns", "playersettings", "strings", "thorn"],
[
"/api/cards",
"/api/config",
"/api/events",
"/api/quickplay",
"/api/role",
"/api/version",
"/api/versioncheck",
"/motd",
"/pageview",
"/ping",
"/role",
"/strings",
"/v1/ping",
"/v1/player",
"/v1/regions",
"/v1/services",
]),
new(
ServiceNames.Moderation,
["bugreporting", "moderation"],
[
"/api/admin",
"/api/banappeal",
"/api/bugreporting",
"/api/moderation",
"/api/playerreporting",
"/api/playersbanned",
"/api/sanitize",
"/api/testcasemanagement",
]),
new(
ServiceNames.Web,
["www", AdminHost, "feed"],
[
"/api/site",
],
IncludesApexHost: true),
];
public static IReadOnlyList<DorkNetRouteGroup> RouteGroups => ServiceRouteGroups;
public static bool IsInfrastructurePath(string path)
{
return path.Equals("/healthz", StringComparison.OrdinalIgnoreCase)
|| path.StartsWith("/internal/", StringComparison.OrdinalIgnoreCase);
}
public static string ResolvePublicService(string? host, string path, string apexDomain)
{
var normalizedPath = NormalizePath(path);
var subdomain = Subdomain(host, apexDomain);
if (string.Equals(subdomain, AdminHost, StringComparison.OrdinalIgnoreCase))
{
return ServiceNames.Web;
}
foreach (var group in ServiceRouteGroups.Where(group => group.ServiceName != ServiceNames.Web))
{
if (MatchesHost(subdomain, group.HostSubdomains))
{
return group.ServiceName;
}
}
foreach (var group in ServiceRouteGroups)
{
if (StartsWithAny(normalizedPath, group.PathPrefixes))
{
return group.ServiceName;
}
}
var web = ServiceRouteGroups.First(group => group.ServiceName == ServiceNames.Web);
if ((subdomain is null && web.IncludesApexHost) || MatchesHost(subdomain, web.HostSubdomains))
{
return ServiceNames.Web;
}
return ServiceNames.Monolith;
}
public static bool IsOwnedBy(string serviceName, string? host, string path, string apexDomain)
{
if (IsInfrastructurePath(path))
{
return true;
}
return string.Equals(
ResolvePublicService(host, path, apexDomain),
serviceName,
StringComparison.OrdinalIgnoreCase);
}
public static string? Subdomain(string? host, string apexDomain)
{
if (string.IsNullOrWhiteSpace(host))
{
return null;
}
var normalizedHost = host.Split(':', 2)[0].Trim().ToLowerInvariant();
var normalizedApex = apexDomain.Split(':', 2)[0].Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(normalizedHost)
|| string.IsNullOrWhiteSpace(normalizedApex)
|| string.Equals(normalizedHost, normalizedApex, StringComparison.OrdinalIgnoreCase))
{
return null;
}
var suffix = "." + normalizedApex;
if (normalizedHost.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
{
return normalizedHost[..^suffix.Length];
}
var dot = normalizedHost.IndexOf('.');
return dot > 0 ? normalizedHost[..dot] : null;
}
private static bool StartsWithAny(string path, IEnumerable<string> prefixes)
{
foreach (var prefix in prefixes)
{
if (StartsWithPathSegment(path, prefix))
{
return true;
}
}
return false;
}
private static bool StartsWithPathSegment(string path, string prefix)
{
var normalizedPath = NormalizePath(path);
var normalizedPrefix = NormalizePath(prefix).TrimEnd('/');
if (string.IsNullOrWhiteSpace(normalizedPrefix))
{
normalizedPrefix = "/";
}
return normalizedPath.Equals(normalizedPrefix, StringComparison.OrdinalIgnoreCase)
|| normalizedPath.StartsWith(normalizedPrefix + "/", StringComparison.OrdinalIgnoreCase);
}
private static bool MatchesHost(string? subdomain, IEnumerable<string> candidates)
{
if (string.IsNullOrWhiteSpace(subdomain))
{
return false;
}
return candidates.Contains(subdomain, StringComparer.OrdinalIgnoreCase);
}
private static string NormalizePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return "/";
}
return path[0] == '/' ? path : "/" + path;
}
}
public sealed record DorkNetRouteGroup(
string ServiceName,
string[] HostSubdomains,
string[] PathPrefixes,
bool IncludesApexHost = false);
+13 -1
View File
@@ -5,7 +5,13 @@ public sealed class DorkNetServiceMapOptions
public string Identity { get; set; } = "http://localhost:8081"; public string Identity { get; set; } = "http://localhost:8081";
public string Rooms { get; set; } = "http://localhost:8082"; public string Rooms { get; set; } = "http://localhost:8082";
public string Notify { get; set; } = "http://localhost:8083"; public string Notify { get; set; } = "http://localhost:8083";
public string Monolith { get; set; } = "http://localhost:8084"; public string Content { get; set; } = "http://localhost:8084";
public string Social { get; set; } = "http://localhost:8085";
public string Commerce { get; set; } = "http://localhost:8086";
public string Platform { get; set; } = "http://localhost:8087";
public string Moderation { get; set; } = "http://localhost:8088";
public string Web { get; set; } = "http://localhost:8089";
public string Monolith { get; set; } = "http://localhost:8090";
public IReadOnlyList<DorkNetServiceEndpoint> Endpoints() public IReadOnlyList<DorkNetServiceEndpoint> Endpoints()
{ {
@@ -14,6 +20,12 @@ public sealed class DorkNetServiceMapOptions
new(ServiceNames.Identity, Identity), new(ServiceNames.Identity, Identity),
new(ServiceNames.Rooms, Rooms), new(ServiceNames.Rooms, Rooms),
new(ServiceNames.Notify, Notify), new(ServiceNames.Notify, Notify),
new(ServiceNames.Content, Content),
new(ServiceNames.Social, Social),
new(ServiceNames.Commerce, Commerce),
new(ServiceNames.Platform, Platform),
new(ServiceNames.Moderation, Moderation),
new(ServiceNames.Web, Web),
new(ServiceNames.Monolith, Monolith), new(ServiceNames.Monolith, Monolith),
]; ];
+6
View File
@@ -6,5 +6,11 @@ public static class ServiceNames
public const string Identity = "identity"; public const string Identity = "identity";
public const string Rooms = "rooms"; public const string Rooms = "rooms";
public const string Notify = "notify"; public const string Notify = "notify";
public const string Content = "content";
public const string Social = "social";
public const string Commerce = "commerce";
public const string Platform = "platform";
public const string Moderation = "moderation";
public const string Web = "web";
public const string Monolith = "monolith"; public const string Monolith = "monolith";
} }
+4
View File
@@ -6,6 +6,10 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" /> <ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" /> <ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
+230
View File
@@ -1,6 +1,7 @@
using DorkNet.Contracts; using DorkNet.Contracts;
using DorkNet.ServiceDefaults; using DorkNet.ServiceDefaults;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Yarp.ReverseProxy.Configuration;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json", builder.Configuration.AddJsonFile("appsettings.Local.json",
@@ -9,6 +10,17 @@ builder.Configuration.AddJsonFile("appsettings.Local.json",
builder.AddDorkNetServiceDefaults(ServiceNames.Gateway); builder.AddDorkNetServiceDefaults(ServiceNames.Gateway);
builder.Services.Configure<DorkNetServiceMapOptions>( builder.Services.Configure<DorkNetServiceMapOptions>(
builder.Configuration.GetSection("DorkNet:Services")); builder.Configuration.GetSection("DorkNet:Services"));
var serviceMap = builder.Configuration
.GetSection("DorkNet:Services")
.Get<DorkNetServiceMapOptions>() ?? new DorkNetServiceMapOptions();
var apexDomain = builder.Configuration["Domain:Apex"]
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
?? "localhost";
builder.Services
.AddReverseProxy()
.LoadFromMemory(
BuildRoutes(apexDomain, serviceMap),
BuildClusters(serviceMap));
var app = builder.Build(); var app = builder.Build();
@@ -17,6 +29,19 @@ app.MapDorkNetServiceDefaults();
app.MapGet("/", () => Results.Redirect("/healthz")); app.MapGet("/", () => Results.Redirect("/healthz"));
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", () =>
Results.Ok(new
{
ApexDomain = apexDomain,
Routes = BuildRoutes(apexDomain, serviceMap).Select(r => new
{
r.RouteId,
r.ClusterId,
r.Match.Hosts,
r.Match.Path,
r.Order,
}),
}));
app.MapGet("/internal/services/health", async ( app.MapGet("/internal/services/health", async (
IOptions<DorkNetServiceMapOptions> options, IOptions<DorkNetServiceMapOptions> options,
@@ -34,6 +59,8 @@ app.MapGet("/internal/services/health", async (
return Results.Ok(probes); return Results.Ok(probes);
}); });
app.MapReverseProxy();
app.Run(); app.Run();
static async Task<ServiceProbeResponse> ProbeServiceAsync( static async Task<ServiceProbeResponse> ProbeServiceAsync(
@@ -66,3 +93,206 @@ static async Task<ServiceProbeResponse> ProbeServiceAsync(
checkedAt); checkedAt);
} }
} }
static IReadOnlyList<RouteConfig> BuildRoutes(
string apexDomain,
DorkNetServiceMapOptions services)
{
var routes = new List<RouteConfig>();
var order = 0;
var webGroup = DorkNetRouteOwnership.RouteGroups
.First(group => group.ServiceName == ServiceNames.Web);
AddHostRoute(
routes,
ref order,
webGroup.ServiceName,
BaseUrlFor(services, webGroup.ServiceName),
"web-admin-host",
[SubdomainHost("admin", apexDomain)]);
foreach (var group in DorkNetRouteOwnership.RouteGroups.Where(group => group.ServiceName != ServiceNames.Web))
{
AddHostRoute(
routes,
ref order,
group.ServiceName,
BaseUrlFor(services, group.ServiceName),
$"{group.ServiceName}-hosts",
group.HostSubdomains.Select(subdomain => SubdomainHost(subdomain, apexDomain)).ToArray());
}
foreach (var group in DorkNetRouteOwnership.RouteGroups)
{
AddPathRoutes(
routes,
ref order,
group.ServiceName,
BaseUrlFor(services, group.ServiceName),
group.PathPrefixes);
}
AddHostRoute(
routes,
ref order,
webGroup.ServiceName,
BaseUrlFor(services, webGroup.ServiceName),
"web-hosts",
BuildWebHosts(webGroup, apexDomain));
if (!string.IsNullOrWhiteSpace(services.Monolith))
{
routes.Add(Route(ServiceNames.Monolith, "monolith-fallback", "/{**catch-all}", order++));
}
return routes;
}
static IReadOnlyList<ClusterConfig> BuildClusters(DorkNetServiceMapOptions services)
{
var clusters = new List<ClusterConfig>();
foreach (var endpoint in services.Endpoints())
{
AddCluster(clusters, endpoint.Name, endpoint.BaseUrl);
}
return clusters;
}
static void AddHostRoute(
List<RouteConfig> routes,
ref int order,
string serviceName,
string baseUrl,
string routeId,
string[] hosts)
{
if (string.IsNullOrWhiteSpace(baseUrl) || hosts.Length == 0)
{
return;
}
routes.Add(Route(
serviceName,
routeId,
"/{**catch-all}",
order++,
hosts));
}
static void AddPathRoutes(
List<RouteConfig> routes,
ref int order,
string serviceName,
string baseUrl,
IEnumerable<string> paths)
{
if (string.IsNullOrWhiteSpace(baseUrl))
{
return;
}
foreach (var path in paths)
{
var routeId = $"{serviceName}-{path.Trim('/').Replace('/', '-').Replace("{**catch-all}", "all")}";
var exactPath = NormalizeRoutePrefix(path);
routes.Add(Route(serviceName, $"{routeId}-exact", exactPath, order++));
routes.Add(Route(serviceName, routeId, $"{exactPath}/{{**catch-all}}", order++));
}
}
static string BaseUrlFor(DorkNetServiceMapOptions services, string serviceName)
{
return serviceName switch
{
ServiceNames.Identity => services.Identity,
ServiceNames.Rooms => services.Rooms,
ServiceNames.Notify => services.Notify,
ServiceNames.Content => services.Content,
ServiceNames.Social => services.Social,
ServiceNames.Commerce => services.Commerce,
ServiceNames.Platform => services.Platform,
ServiceNames.Moderation => services.Moderation,
ServiceNames.Web => services.Web,
ServiceNames.Monolith => services.Monolith,
_ => string.Empty,
};
}
static string[] BuildWebHosts(DorkNetRouteGroup group, string apexDomain)
{
var hosts = new List<string>();
if (group.IncludesApexHost)
{
hosts.Add(apexDomain);
}
hosts.AddRange(group.HostSubdomains
.Where(subdomain => !string.Equals(subdomain, "admin", StringComparison.OrdinalIgnoreCase))
.Select(subdomain => SubdomainHost(subdomain, apexDomain)));
return hosts.ToArray();
}
static string SubdomainHost(string subdomain, string apexDomain)
{
return $"{subdomain}.{apexDomain}";
}
static string NormalizeRoutePrefix(string path)
{
if (string.IsNullOrWhiteSpace(path) || path == "/")
{
return "/";
}
return (path[0] == '/' ? path : "/" + path).TrimEnd('/');
}
static RouteConfig Route(
string clusterId,
string routeId,
string path,
int order,
string[]? hosts = null)
{
return new RouteConfig
{
RouteId = routeId,
ClusterId = clusterId,
Order = order,
Match = new RouteMatch
{
Hosts = hosts,
Path = path,
},
Transforms =
[
new Dictionary<string, string>
{
["RequestHeaderOriginalHost"] = "true",
},
],
};
}
static void AddCluster(
List<ClusterConfig> clusters,
string serviceName,
string baseUrl)
{
if (string.IsNullOrWhiteSpace(baseUrl))
{
return;
}
var address = baseUrl.EndsWith('/') ? baseUrl : baseUrl + "/";
clusters.Add(new ClusterConfig
{
ClusterId = serviceName,
Destinations = new Dictionary<string, DestinationConfig>
{
["primary"] = new() { Address = address },
},
});
}
+7 -1
View File
@@ -11,7 +11,13 @@
"Identity": "http://localhost:8081", "Identity": "http://localhost:8081",
"Rooms": "http://localhost:8082", "Rooms": "http://localhost:8082",
"Notify": "http://localhost:8083", "Notify": "http://localhost:8083",
"Monolith": "http://localhost:8084" "Content": "http://localhost:8084",
"Social": "http://localhost:8085",
"Commerce": "http://localhost:8086",
"Platform": "http://localhost:8087",
"Moderation": "http://localhost:8088",
"Web": "http://localhost:8089",
"Monolith": "http://localhost:8090"
} }
} }
} }
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
@@ -0,0 +1,90 @@
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HostFiltering;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
namespace DorkNet.Server.Tests;
public sealed class DorkNetServerFactory : WebApplicationFactory<Program>
{
private const string JwtSecret = "endpoint-contract-secret-000000000000000000000000000000000000000000";
private readonly string _dataRoot;
public DorkNetServerFactory()
{
_dataRoot = Path.Combine(Path.GetTempPath(), "dorknet-server-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_dataRoot);
}
public string ApexDomain => "dork.test";
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Development");
builder.UseSetting("Domain:Apex", ApexDomain);
builder.ConfigureAppConfiguration((_, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string?>
{
["AllowedHosts"] = "*",
["Database:Provider"] = "sqlite",
["Database:SqlitePath"] = Path.Combine(_dataRoot, "dorknet-test.db"),
["Domain:Apex"] = ApexDomain,
["Jwt:Secret"] = JwtSecret,
["S3:Endpoint"] = "",
["S3:AccessKey"] = "",
["S3:SecretKey"] = "",
["DorkNet:DefaultClientVersion"] = "december_2020_12_18",
["DorkNet:SupportedVersions:0"] = "december_2020_12_18",
});
});
builder.ConfigureServices(services =>
{
services.PostConfigure<HostFilteringOptions>(options =>
{
options.AllowedHosts =
[
ApexDomain,
$"*.{ApexDomain}",
"localhost",
"127.0.0.1",
];
options.AllowEmptyHosts = true;
options.IncludeFailureMessage = true;
});
services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret)),
ValidateIssuer = true,
ValidIssuer = "https://api.rec.net/",
ValidateAudience = false,
ClockSkew = TimeSpan.Zero,
};
});
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
try
{
if (Directory.Exists(_dataRoot))
{
Directory.Delete(_dataRoot, recursive: true);
}
}
catch
{
// Test cleanup must not hide the original test failure.
}
}
}
@@ -0,0 +1,222 @@
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
namespace DorkNet.Server.Tests;
public static partial class EndpointContractDiscovery
{
public static IReadOnlyList<EndpointContract> Discover()
{
var serverAssembly = typeof(Program).Assembly;
var controllerBaseType = typeof(ControllerBase);
return serverAssembly.GetTypes()
.Where(t => !t.IsAbstract && controllerBaseType.IsAssignableFrom(t))
.SelectMany(DiscoverControllerRoutes)
.OrderBy(route => route.Method, StringComparer.Ordinal)
.ThenBy(route => route.SamplePath, StringComparer.Ordinal)
.ThenBy(route => route.Source, StringComparer.Ordinal)
.ToArray();
}
private static IEnumerable<EndpointContract> DiscoverControllerRoutes(Type controller)
{
var controllerName = ControllerName(controller);
var controllerRouteTemplates = controller.GetCustomAttributes(inherit: true)
.OfType<IRouteTemplateProvider>()
.Select(a => a.Template)
.Where(t => !string.IsNullOrWhiteSpace(t))
.Cast<string>()
.DefaultIfEmpty("")
.ToArray();
foreach (var method in controller.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
{
var httpAttrs = method.GetCustomAttributes(inherit: true)
.OfType<HttpMethodAttribute>()
.ToArray();
if (httpAttrs.Length == 0)
{
continue;
}
foreach (var attr in httpAttrs)
{
var attrMethods = attr.HttpMethods.Any()
? attr.HttpMethods
: new[] { "ANY" };
foreach (var httpMethod in attrMethods)
{
foreach (var controllerTemplate in controllerRouteTemplates)
{
var template = CombineTemplates(controllerTemplate, attr.Template);
template = ReplaceRouteTokens(template, controllerName, method.Name);
yield return new EndpointContract(
httpMethod.ToUpperInvariant(),
template,
ToSamplePath(template),
$"{controller.FullName}.{method.Name}",
RequiresAuthorization(controller, method),
IsAdminEndpoint(controller, method),
ConsumesContentTypes(controller, method));
}
}
}
}
}
private static bool RequiresAuthorization(Type controller, MethodInfo method)
{
var attributes = controller.GetCustomAttributes(inherit: true)
.Concat(method.GetCustomAttributes(inherit: true))
.ToArray();
if (attributes.OfType<IAllowAnonymous>().Any())
{
return false;
}
return attributes.OfType<IAuthorizeData>().Any();
}
private static bool IsAdminEndpoint(Type controller, MethodInfo method)
{
return controller.FullName?.Contains(".Controllers.Admin.", StringComparison.Ordinal) == true
|| method.Name.Contains("Admin", StringComparison.OrdinalIgnoreCase);
}
private static string[] ConsumesContentTypes(Type controller, MethodInfo method)
{
var consumes = method.GetCustomAttributes(inherit: true)
.Concat(controller.GetCustomAttributes(inherit: true))
.OfType<ConsumesAttribute>()
.SelectMany(a => a.ContentTypes)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
return consumes.Length == 0
? ["application/json"]
: consumes;
}
private static string ControllerName(Type controller)
{
const string suffix = "Controller";
return controller.Name.EndsWith(suffix, StringComparison.Ordinal)
? controller.Name[..^suffix.Length]
: controller.Name;
}
private static string CombineTemplates(string? controllerTemplate, string? actionTemplate)
{
controllerTemplate ??= "";
actionTemplate ??= "";
if (actionTemplate.StartsWith("~/", StringComparison.Ordinal))
{
return "/" + actionTemplate[2..].TrimStart('/');
}
if (actionTemplate.StartsWith("/", StringComparison.Ordinal))
{
return "/" + actionTemplate.TrimStart('/');
}
var left = controllerTemplate.Trim('/');
var right = actionTemplate.Trim('/');
if (string.IsNullOrEmpty(left) && string.IsNullOrEmpty(right))
{
return "/";
}
if (string.IsNullOrEmpty(left))
{
return "/" + right;
}
if (string.IsNullOrEmpty(right))
{
return "/" + left;
}
return "/" + left + "/" + right;
}
private static string ReplaceRouteTokens(string template, string controllerName, string actionName)
{
return template
.Replace("[controller]", controllerName, StringComparison.OrdinalIgnoreCase)
.Replace("[action]", actionName, StringComparison.OrdinalIgnoreCase);
}
private static string ToSamplePath(string template)
{
var path = RouteParameterRegex().Replace(template, match =>
{
var name = match.Groups["name"].Value.ToLowerInvariant();
var constraint = match.Groups["constraint"].Value.ToLowerInvariant();
if (name.Contains("playerid") || name.Contains("accountid") || name == "id")
{
return EndpointContract.TestPlayerToken;
}
if (constraint.Contains("long") || constraint.Contains("int") || name.EndsWith("id"))
{
return "1";
}
if (constraint.Contains("bool"))
{
return "true";
}
if (constraint.Contains("guid"))
{
return "00000000-0000-0000-0000-000000000001";
}
if (name.Contains("roomname"))
{
return "DormRoom";
}
if (name.Contains("locale"))
{
return "en";
}
return "sample";
});
path = Regex.Replace(path, "/+", "/");
return path.StartsWith('/') ? path : "/" + path;
}
[GeneratedRegex(@"\{[*]?(?<name>[^}:=\?]+)(?<constraint>:[^}=\?]+)?(?<optional>\?)?(?<default>=[^}]+)?\}")]
private static partial Regex RouteParameterRegex();
}
public sealed record EndpointContract(
string Method,
string Template,
string SamplePath,
string Source,
bool RequiresAuthorization,
bool IsAdminEndpoint,
string[] Consumes)
{
public const string TestPlayerToken = "__test_player_id__";
public string PathFor(GameClientSession session)
{
return SamplePath.Replace(
TestPlayerToken,
session.PlayerId.ToString(),
StringComparison.Ordinal);
}
}
@@ -0,0 +1,411 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace DorkNet.Server.Tests;
public sealed class GameClientEndpointContractTests :
IClassFixture<DorkNetServerFactory>
{
private readonly DorkNetServerFactory _factory;
public GameClientEndpointContractTests(DorkNetServerFactory factory)
{
_factory = factory;
}
[Fact]
public async Task Game_client_contract_probe_reaches_every_controller_route()
{
var routes = EndpointContractDiscovery.Discover();
Assert.NotEmpty(routes);
using var client = _factory.CreateClient(new()
{
AllowAutoRedirect = false,
});
client.BaseAddress = new Uri($"http://api.{_factory.ApexDomain}");
client.Timeout = TimeSpan.FromSeconds(20);
var session = await GameClientSessionFactory.CreateAsync(client, _factory.ApexDomain);
var failures = new List<string>();
foreach (var route in routes)
{
var result = await ProbeAsync(client, route, session, _factory.ApexDomain);
if (!result.Passed)
{
failures.Add(
$"{route.Method,-6} {result.Host}{result.Path} {route.Source} expected {result.Expected}, got {result.Observed}"
+ (string.IsNullOrWhiteSpace(result.ResponseExcerpt) ? "" : $" body={result.ResponseExcerpt}"));
}
}
Assert.True(
failures.Count == 0,
$"Endpoint contract failures ({failures.Count}/{routes.Count}):{Environment.NewLine}"
+ string.Join(Environment.NewLine, failures));
}
private static async Task<EndpointProbeResult> ProbeAsync(
HttpClient client,
EndpointContract route,
GameClientSession session,
string apexDomain)
{
var path = route.PathFor(session);
path = AddClientQuery(path, route, session);
var host = HostFor(path, apexDomain);
using var request = new HttpRequestMessage(
ToHttpMethod(route.Method),
new Uri($"http://{host}{path}"));
request.Headers.Accept.ParseAdd("application/json");
request.Headers.UserAgent.ParseAdd("RecRoom/2020.12.18");
request.Headers.TryAddWithoutValidation("X-DorkNet-Version", "december_2020_12_18");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", session.AccessToken);
if (NeedsRequestBody(request.Method))
{
request.Content = CreateContent(route, session);
}
try
{
using var response = await client.SendAsync(request);
var responseText = await response.Content.ReadAsStringAsync();
var statusCode = (int)response.StatusCode;
var passed = ResponseMatchesContract(route, response.StatusCode);
return new EndpointProbeResult(
route.Method,
host,
path,
ExpectedContract(route),
statusCode,
null,
Excerpt(responseText),
passed);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
return new EndpointProbeResult(
route.Method,
host,
path,
ExpectedContract(route),
null,
ex.Message,
null,
false);
}
}
private static bool ResponseMatchesContract(
EndpointContract route,
HttpStatusCode statusCode)
{
if ((int)statusCode >= 500)
{
return false;
}
if (statusCode is HttpStatusCode.MethodNotAllowed
or HttpStatusCode.NotImplemented
or HttpStatusCode.UpgradeRequired)
{
return false;
}
if (route.RequiresAuthorization
&& statusCode is HttpStatusCode.Unauthorized)
{
return false;
}
return true;
}
private static string ExpectedContract(EndpointContract route)
{
if (route.IsAdminEndpoint)
{
return "admin endpoint may reject non-admin test player, but must not 5xx/405/426";
}
if (route.RequiresAuthorization)
{
return "authenticated test player should reach the action without 401/5xx/405/426; 403 is valid for owner/admin-only resources";
}
return "game-client request should not 5xx/405/426";
}
private static HttpMethod ToHttpMethod(string method)
{
return string.Equals(method, "ANY", StringComparison.OrdinalIgnoreCase)
? HttpMethod.Get
: new HttpMethod(method);
}
private static bool NeedsRequestBody(HttpMethod method)
{
return method == HttpMethod.Post
|| method == HttpMethod.Put
|| method.Method.Equals("PATCH", StringComparison.OrdinalIgnoreCase);
}
private static HttpContent CreateContent(
EndpointContract route,
GameClientSession session)
{
var consumes = route.Consumes.Select(c => c.ToLowerInvariant()).ToArray();
if (consumes.Any(c => c.Contains("application/x-www-form-urlencoded")))
{
return new FormUrlEncodedContent(new Dictionary<string, string>
{
["account_id"] = session.PlayerId.ToString(),
["AccountId"] = session.PlayerId.ToString(),
["clubId"] = "1",
["ClubId"] = "1",
["description"] = "Endpoint contract description",
["deviceId"] = session.DeviceId,
["grant_type"] = "password",
["image"] = "endpoint-contract.png",
["imageName"] = "endpoint-contract.png",
["fileName"] = "endpoint-contract.png",
["message"] = "Endpoint contract message",
["name"] = "EndpointContract",
["newPassword"] = "endpoint-contract-password",
["oldPassword"] = "",
["password"] = "endpoint-contract-password",
["ping"] = "42",
["platformId"] = session.PlatformId,
["playerId"] = session.PlayerId.ToString(),
["playerIds"] = session.PlayerId.ToString(),
["query"] = "DormRoom",
["region"] = "us",
["targetPlayerId"] = session.PlayerId.ToString(),
["text"] = "Endpoint contract message",
["username"] = "EndpointContract",
["device_id"] = session.DeviceId,
["platform"] = "0",
["platform_id"] = session.PlatformId,
["Ids"] = session.PlayerId.ToString(),
["PlayerIds"] = session.PlayerId.ToString(),
});
}
if (consumes.Any(c => c.Contains("multipart/form-data")))
{
var content = new MultipartFormDataContent();
content.Add(new StringContent("endpoint-contract"), "name");
content.Add(new StringContent(session.PlayerId.ToString()), "playerId");
content.Add(new StringContent("endpoint-contract.png"), "imageName");
content.Add(new StringContent("endpoint-contract.png"), "fileName");
content.Add(new ByteArrayContent([]), "file", "endpoint-contract.bin");
return content;
}
if (consumes.Any(c => c.Contains("text/plain")))
{
return new StringContent("", Encoding.UTF8, "text/plain");
}
var json = JsonSerializer.Serialize(new
{
PlayerId = session.PlayerId,
AccountId = session.PlayerId,
ClubId = 1,
Description = "Endpoint contract description",
DeviceId = session.DeviceId,
FileName = "endpoint-contract.png",
Id = session.PlayerId,
Image = "endpoint-contract.png",
ImageName = "endpoint-contract.png",
Message = "Endpoint contract message",
Platform = 0,
PlatformId = session.PlatformId,
PlayerIds = new[] { session.PlayerId },
Name = "EndpointContract",
Password = "endpoint-contract-password",
Query = "DormRoom",
RoomName = "DormRoom",
TargetPlayerId = session.PlayerId,
Text = "Endpoint contract message",
});
return new StringContent(json, Encoding.UTF8, "application/json");
}
private static string AddClientQuery(
string path,
EndpointContract route,
GameClientSession session)
{
var separator = path.Contains('?', StringComparison.Ordinal) ? "&" : "?";
var lower = path.ToLowerInvariant();
var query = new List<string>();
if (lower.Contains("search", StringComparison.Ordinal)
|| lower.Contains("query", StringComparison.Ordinal))
{
query.Add("query=EndpointContract");
}
if (lower == "/members/bulk")
{
query.Add("clubId=1");
query.Add($"playerIds={session.PlayerId}");
}
if (lower == "/player/photonregionpings")
{
query.Add("region=us");
query.Add("ping=42");
}
if (route.Source.Contains("CachedLogins", StringComparison.Ordinal)
|| lower.Contains("platformlogin/cached", StringComparison.Ordinal)
|| lower.Contains("savedlogins", StringComparison.Ordinal))
{
query.Add("platform=0");
query.Add($"platformId={Uri.EscapeDataString(session.PlatformId)}");
}
if (route.Source.Contains("Bulk", StringComparison.Ordinal)
&& !query.Any(q => q.StartsWith("Ids=", StringComparison.OrdinalIgnoreCase)))
{
query.Add($"Ids={session.PlayerId}");
}
if (query.Count == 0)
{
return path;
}
return path + separator + string.Join("&", query);
}
private static string HostFor(string path, string apexDomain)
{
var lower = path.ToLowerInvariant();
if (lower.StartsWith("/connect/")
|| lower.StartsWith("/.well-known/")
|| lower.StartsWith("/eac/")
|| lower.StartsWith("/cachedlogin/")
|| lower.StartsWith("/photon/")
|| lower.StartsWith("/api/platformlogin/")
|| lower.StartsWith("/api/version")
|| lower.StartsWith("/api/auth/"))
{
return $"auth.{apexDomain}";
}
if (lower.StartsWith("/account/"))
{
return $"accounts.{apexDomain}";
}
if (lower.StartsWith("/roomserver/")
|| lower.StartsWith("/rooms")
|| lower.StartsWith("/playlists")
|| lower.StartsWith("/featuredrooms")
|| lower.StartsWith("/hot_rooms")
|| lower.StartsWith("/search_rooms")
|| lower.StartsWith("/api/rooms")
|| lower.StartsWith("/api/roomkeys")
|| lower.StartsWith("/api/gamesessions")
|| lower.StartsWith("/api/curatedroomplaylists"))
{
return $"rooms.{apexDomain}";
}
if (lower.StartsWith("/v1/find")
|| lower.StartsWith("/v1/join")
|| lower.StartsWith("/v1/leave")
|| lower.StartsWith("/v1/session")
|| lower.StartsWith("/roominstance/")
|| lower.StartsWith("/goto/")
|| lower.StartsWith("/player/"))
{
return $"match.{apexDomain}";
}
if (lower.StartsWith("/thread"))
{
return $"chat.{apexDomain}";
}
if (lower.StartsWith("/club")
|| lower.StartsWith("/announcements/")
|| lower.StartsWith("/subscription/")
|| lower.StartsWith("/members/")
|| lower.StartsWith("/api/clubreporting/"))
{
return $"clubs.{apexDomain}";
}
if (lower.StartsWith("/api/storefronts/")
|| lower.StartsWith("/api/purchase/")
|| lower.StartsWith("/api/subscription/"))
{
return $"commerce.{apexDomain}";
}
if (lower.StartsWith("/hub/") || lower.StartsWith("/api/notification/"))
{
return $"notify.{apexDomain}";
}
if (lower.StartsWith("/v1/regions"))
{
return $"geo.{apexDomain}";
}
if (lower.StartsWith("/v1/services") || lower.StartsWith("/v1/ping"))
{
return $"ns.{apexDomain}";
}
if (lower.StartsWith("/upload"))
{
return $"storage.{apexDomain}";
}
if (lower.StartsWith("/img/") || lower.StartsWith("/room/") || lower.StartsWith("/data/"))
{
return $"cdn.{apexDomain}";
}
if (lower.StartsWith("/api/admin/"))
{
return $"admin.{apexDomain}";
}
return $"api.{apexDomain}";
}
private static string? Excerpt(string? body)
{
if (string.IsNullOrWhiteSpace(body))
{
return null;
}
var compact = body.ReplaceLineEndings(" ").Trim();
return compact.Length <= 240 ? compact : compact[..240] + "...";
}
}
public sealed record EndpointProbeResult(
string Method,
string Host,
string Path,
string Expected,
int? StatusCode,
string? Error,
string? ResponseExcerpt,
bool Passed)
{
public string Observed => StatusCode is int code ? code.ToString() : Error ?? "no response";
}
+65
View File
@@ -0,0 +1,65 @@
using System.Net.Http.Headers;
using System.Text.Json;
namespace DorkNet.Server.Tests;
public sealed record GameClientSession(
long PlayerId,
string AccessToken,
string DeviceId,
string PlatformId);
public static class GameClientSessionFactory
{
public static async Task<GameClientSession> CreateAsync(
HttpClient client,
string apexDomain)
{
var deviceId = $"endpoint-contract-device-{Guid.NewGuid():N}";
var platformId = $"endpoint-contract-platform-{Guid.NewGuid():N}";
var tokenRequest = new HttpRequestMessage(
HttpMethod.Post,
new Uri($"http://auth.{apexDomain}/connect/token"));
tokenRequest.Headers.Accept.ParseAdd("application/json");
tokenRequest.Headers.UserAgent.ParseAdd("RecRoom/2020.12.18");
tokenRequest.Headers.TryAddWithoutValidation("X-DorkNet-Version", "december_2020_12_18");
tokenRequest.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "password",
["username"] = "EndpointContract",
["device_id"] = deviceId,
["platform"] = "0",
["platform_id"] = platformId,
});
using var tokenResponse = await client.SendAsync(tokenRequest);
if (!tokenResponse.IsSuccessStatusCode)
{
var body = await tokenResponse.Content.ReadAsStringAsync();
throw new InvalidOperationException(
$"Test player token request failed with {(int)tokenResponse.StatusCode} {tokenResponse.StatusCode}: {body}");
}
using var tokenJson = await JsonDocument.ParseAsync(
await tokenResponse.Content.ReadAsStreamAsync());
var accessToken = tokenJson.RootElement.GetProperty("access_token").GetString()
?? throw new InvalidOperationException("Token response did not include access_token.");
var userInfoRequest = new HttpRequestMessage(
HttpMethod.Get,
new Uri($"http://auth.{apexDomain}/connect/userinfo"));
userInfoRequest.Headers.Accept.ParseAdd("application/json");
userInfoRequest.Headers.UserAgent.ParseAdd("RecRoom/2020.12.18");
userInfoRequest.Headers.TryAddWithoutValidation("X-DorkNet-Version", "december_2020_12_18");
userInfoRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using var userInfoResponse = await client.SendAsync(userInfoRequest);
userInfoResponse.EnsureSuccessStatusCode();
using var userInfoJson = await JsonDocument.ParseAsync(
await userInfoResponse.Content.ReadAsStreamAsync());
var playerId = long.Parse(userInfoJson.RootElement.GetProperty("sub").GetString()!);
return new GameClientSession(playerId, accessToken, deviceId, platformId);
}
}
@@ -201,12 +201,12 @@ public class AccountApiController(
[HttpPost("api/account/v1/profileimage")] [HttpPost("api/account/v1/profileimage")]
[HttpPost("api/account/me/profileimage")] [HttpPost("api/account/me/profileimage")]
[HttpPost("account/me/profileimage")] [HttpPost("account/me/profileimage", Order = 10)]
[HttpPost("profileimage")] [HttpPost("profileimage", Order = 10)]
[HttpPut("api/account/v1/profileimage")] [HttpPut("api/account/v1/profileimage")]
[HttpPut("api/account/me/profileimage")] [HttpPut("api/account/me/profileimage")]
[HttpPut("account/me/profileimage")] [HttpPut("account/me/profileimage", Order = 10)]
[HttpPut("profileimage")] [HttpPut("profileimage", Order = 10)]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")] [Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
public async Task<IActionResult> SetProfileImage( public async Task<IActionResult> SetProfileImage(
[FromForm(Name = "imageName")] string? imageName, [FromForm(Name = "imageName")] string? imageName,
@@ -41,9 +41,9 @@ public class PageviewController(ILogger<PageviewController> logger) : Controller
/// intentionally return an empty url + a 1-hour freshness — the /// intentionally return an empty url + a 1-hour freshness — the
/// private server has no in-app deep-link system, so there's /// private server has no in-app deep-link system, so there's
/// nothing to navigate to and no reason to be polled more often.</summary> /// nothing to navigate to and no reason to be polled more often.</summary>
[HttpPost("pageview/consume")] [HttpPost("pageview/consume", Order = 10)]
[HttpPost("/pageview/consume")] [HttpPost("/pageview/consume")]
[HttpGet("pageview/consume")] [HttpGet("pageview/consume", Order = 10)]
[HttpGet("/pageview/consume")] [HttpGet("/pageview/consume")]
public IActionResult Consume() public IActionResult Consume()
{ {
@@ -23,7 +23,7 @@ public class AuthAccountController(DorkNetDbContext db) : ControllerBase
/// <c>ExpectPrimitiveResponse&lt;bool&gt;</c> so the body must be /// <c>ExpectPrimitiveResponse&lt;bool&gt;</c> so the body must be
/// the literal JSON <c>true</c> or <c>false</c>.</summary> /// the literal JSON <c>true</c> or <c>false</c>.</summary>
[HttpGet("/account/me/haspassword")] [HttpGet("/account/me/haspassword")]
[HttpGet("/api/platformlogin/haspassword")] [HttpGet("/api/platformlogin/haspassword", Order = 10)]
[Authorize] [Authorize]
public async Task<IActionResult> HasPassword() public async Task<IActionResult> HasPassword()
{ {
@@ -40,8 +40,8 @@ public class AuthAccountController(DorkNetDbContext db) : ControllerBase
/// one; first-time creation skips that check. Form-encoded body.</summary> /// one; first-time creation skips that check. Form-encoded body.</summary>
[HttpPost("/account/me/changepassword")] [HttpPost("/account/me/changepassword")]
[HttpPost("/account/me/createpassword")] [HttpPost("/account/me/createpassword")]
[HttpPost("/api/platformlogin/password")] [HttpPost("/api/platformlogin/password", Order = 10)]
[HttpPut("/api/platformlogin/password")] [HttpPut("/api/platformlogin/password", Order = 10)]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")] [Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
[Authorize] [Authorize]
public async Task<IActionResult> ChangePassword( public async Task<IActionResult> ChangePassword(
@@ -76,7 +76,7 @@ public class AuthAccountController(DorkNetDbContext db) : ControllerBase
/// account from the admin tab and the user re-creates it via /// account from the admin tab and the user re-creates it via
/// changepassword on next login. Anonymous calls 401.</summary> /// changepassword on next login. Anonymous calls 401.</summary>
[HttpPost("/account/me/resetpassword")] [HttpPost("/account/me/resetpassword")]
[HttpPost("/api/platformlogin/resetpassword")] [HttpPost("/api/platformlogin/resetpassword", Order = 10)]
[Authorize] [Authorize]
[AdminOnly] [AdminOnly]
public async Task<IActionResult> AdminResetPassword( public async Task<IActionResult> AdminResetPassword(
@@ -668,11 +668,14 @@ public class ClubsController(ClubService clubs) : ControllerBase
[HttpPost("/members/bulk")] [HttpPost("/members/bulk")]
public async Task<IActionResult> MembersBulk() public async Task<IActionResult> MembersBulk()
{ {
var form = Request.HasFormContentType ? Request.Form : null;
if (!long.TryParse(Request.Query["clubId"].ToString(), out var clubId) if (!long.TryParse(Request.Query["clubId"].ToString(), out var clubId)
&& !long.TryParse(Request.Form["clubId"].ToString(), out clubId)) && (form is null || !long.TryParse(form["clubId"].ToString(), out clubId)))
return BadRequest(new { error = "missing_club_id" }); return BadRequest(new { error = "missing_club_id" });
var idsRaw = Request.Query["playerIds"].Concat(Request.Form["playerIds"]); var idsRaw = form is null
? Request.Query["playerIds"].AsEnumerable()
: Request.Query["playerIds"].Concat(form["playerIds"]);
var ids = idsRaw var ids = idsRaw
.SelectMany(v => (v ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) .SelectMany(v => (v ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
.Select(s => long.TryParse(s, out var n) ? n : 0) .Select(s => long.TryParse(s, out var n) ? n : 0)
@@ -223,9 +223,12 @@ public class MatchPlayerController(
var playerId = TryGetCurrentPlayerId(); var playerId = TryGetCurrentPlayerId();
if (playerId == 0) return Ok(); if (playerId == 0) return Ok();
var form = Request.Form; var regions = Request.HasFormContentType
var regions = form["region"].ToArray(); ? Request.Form["region"].ToArray()
var pings = form["ping"].ToArray(); : Request.Query["region"].ToArray();
var pings = Request.HasFormContentType
? Request.Form["ping"].ToArray()
: Request.Query["ping"].ToArray();
var pairs = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); var pairs = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < Math.Min(regions.Length, pings.Length); i++) for (var i = 0; i < Math.Min(regions.Length, pings.Length); i++)
{ {
+2
View File
@@ -15,3 +15,5 @@ var app = builder.Build();
await app.RunDatabaseBootstrapAsync(); await app.RunDatabaseBootstrapAsync();
app.UseDorkNetPipeline(); app.UseDorkNetPipeline();
app.Run(); app.Run();
public partial class Program;
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DorkNet.ServiceDefaults; namespace DorkNet.ServiceDefaults;
@@ -21,9 +22,15 @@ public static class DorkNetServiceDefaultsExtensions
return builder; return builder;
} }
public static WebApplication MapDorkNetServiceDefaults(this WebApplication app) public static WebApplication MapDorkNetServiceDefaults(
this WebApplication app,
bool mapPublicHealth = true)
{ {
app.MapHealthChecks("/healthz"); if (mapPublicHealth)
{
app.MapHealthChecks("/healthz");
}
app.MapGet("/internal/healthz", (DorkNetServiceInfo info) => app.MapGet("/internal/healthz", (DorkNetServiceInfo info) =>
{ {
return Results.Ok(new ServiceHealthResponse( return Results.Ok(new ServiceHealthResponse(
@@ -34,4 +41,43 @@ public static class DorkNetServiceDefaultsExtensions
return app; return app;
} }
public static WebApplication UseDorkNetRouteOwnershipGuard(
this WebApplication app,
string serviceName)
{
app.Use(async (ctx, next) =>
{
var domain = Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
?? ctx.RequestServices.GetService<Microsoft.Extensions.Configuration.IConfiguration>()?["Domain:Apex"]
?? "localhost";
var path = ctx.Request.Path.Value ?? "/";
if (DorkNetRouteOwnership.IsOwnedBy(serviceName, ctx.Request.Host.Host, path, domain))
{
await next();
return;
}
var owner = DorkNetRouteOwnership.ResolvePublicService(ctx.Request.Host.Host, path, domain);
var logger = ctx.RequestServices
.GetRequiredService<ILoggerFactory>()
.CreateLogger("DorkNet.ServiceDefaults.RouteGuard");
logger.LogDebug(
"[route-guard] service={Service} rejected {Method} {Host}{Path}; owner={Owner}",
serviceName,
ctx.Request.Method,
ctx.Request.Host,
ctx.Request.Path,
owner);
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
await ctx.Response.WriteAsJsonAsync(new
{
error = "route_not_owned_by_service",
service = serviceName,
owner,
});
});
return app;
}
} }
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
</ItemGroup>
</Project>
+37
View File
@@ -0,0 +1,37 @@
using DorkNet.Contracts;
using DorkNet.ServiceDefaults;
using DorkNet.Server.Startup;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json",
optional: true, reloadOnChange: true);
builder.AddDorkNetServiceDefaults(ServiceNames.Commerce);
builder.AddDorkNetServices();
var app = builder.Build();
await app.RunDatabaseBootstrapAsync();
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
app.MapGet("/internal/commerce/capabilities", () =>
Results.Ok(new ServiceCapabilityResponse(
ServiceNames.Commerce,
[
"catalog",
"storefronts",
"econ",
"inventory",
"inventions",
],
[
"/api/catalog/*",
"/api/storefronts/*",
"/econ/*",
"/api/equipment/*",
"/api/inventions/*",
])));
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Commerce);
app.UseDorkNetPipeline();
app.Run();
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
</ItemGroup>
</Project>
+37
View File
@@ -0,0 +1,37 @@
using DorkNet.Contracts;
using DorkNet.ServiceDefaults;
using DorkNet.Server.Startup;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json",
optional: true, reloadOnChange: true);
builder.AddDorkNetServiceDefaults(ServiceNames.Content);
builder.AddDorkNetServices();
var app = builder.Build();
await app.RunDatabaseBootstrapAsync();
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
app.MapGet("/internal/content/capabilities", () =>
Results.Ok(new ServiceCapabilityResponse(
ServiceNames.Content,
[
"cdn",
"images",
"photos",
"room-blobs",
"storage",
],
[
"/api/images/*",
"/api/photos/*",
"/data/*",
"/img/*",
"/upload",
])));
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Content);
app.UseDorkNetPipeline();
app.Run();
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" /> <ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" /> <ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+7 -1
View File
@@ -1,15 +1,18 @@
using DorkNet.Contracts; using DorkNet.Contracts;
using DorkNet.ServiceDefaults; using DorkNet.ServiceDefaults;
using DorkNet.Server.Startup;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json", builder.Configuration.AddJsonFile("appsettings.Local.json",
optional: true, reloadOnChange: true); optional: true, reloadOnChange: true);
builder.AddDorkNetServiceDefaults(ServiceNames.Identity); builder.AddDorkNetServiceDefaults(ServiceNames.Identity);
builder.AddDorkNetServices();
var app = builder.Build(); var app = builder.Build();
app.MapDorkNetServiceDefaults(); await app.RunDatabaseBootstrapAsync();
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
app.MapGet("/internal/identity/capabilities", () => app.MapGet("/internal/identity/capabilities", () =>
Results.Ok(new ServiceCapabilityResponse( Results.Ok(new ServiceCapabilityResponse(
ServiceNames.Identity, ServiceNames.Identity,
@@ -26,4 +29,7 @@ app.MapGet("/internal/identity/capabilities", () =>
"/api/auth/*", "/api/auth/*",
]))); ])));
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Identity);
app.UseDorkNetPipeline();
app.Run(); app.Run();
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
</ItemGroup>
</Project>
+37
View File
@@ -0,0 +1,37 @@
using DorkNet.Contracts;
using DorkNet.ServiceDefaults;
using DorkNet.Server.Startup;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json",
optional: true, reloadOnChange: true);
builder.AddDorkNetServiceDefaults(ServiceNames.Moderation);
builder.AddDorkNetServices();
var app = builder.Build();
await app.RunDatabaseBootstrapAsync();
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
app.MapGet("/internal/moderation/capabilities", () =>
Results.Ok(new ServiceCapabilityResponse(
ServiceNames.Moderation,
[
"bug-reporting",
"player-reporting",
"sanitize",
"admin-api",
"testcase-management",
],
[
"/api/bugreporting/*",
"/api/playerreporting/*",
"/api/sanitize/*",
"/api/admin/*",
"/api/testcasemanagement/*",
])));
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Moderation);
app.UseDorkNetPipeline();
app.Run();
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" /> <ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" /> <ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+7 -1
View File
@@ -1,15 +1,18 @@
using DorkNet.Contracts; using DorkNet.Contracts;
using DorkNet.ServiceDefaults; using DorkNet.ServiceDefaults;
using DorkNet.Server.Startup;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json", builder.Configuration.AddJsonFile("appsettings.Local.json",
optional: true, reloadOnChange: true); optional: true, reloadOnChange: true);
builder.AddDorkNetServiceDefaults(ServiceNames.Notify); builder.AddDorkNetServiceDefaults(ServiceNames.Notify);
builder.AddDorkNetServices();
var app = builder.Build(); var app = builder.Build();
app.MapDorkNetServiceDefaults(); await app.RunDatabaseBootstrapAsync();
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
app.MapGet("/internal/notify/capabilities", () => app.MapGet("/internal/notify/capabilities", () =>
Results.Ok(new ServiceCapabilityResponse( Results.Ok(new ServiceCapabilityResponse(
ServiceNames.Notify, ServiceNames.Notify,
@@ -25,4 +28,7 @@ app.MapGet("/internal/notify/capabilities", () =>
"/player/*presence*", "/player/*presence*",
]))); ])));
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Notify);
app.UseDorkNetPipeline();
app.Run(); app.Run();
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
</ItemGroup>
</Project>
+37
View File
@@ -0,0 +1,37 @@
using DorkNet.Contracts;
using DorkNet.ServiceDefaults;
using DorkNet.Server.Startup;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json",
optional: true, reloadOnChange: true);
builder.AddDorkNetServiceDefaults(ServiceNames.Platform);
builder.AddDorkNetServices();
var app = builder.Build();
await app.RunDatabaseBootstrapAsync();
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
app.MapGet("/internal/platform/capabilities", () =>
Results.Ok(new ServiceCapabilityResponse(
ServiceNames.Platform,
[
"service-directory",
"config",
"version-check",
"geo",
"strings",
],
[
"/v1/services",
"/api/config/*",
"/api/versioncheck/*",
"/v1/regions",
"/strings/*",
])));
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Platform);
app.UseDorkNetPipeline();
app.Run();
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" /> <ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" /> <ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+7 -1
View File
@@ -1,15 +1,18 @@
using DorkNet.Contracts; using DorkNet.Contracts;
using DorkNet.ServiceDefaults; using DorkNet.ServiceDefaults;
using DorkNet.Server.Startup;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json", builder.Configuration.AddJsonFile("appsettings.Local.json",
optional: true, reloadOnChange: true); optional: true, reloadOnChange: true);
builder.AddDorkNetServiceDefaults(ServiceNames.Rooms); builder.AddDorkNetServiceDefaults(ServiceNames.Rooms);
builder.AddDorkNetServices();
var app = builder.Build(); var app = builder.Build();
app.MapDorkNetServiceDefaults(); await app.RunDatabaseBootstrapAsync();
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
app.MapGet("/internal/rooms/capabilities", () => app.MapGet("/internal/rooms/capabilities", () =>
Results.Ok(new ServiceCapabilityResponse( Results.Ok(new ServiceCapabilityResponse(
ServiceNames.Rooms, ServiceNames.Rooms,
@@ -28,4 +31,7 @@ app.MapGet("/internal/rooms/capabilities", () =>
"/v1/join/*", "/v1/join/*",
]))); ])));
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Rooms);
app.UseDorkNetPipeline();
app.Run(); app.Run();
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
</ItemGroup>
</Project>
+37
View File
@@ -0,0 +1,37 @@
using DorkNet.Contracts;
using DorkNet.ServiceDefaults;
using DorkNet.Server.Startup;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json",
optional: true, reloadOnChange: true);
builder.AddDorkNetServiceDefaults(ServiceNames.Social);
builder.AddDorkNetServices();
var app = builder.Build();
await app.RunDatabaseBootstrapAsync();
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
app.MapGet("/internal/social/capabilities", () =>
Results.Ok(new ServiceCapabilityResponse(
ServiceNames.Social,
[
"clubs",
"groups",
"announcements",
"player-events",
"subscriptions",
],
[
"/club/*",
"/api/groups/*",
"/api/playerevents/*",
"/api/playersubscriptions/*",
"/announcements/*",
])));
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Social);
app.UseDorkNetPipeline();
app.Run();
+8
View File
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
</ItemGroup>
</Project>
+36
View File
@@ -0,0 +1,36 @@
using DorkNet.Contracts;
using DorkNet.ServiceDefaults;
using DorkNet.Server.Startup;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json",
optional: true, reloadOnChange: true);
builder.AddDorkNetServiceDefaults(ServiceNames.Web);
builder.AddDorkNetServices();
var app = builder.Build();
await app.RunDatabaseBootstrapAsync();
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
app.MapGet("/internal/web/capabilities", () =>
Results.Ok(new ServiceCapabilityResponse(
ServiceNames.Web,
[
"public-site",
"admin-site",
"feed-site",
"site-api",
],
[
"/",
"www.*",
"admin.*",
"feed.*",
"/api/site/*",
])));
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Web);
app.UseDorkNetPipeline();
app.Run();
+8
View File
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+98 -19
View File
@@ -7,10 +7,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Server", "DorkNet.S
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Models", "DorkNet.Models\DorkNet.Models.csproj", "{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Models", "DorkNet.Models\DorkNet.Models.csproj", "{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{07C2787E-EAC7-C090-1BA3-A61EC2A24D84}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MigrateSqliteToPostgres", "Tools\MigrateSqliteToPostgres\MigrateSqliteToPostgres.csproj", "{70C60098-1B11-4C13-943B-A8A056429EAE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.AdminMobile", "DorkNet.AdminMobile\DorkNet.AdminMobile.csproj", "{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.AdminMobile", "DorkNet.AdminMobile\DorkNet.AdminMobile.csproj", "{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Contracts", "DorkNet.Contracts\DorkNet.Contracts.csproj", "{7D9E995E-5333-43B2-848B-3522C6C3D893}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Contracts", "DorkNet.Contracts\DorkNet.Contracts.csproj", "{7D9E995E-5333-43B2-848B-3522C6C3D893}"
@@ -25,6 +21,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Rooms", "D
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Notify", "DorkNet.Services.Notify\DorkNet.Services.Notify.csproj", "{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Notify", "DorkNet.Services.Notify\DorkNet.Services.Notify.csproj", "{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Server.Tests", "DorkNet.Server.Tests\DorkNet.Server.Tests.csproj", "{43230177-A02F-440A-9B59-B79AB5E7BC27}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Content", "DorkNet.Services.Content\DorkNet.Services.Content.csproj", "{8958DA76-3925-4F12-8884-DE2E79FE9677}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Social", "DorkNet.Services.Social\DorkNet.Services.Social.csproj", "{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Commerce", "DorkNet.Services.Commerce\DorkNet.Services.Commerce.csproj", "{77BBD5A3-A610-4A20-A61A-B4AB56291C27}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Platform", "DorkNet.Services.Platform\DorkNet.Services.Platform.csproj", "{C68AA2CA-9E80-46E5-9EE0-1544CE070036}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Moderation", "DorkNet.Services.Moderation\DorkNet.Services.Moderation.csproj", "{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Web", "DorkNet.Services.Web\DorkNet.Services.Web.csproj", "{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -59,18 +69,6 @@ Global
{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x64.Build.0 = Release|Any CPU {8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x64.Build.0 = Release|Any CPU
{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x86.ActiveCfg = Release|Any CPU {8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x86.ActiveCfg = Release|Any CPU
{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x86.Build.0 = Release|Any CPU {8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x86.Build.0 = Release|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|x64.ActiveCfg = Debug|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|x64.Build.0 = Debug|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|x86.ActiveCfg = Debug|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|x86.Build.0 = Debug|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|Any CPU.Build.0 = Release|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|x64.ActiveCfg = Release|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|x64.Build.0 = Release|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|x86.ActiveCfg = Release|Any CPU
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|x86.Build.0 = Release|Any CPU
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|x64.ActiveCfg = Debug|Any CPU {C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -155,11 +153,92 @@ Global
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|x64.Build.0 = Release|Any CPU {5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|x64.Build.0 = Release|Any CPU
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|x86.ActiveCfg = Release|Any CPU {5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|x86.ActiveCfg = Release|Any CPU
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|x86.Build.0 = Release|Any CPU {5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|x86.Build.0 = Release|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|x64.ActiveCfg = Debug|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|x64.Build.0 = Debug|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|x86.ActiveCfg = Debug|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|x86.Build.0 = Debug|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|Any CPU.Build.0 = Release|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|x64.ActiveCfg = Release|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|x64.Build.0 = Release|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|x86.ActiveCfg = Release|Any CPU
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|x86.Build.0 = Release|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|x64.ActiveCfg = Debug|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|x64.Build.0 = Debug|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|x86.ActiveCfg = Debug|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|x86.Build.0 = Debug|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|Any CPU.Build.0 = Release|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|x64.ActiveCfg = Release|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|x64.Build.0 = Release|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|x86.ActiveCfg = Release|Any CPU
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|x86.Build.0 = Release|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|x64.ActiveCfg = Debug|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|x64.Build.0 = Debug|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|x86.ActiveCfg = Debug|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|x86.Build.0 = Debug|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|Any CPU.Build.0 = Release|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|x64.ActiveCfg = Release|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|x64.Build.0 = Release|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|x86.ActiveCfg = Release|Any CPU
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|x86.Build.0 = Release|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|x64.ActiveCfg = Debug|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|x64.Build.0 = Debug|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|x86.ActiveCfg = Debug|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|x86.Build.0 = Debug|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|Any CPU.Build.0 = Release|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|x64.ActiveCfg = Release|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|x64.Build.0 = Release|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|x86.ActiveCfg = Release|Any CPU
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|x86.Build.0 = Release|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|x64.ActiveCfg = Debug|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|x64.Build.0 = Debug|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|x86.ActiveCfg = Debug|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|x86.Build.0 = Debug|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|Any CPU.Build.0 = Release|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|x64.ActiveCfg = Release|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|x64.Build.0 = Release|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|x86.ActiveCfg = Release|Any CPU
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|x86.Build.0 = Release|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|x64.ActiveCfg = Debug|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|x64.Build.0 = Debug|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|x86.ActiveCfg = Debug|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|x86.Build.0 = Debug|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|Any CPU.Build.0 = Release|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|x64.ActiveCfg = Release|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|x64.Build.0 = Release|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|x86.ActiveCfg = Release|Any CPU
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|x86.Build.0 = Release|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|x64.ActiveCfg = Debug|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|x64.Build.0 = Debug|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|x86.ActiveCfg = Debug|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|x86.Build.0 = Debug|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|Any CPU.Build.0 = Release|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|x64.ActiveCfg = Release|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|x64.Build.0 = Release|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|x86.ActiveCfg = Release|Any CPU
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{70C60098-1B11-4C13-943B-A8A056429EAE} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
EndGlobalSection
EndGlobal EndGlobal
+16 -13
View File
@@ -23,7 +23,7 @@ main also auto-detects your install.
| **Version key** (sent as `X-DorkNet-Version` header) | `december_2020_12_18` | | **Version key** (sent as `X-DorkNet-Version` header) | `december_2020_12_18` |
| **Version plugin** | `DorkNet.Server/Versions/Late2020/Late2020VersionPlugin.cs` | | **Version plugin** | `DorkNet.Server/Versions/Late2020/Late2020VersionPlugin.cs` |
| **Schema** | One EF `Initial` migration; SQLite + Postgres both work | | **Schema** | One EF `Initial` migration; SQLite + Postgres both work |
| **Runtime topology** | Full API still runs from `DorkNet.Server`; microservice gateway/service hosts are scaffolded for incremental split | | **Runtime topology** | Standalone `DorkNet.Server`, plus gateway-fronted service slices with monolith fallback |
| **Diverges from `march-2020-03-10` by** | ~170 files | | **Diverges from `march-2020-03-10` by** | ~170 files |
Subsystems present on this branch that the March branch doesn't have: Subsystems present on this branch that the March branch doesn't have:
@@ -84,7 +84,7 @@ categories / playlists, and starts listening. You should see
`Application started.` in the log; `curl http://localhost:8080/healthz` `Application started.` in the log; `curl http://localhost:8080/healthz`
returns 200. returns 200.
For Docker, Dokploy, and microservices-scaffold deploy notes, see For Docker, Dokploy, and microservices deploy notes, see
[docs/deploy.md](docs/deploy.md). [docs/deploy.md](docs/deploy.md).
--- ---
@@ -201,10 +201,10 @@ Default `0` (off).
|---|---| |---|---|
| `DorkNet.Server/` | ASP.NET Core backend (controllers, services, data, hubs, middleware) | | `DorkNet.Server/` | ASP.NET Core backend (controllers, services, data, hubs, middleware) |
| `DorkNet.Models/` | DTOs shared between server and client mod | | `DorkNet.Models/` | DTOs shared between server and client mod |
| `DorkNet.Contracts/` | Shared service names, service-map options, and health/probe contracts | | `DorkNet.Contracts/` | Shared service names, route ownership, service-map options, and health/probe contracts |
| `DorkNet.ServiceDefaults/` | Shared microservice health, JSON, HTTP client, and service identity setup | | `DorkNet.ServiceDefaults/` | Shared service health, JSON, HTTP client, service identity, and route-guard setup |
| `DorkNet.Gateway/` | Edge/gateway host scaffold with service-map and service-health endpoints | | `DorkNet.Gateway/` | Edge reverse proxy with service-map, route-table, and service-health endpoints |
| `DorkNet.Services.*` | Initial identity, rooms, and notify service host scaffolds | | `DorkNet.Services.*` | Gateway-fronted service hosts for identity, rooms, notify, content, social, commerce, platform, moderation, and web |
| `DorkNet.ClientMod/` | MelonLoader IL2CPP mod — the client patcher | | `DorkNet.ClientMod/` | MelonLoader IL2CPP mod — the client patcher |
| `tools/` | Installers (`install-melon.ps1`, `install-legacy-client.ps1`, `remove-eac.ps1`), Cloudflare tunnel templates | | `tools/` | Installers (`install-melon.ps1`, `install-legacy-client.ps1`, `remove-eac.ps1`), Cloudflare tunnel templates |
| `Dockerfile`, `Dockerfile.service`, `docker-compose.microservices*.yml` | Full-server, local microservices, and Dokploy microservices deployment entrypoints | | `Dockerfile`, `Dockerfile.service`, `docker-compose.microservices*.yml` | Full-server, local microservices, and Dokploy microservices deployment entrypoints |
@@ -213,16 +213,19 @@ See [docs/architecture.md](docs/architecture.md) for the full mental
model (project layout, request lifecycle, watch-mirror controller model (project layout, request lifecycle, watch-mirror controller
pattern, where to start contributing). pattern, where to start contributing).
### Microservices scaffold ### Microservices
`docker-compose.microservices.yml` starts the new gateway, identity, `docker-compose.microservices.yml` starts the gateway, identity, rooms,
rooms, and notify service hosts plus Compose-managed Postgres and Redis notify, content, social, commerce, platform, moderation, web, and
monolith fallback service hosts plus Compose-managed Postgres and Redis
for local testing. `docker-compose.microservices.dokploy.yml` is the for local testing. `docker-compose.microservices.dokploy.yml` is the
Dokploy version; it adds a `cloudflared` sidecar so Cloudflare Tunnel Dokploy version; it adds a `cloudflared` sidecar so Cloudflare Tunnel
can route the apex and wildcard hostnames to the gateway without can route the apex and wildcard hostnames to the gateway without Dokploy
Dokploy domain rows. These files do **not** replace the full domain rows.
`DorkNet.Server` runtime yet; the public API is still served by the
monolith while domains are peeled into services incrementally. The gateway routes owned host/path slices to the dedicated service hosts.
The monolith fallback is still present for unknown route families, so
public client URLs remain stable while the split continues.
The compose file intentionally does not start object storage. Point it at The compose file intentionally does not start object storage. Point it at
your separate S3-compatible instance: your separate S3-compatible instance:
+120 -6
View File
@@ -1,3 +1,14 @@
x-dorknet-service-env: &dorknet-service-env
DORKNET_DOMAIN: ${DORKNET_DOMAIN:?Set DORKNET_DOMAIN in Dokploy}
DORKNET_JWT_SECRET: ${DORKNET_JWT_SECRET:?Set DORKNET_JWT_SECRET in Dokploy}
Database__Provider: postgres
ConnectionStrings__Default: Host=postgres;Port=5432;Database=dorknet;Username=dorknet;Password=${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in Dokploy}
ConnectionStrings__Redis: redis:6379
S3__Endpoint: ${DORKNET_S3_ENDPOINT:?Set DORKNET_S3_ENDPOINT in Dokploy}
S3__AccessKey: ${DORKNET_S3_ACCESS_KEY:?Set DORKNET_S3_ACCESS_KEY in Dokploy}
S3__SecretKey: ${DORKNET_S3_SECRET_KEY:?Set DORKNET_S3_SECRET_KEY in Dokploy}
S3__Region: ${DORKNET_S3_REGION:-garage}
services: services:
gateway: gateway:
build: build:
@@ -11,13 +22,26 @@ services:
DorkNet__Services__Identity: http://identity:8080 DorkNet__Services__Identity: http://identity:8080
DorkNet__Services__Rooms: http://rooms:8080 DorkNet__Services__Rooms: http://rooms:8080
DorkNet__Services__Notify: http://notify:8080 DorkNet__Services__Notify: http://notify:8080
DorkNet__Services__Monolith: "" DorkNet__Services__Content: http://content:8080
DorkNet__Services__Social: http://social:8080
DorkNet__Services__Commerce: http://commerce:8080
DorkNet__Services__Platform: http://platform:8080
DorkNet__Services__Moderation: http://moderation:8080
DorkNet__Services__Web: http://web:8080
DorkNet__Services__Monolith: http://monolith:8080
expose: expose:
- "8080" - "8080"
depends_on: depends_on:
- identity - identity
- rooms - rooms
- notify - notify
- content
- social
- commerce
- platform
- moderation
- web
- monolith
cloudflared: cloudflared:
image: cloudflare/cloudflared:latest image: cloudflare/cloudflared:latest
@@ -33,6 +57,8 @@ services:
args: args:
PROJECT_PATH: DorkNet.Services.Identity/DorkNet.Services.Identity.csproj PROJECT_PATH: DorkNet.Services.Identity/DorkNet.Services.Identity.csproj
APP_DLL: DorkNet.Services.Identity.dll APP_DLL: DorkNet.Services.Identity.dll
environment:
<<: *dorknet-service-env
depends_on: depends_on:
- postgres - postgres
- redis - redis
@@ -45,10 +71,7 @@ services:
PROJECT_PATH: DorkNet.Services.Rooms/DorkNet.Services.Rooms.csproj PROJECT_PATH: DorkNet.Services.Rooms/DorkNet.Services.Rooms.csproj
APP_DLL: DorkNet.Services.Rooms.dll APP_DLL: DorkNet.Services.Rooms.dll
environment: environment:
S3__Endpoint: ${DORKNET_S3_ENDPOINT:-} <<: *dorknet-service-env
S3__AccessKey: ${DORKNET_S3_ACCESS_KEY:-}
S3__SecretKey: ${DORKNET_S3_SECRET_KEY:-}
S3__Region: ${DORKNET_S3_REGION:-garage}
depends_on: depends_on:
- postgres - postgres
- redis - redis
@@ -60,7 +83,98 @@ services:
args: args:
PROJECT_PATH: DorkNet.Services.Notify/DorkNet.Services.Notify.csproj PROJECT_PATH: DorkNet.Services.Notify/DorkNet.Services.Notify.csproj
APP_DLL: DorkNet.Services.Notify.dll APP_DLL: DorkNet.Services.Notify.dll
environment:
<<: *dorknet-service-env
depends_on: depends_on:
- postgres
- redis
content:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Content/DorkNet.Services.Content.csproj
APP_DLL: DorkNet.Services.Content.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
social:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Social/DorkNet.Services.Social.csproj
APP_DLL: DorkNet.Services.Social.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
commerce:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Commerce/DorkNet.Services.Commerce.csproj
APP_DLL: DorkNet.Services.Commerce.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
platform:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Platform/DorkNet.Services.Platform.csproj
APP_DLL: DorkNet.Services.Platform.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
moderation:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Moderation/DorkNet.Services.Moderation.csproj
APP_DLL: DorkNet.Services.Moderation.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
web:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Web/DorkNet.Services.Web.csproj
APP_DLL: DorkNet.Services.Web.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
monolith:
build:
context: .
dockerfile: Dockerfile
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis - redis
postgres: postgres:
@@ -68,7 +182,7 @@ services:
environment: environment:
POSTGRES_DB: dorknet POSTGRES_DB: dorknet
POSTGRES_USER: dorknet POSTGRES_USER: dorknet
POSTGRES_PASSWORD: dorknet_dev POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in Dokploy}
volumes: volumes:
- microservices-postgres:/var/lib/postgresql/data - microservices-postgres:/var/lib/postgresql/data
+121 -6
View File
@@ -1,3 +1,14 @@
x-dorknet-service-env: &dorknet-service-env
DORKNET_DOMAIN: ${DORKNET_DOMAIN:-localhost}
DORKNET_JWT_SECRET: ${DORKNET_JWT_SECRET:-dorknet-dev-jwt-secret-000000000000000000000000000000000000000000}
Database__Provider: postgres
ConnectionStrings__Default: Host=postgres;Port=5432;Database=dorknet;Username=dorknet;Password=${POSTGRES_PASSWORD:-dorknet_dev}
ConnectionStrings__Redis: redis:6379
S3__Endpoint: ${DORKNET_S3_ENDPOINT:-}
S3__AccessKey: ${DORKNET_S3_ACCESS_KEY:-}
S3__SecretKey: ${DORKNET_S3_SECRET_KEY:-}
S3__Region: ${DORKNET_S3_REGION:-garage}
services: services:
gateway: gateway:
build: build:
@@ -7,16 +18,30 @@ services:
PROJECT_PATH: DorkNet.Gateway/DorkNet.Gateway.csproj PROJECT_PATH: DorkNet.Gateway/DorkNet.Gateway.csproj
APP_DLL: DorkNet.Gateway.dll APP_DLL: DorkNet.Gateway.dll
environment: environment:
DORKNET_DOMAIN: ${DORKNET_DOMAIN:-localhost}
DorkNet__Services__Identity: http://identity:8080 DorkNet__Services__Identity: http://identity:8080
DorkNet__Services__Rooms: http://rooms:8080 DorkNet__Services__Rooms: http://rooms:8080
DorkNet__Services__Notify: http://notify:8080 DorkNet__Services__Notify: http://notify:8080
DorkNet__Services__Monolith: "" DorkNet__Services__Content: http://content:8080
DorkNet__Services__Social: http://social:8080
DorkNet__Services__Commerce: http://commerce:8080
DorkNet__Services__Platform: http://platform:8080
DorkNet__Services__Moderation: http://moderation:8080
DorkNet__Services__Web: http://web:8080
DorkNet__Services__Monolith: http://monolith:8080
ports: ports:
- "8080:8080" - "8080:8080"
depends_on: depends_on:
- identity - identity
- rooms - rooms
- notify - notify
- content
- social
- commerce
- platform
- moderation
- web
- monolith
identity: identity:
build: build:
@@ -25,6 +50,8 @@ services:
args: args:
PROJECT_PATH: DorkNet.Services.Identity/DorkNet.Services.Identity.csproj PROJECT_PATH: DorkNet.Services.Identity/DorkNet.Services.Identity.csproj
APP_DLL: DorkNet.Services.Identity.dll APP_DLL: DorkNet.Services.Identity.dll
environment:
<<: *dorknet-service-env
depends_on: depends_on:
- postgres - postgres
- redis - redis
@@ -37,10 +64,7 @@ services:
PROJECT_PATH: DorkNet.Services.Rooms/DorkNet.Services.Rooms.csproj PROJECT_PATH: DorkNet.Services.Rooms/DorkNet.Services.Rooms.csproj
APP_DLL: DorkNet.Services.Rooms.dll APP_DLL: DorkNet.Services.Rooms.dll
environment: environment:
S3__Endpoint: ${DORKNET_S3_ENDPOINT:-} <<: *dorknet-service-env
S3__AccessKey: ${DORKNET_S3_ACCESS_KEY:-}
S3__SecretKey: ${DORKNET_S3_SECRET_KEY:-}
S3__Region: ${DORKNET_S3_REGION:-garage}
depends_on: depends_on:
- postgres - postgres
- redis - redis
@@ -52,7 +76,98 @@ services:
args: args:
PROJECT_PATH: DorkNet.Services.Notify/DorkNet.Services.Notify.csproj PROJECT_PATH: DorkNet.Services.Notify/DorkNet.Services.Notify.csproj
APP_DLL: DorkNet.Services.Notify.dll APP_DLL: DorkNet.Services.Notify.dll
environment:
<<: *dorknet-service-env
depends_on: depends_on:
- postgres
- redis
content:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Content/DorkNet.Services.Content.csproj
APP_DLL: DorkNet.Services.Content.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
social:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Social/DorkNet.Services.Social.csproj
APP_DLL: DorkNet.Services.Social.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
commerce:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Commerce/DorkNet.Services.Commerce.csproj
APP_DLL: DorkNet.Services.Commerce.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
platform:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Platform/DorkNet.Services.Platform.csproj
APP_DLL: DorkNet.Services.Platform.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
moderation:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Moderation/DorkNet.Services.Moderation.csproj
APP_DLL: DorkNet.Services.Moderation.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
web:
build:
context: .
dockerfile: Dockerfile.service
args:
PROJECT_PATH: DorkNet.Services.Web/DorkNet.Services.Web.csproj
APP_DLL: DorkNet.Services.Web.dll
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis
monolith:
build:
context: .
dockerfile: Dockerfile
environment:
<<: *dorknet-service-env
depends_on:
- postgres
- redis - redis
postgres: postgres:
@@ -60,7 +175,7 @@ services:
environment: environment:
POSTGRES_DB: dorknet POSTGRES_DB: dorknet
POSTGRES_USER: dorknet POSTGRES_USER: dorknet
POSTGRES_PASSWORD: dorknet_dev POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dorknet_dev}
volumes: volumes:
- microservices-postgres:/var/lib/postgresql/data - microservices-postgres:/var/lib/postgresql/data
+51 -27
View File
@@ -18,10 +18,16 @@ DorkNet.sln
├── DorkNet.Models Shared DTO + serialization types ├── DorkNet.Models Shared DTO + serialization types
├── DorkNet.Contracts Shared service contracts for the service split ├── DorkNet.Contracts Shared service contracts for the service split
├── DorkNet.ServiceDefaults Shared service-host setup ├── DorkNet.ServiceDefaults Shared service-host setup
├── DorkNet.Gateway Gateway/edge host scaffold ├── DorkNet.Gateway Gateway/edge reverse proxy
├── DorkNet.Services.Identity Identity service scaffold ├── DorkNet.Services.Identity Auth/accounts service host
├── DorkNet.Services.Rooms Rooms service scaffold ├── DorkNet.Services.Rooms Rooms/matchmaking service host
├── DorkNet.Services.Notify Notify service scaffold ├── DorkNet.Services.Notify Notify/chat service host
├── DorkNet.Services.Content CDN/images/photos/storage service host
├── DorkNet.Services.Social Clubs/groups/events/social service host
├── DorkNet.Services.Commerce Store/econ/inventory service host
├── DorkNet.Services.Platform Service directory/config/platform service host
├── DorkNet.Services.Moderation Bug reporting/moderation service host
├── DorkNet.Services.Web Public/admin/feed web host
└── DorkNet.ClientMod MelonLoader 6 IL2CPP mod — the client patcher └── DorkNet.ClientMod MelonLoader 6 IL2CPP mod — the client patcher
``` ```
@@ -29,12 +35,18 @@ DorkNet.sln
|---|---|---| |---|---|---|
| `DorkNet.Server` | REST API, SignalR hub, admin SPA host, static-site host, image CDN, matchmaking, persistence | .NET 9 | | `DorkNet.Server` | REST API, SignalR hub, admin SPA host, static-site host, image CDN, matchmaking, persistence | .NET 9 |
| `DorkNet.Models` | DTOs the wire protocol speaks. No project dependencies. | .NET 9 | | `DorkNet.Models` | DTOs the wire protocol speaks. No project dependencies. | .NET 9 |
| `DorkNet.Contracts` | Service names, service-map options, health responses, probe responses | .NET 9 | | `DorkNet.Contracts` | Service names, route ownership, service-map options, health responses, probe responses | .NET 9 |
| `DorkNet.ServiceDefaults` | Common microservice health checks, JSON defaults, HTTP clients, service identity | .NET 9 | | `DorkNet.ServiceDefaults` | Common service health checks, JSON defaults, HTTP clients, service identity, route guard | .NET 9 |
| `DorkNet.Gateway` | Edge/gateway scaffold; exposes internal service map and service health probes | .NET 9 | | `DorkNet.Gateway` | Edge reverse proxy; routes host/path slices to services and exposes service health probes | .NET 9 |
| `DorkNet.Services.Identity` | Future home for accounts, auth, platform login, JWT issuance | .NET 9 | | `DorkNet.Services.Identity` | Accounts, auth, platform login, JWT issuance route slice | .NET 9 |
| `DorkNet.Services.Rooms` | Future home for rooms, room keys, matchmaking, discovery | .NET 9 | | `DorkNet.Services.Rooms` | Rooms, room keys, matchmaking, discovery route slice | .NET 9 |
| `DorkNet.Services.Notify` | Future home for SignalR edge, notification fan-out, presence, request logs | .NET 9 | | `DorkNet.Services.Notify` | SignalR edge, messages/chat, notification fan-out route slice | .NET 9 |
| `DorkNet.Services.Content` | CDN paths, image/photo APIs, uploads, S3-backed blobs | .NET 9 |
| `DorkNet.Services.Social` | Clubs, groups, announcements, player events, subscriptions | .NET 9 |
| `DorkNet.Services.Commerce` | Catalog, storefronts, econ, inventory, inventions | .NET 9 |
| `DorkNet.Services.Platform` | Service URLs, config, version checks, geo, strings, telemetry-style surfaces | .NET 9 |
| `DorkNet.Services.Moderation` | Bug reporting, player reporting, sanitize, admin API, testcase routes | .NET 9 |
| `DorkNet.Services.Web` | Public site, admin static host, feed static host, site API | .NET 9 |
| `DorkNet.ClientMod` | MelonLoader 0.6.x IL2CPP mod — the client patcher, JSON config | .NET 6 | | `DorkNet.ClientMod` | MelonLoader 0.6.x IL2CPP mod — the client patcher, JSON config | .NET 6 |
`ClientMod` applies the client-side patches needed to point the 2020 `ClientMod` applies the client-side patches needed to point the 2020
@@ -66,31 +78,42 @@ DorkNet.Server/
## Runtime topology ## Runtime topology
`DorkNet.Server` is still the only production-complete runtime. It owns `DorkNet.Server` remains the compatibility fallback and full standalone
the full public Rec Room-compatible HTTP surface, SignalR hub, static runtime. It can still run by itself, serving the full public
hosts, database bootstrap, Redis-backed ephemeral state, and S3-backed Rec Room-compatible HTTP surface, SignalR hub, static hosts, database
blob storage. bootstrap, Redis-backed ephemeral state, and S3-backed blob storage.
The microservice projects are the migration scaffold. They are wired into The microservice shape is a gateway-fronted split:
the solution and Docker Compose, but they only expose internal health and
capability endpoints today: - `DorkNet.Gateway` reverse-proxies public requests by host/path.
- `DorkNet.Services.Identity`, `DorkNet.Services.Rooms`,
`DorkNet.Services.Notify`, `DorkNet.Services.Content`,
`DorkNet.Services.Social`, `DorkNet.Services.Commerce`,
`DorkNet.Services.Platform`, `DorkNet.Services.Moderation`, and
`DorkNet.Services.Web` run the shared server stack behind a route
ownership guard. That keeps response shapes identical while moving
traffic by domain/path slice.
- `DorkNet.Server` runs as `monolith` behind the gateway for unknown
route families that are not assigned to a dedicated service yet.
- `DorkNet.Gateway` exposes `/internal/services` and - `DorkNet.Gateway` exposes `/internal/services` and
`/internal/services/health`. `/internal/services/health`, plus `/internal/routes` for the active
proxy table.
- `DorkNet.Services.Identity` exposes - `DorkNet.Services.Identity` exposes
`/internal/identity/capabilities`. `/internal/identity/capabilities`.
- `DorkNet.Services.Rooms` exposes `/internal/rooms/capabilities`. - `DorkNet.Services.Rooms` exposes `/internal/rooms/capabilities`.
- `DorkNet.Services.Notify` exposes `/internal/notify/capabilities`. - `DorkNet.Services.Notify` exposes `/internal/notify/capabilities`.
- The other slices expose matching `/internal/{service}/capabilities`
endpoints.
`docker-compose.microservices.yml` starts gateway, identity, rooms, and `docker-compose.microservices.yml` starts gateway, all dedicated service
notify alongside Compose-managed Postgres and Redis for local testing. slices, the monolith fallback, and Compose-managed Postgres and Redis for
local testing.
`docker-compose.microservices.dokploy.yml` is the Dokploy shape: it adds `docker-compose.microservices.dokploy.yml` is the Dokploy shape: it adds
a `cloudflared` sidecar that joins the same Compose network and forwards a `cloudflared` sidecar that joins the same Compose network and forwards
Cloudflare Tunnel traffic to `http://gateway:8080`. Object storage is Cloudflare Tunnel traffic to `http://gateway:8080`. Object storage is
external S3-compatible storage configured with `DORKNET_S3_*` external S3-compatible storage configured with `DORKNET_S3_*`
environment variables; the compose files do not run MinIO or Garage. The environment variables; the compose files do not run MinIO or Garage.
next migration step is to move one public route group at a time behind
the gateway while preserving existing URLs and response shapes.
### Controllers/ groupings ### Controllers/ groupings
@@ -277,10 +300,11 @@ worse.
intentional — the decompiled source is provenance only, not intentional — the decompiled source is provenance only, not
shippable. Comments give a future contributor a breadcrumb to shippable. Comments give a future contributor a breadcrumb to
re-derive the wire shape if the decompile changes. re-derive the wire shape if the decompile changes.
- **Microservices are scaffolded, not fully cut over.** The gateway and - **Microservices use shared server code during extraction.** The
service hosts compile and run, but the public API still lives in dedicated service hosts run the proven controller/service stack behind
`DorkNet.Server`. Move endpoints by domain slice, not by copying random route guards instead of copying controllers into new projects. This
controllers. keeps the client contract stable while route families are peeled away
from the monolith fallback.
- **Two-provider EF.** SQLite and Postgres each have their own - **Two-provider EF.** SQLite and Postgres each have their own
EnsureCreated/Migrate dance because some Postgres-only column types EnsureCreated/Migrate dance because some Postgres-only column types
drift the model snapshot. The split is annoying but lets one drift the model snapshot. The split is annoying but lets one
+34 -25
View File
@@ -5,13 +5,12 @@ This branch has two deployable shapes:
- **Production/full API:** build the root `Dockerfile`, which runs - **Production/full API:** build the root `Dockerfile`, which runs
`DorkNet.Server`. This is still the runtime that serves every public `DorkNet.Server`. This is still the runtime that serves every public
Rec Room-compatible endpoint. Rec Room-compatible endpoint.
- **Microservices scaffold:** deploy `docker-compose.microservices.yml` - **Microservices:** deploy `docker-compose.microservices.yml`
for local testing or `docker-compose.microservices.dokploy.yml` on for local testing or `docker-compose.microservices.dokploy.yml` on
Dokploy. Both start `DorkNet.Gateway`, Dokploy. Both start `DorkNet.Gateway`, the dedicated service slices,
`DorkNet.Services.Identity`, `DorkNet.Services.Rooms`, the `DorkNet.Server` monolith fallback, Postgres, and Redis. The
`DorkNet.Services.Notify`, Postgres, and Redis. These hosts currently gateway routes public traffic to owned service slices and sends
expose internal health/capability endpoints only; public routes move unknown route families to the fallback.
behind the gateway incrementally.
Object storage is always a **separate S3-compatible instance**. The Object storage is always a **separate S3-compatible instance**. The
microservices compose files do not start MinIO/Garage. microservices compose files do not start MinIO/Garage.
@@ -120,10 +119,9 @@ service URLs returned to the client by `ConfigService`.
--- ---
## Dokploy: microservices scaffold ## Dokploy: microservices
Use this to run the new gateway/service-host layout. It is not yet a Use this to run the gateway/service-host layout.
replacement for the full public API.
Create a Dokploy **Compose** service from one of these files: Create a Dokploy **Compose** service from one of these files:
@@ -136,10 +134,17 @@ Both compose files provide:
| Service | Purpose | | Service | Purpose |
|---|---| |---|---|
| `gateway` | Edge host scaffold; service map and service health probes | | `gateway` | Edge reverse proxy; service map, route table, and service health probes |
| `identity` | Future identity/auth/accounts service | | `identity` | Auth, accounts, and platform-login route slice |
| `rooms` | Future rooms/matchmaking/discovery service | | `rooms` | Rooms, room keys, playlists, matchmaking, and discovery route slice |
| `notify` | Future notification/presence service | | `notify` | Notify, messages/chat, SignalR, and notification route slice |
| `content` | CDN paths, uploads, images, photos, room blobs, and storage route slice |
| `social` | Clubs, groups, announcements, player events, subscriptions route slice |
| `commerce` | Catalog, storefronts, econ, inventory, inventions route slice |
| `platform` | Service directory, config, version checks, geo, strings, and platform route slice |
| `moderation` | Bug reporting, player reporting, sanitize, admin API, and testcase route slice |
| `web` | Apex/www/admin/feed static hosts and site API route slice |
| `monolith` | Fallback full server for unknown route families not assigned to a service yet |
| `postgres` | Internal Postgres for the service network | | `postgres` | Internal Postgres for the service network |
| `redis` | Internal Redis for ephemeral state and fan-out | | `redis` | Internal Redis for ephemeral state and fan-out |
| `cloudflared` | Cloudflare Tunnel sidecar; Dokploy file only | | `cloudflared` | Cloudflare Tunnel sidecar; Dokploy file only |
@@ -155,6 +160,8 @@ Set these Dokploy Compose environment variables:
```env ```env
DORKNET_DOMAIN=yourdomain.com DORKNET_DOMAIN=yourdomain.com
DORKNET_JWT_SECRET=replace-with-at-least-64-random-characters
POSTGRES_PASSWORD=replace-with-a-random-db-password
CLOUDFLARE_TUNNEL_TOKEN=<token from Cloudflare Zero Trust tunnel> CLOUDFLARE_TUNNEL_TOKEN=<token from Cloudflare Zero Trust tunnel>
``` ```
@@ -165,15 +172,16 @@ yourdomain.com -> http://gateway:8080
*.yourdomain.com -> http://gateway:8080 *.yourdomain.com -> http://gateway:8080
``` ```
The wildcard covers `api`, `auth`, `rooms`, `notify`, and the other The wildcard covers `api`, `auth`, `rooms`, `notify`, `cdn`, `clubs`,
client-facing subdomains. The apex rule is separate because wildcard DNS `commerce`, and the other client-facing subdomains. The apex rule is
does not cover the root domain. Let Cloudflare create the tunnel DNS separate because wildcard DNS does not cover the root domain. Let
records, or create CNAME records to the tunnel target Cloudflare gives Cloudflare create the tunnel DNS records, or create CNAME records to the
you. tunnel target Cloudflare gives you.
When the gateway starts carrying the public API, every public DorkNet Every public DorkNet domain listed above should point to the gateway.
domain listed above should point to `gateway`. Until then, keep the real The gateway preserves the original Host header so service code still sees
player-facing domains on the full `DorkNet.Server` application. `auth.yourdomain.com`, `rooms.yourdomain.com`, and the other client
hosts.
Set these Compose environment variables in Dokploy: Set these Compose environment variables in Dokploy:
@@ -194,11 +202,12 @@ Useful smoke checks after deploy:
curl https://api.yourdomain.com/healthz curl https://api.yourdomain.com/healthz
curl https://api.yourdomain.com/internal/services curl https://api.yourdomain.com/internal/services
curl https://api.yourdomain.com/internal/services/health curl https://api.yourdomain.com/internal/services/health
curl https://api.yourdomain.com/internal/routes
``` ```
Expected during scaffold-only deploys: `/internal/services/health` `/internal/routes` shows which host/path patterns route to each service
reports gateway-visible status for identity, rooms, and notify. It does slice or the monolith fallback. `/internal/services/health` reports
not prove the full game API has moved yet. gateway-visible health for every backend service.
--- ---
@@ -235,7 +244,7 @@ S3__TimeoutSeconds=300
``` ```
For the microservices compose file, use the `DORKNET_S3_*` equivalents; For the microservices compose file, use the `DORKNET_S3_*` equivalents;
Compose maps them into `S3__*` for the rooms service. Compose maps them into `S3__*` for the service containers.
--- ---
+3 -2
View File
@@ -87,8 +87,9 @@ visitor → https://api.localhost/api/versioncheck/v4
→ Cloudflare edge (real LE cert, validated) → Cloudflare edge (real LE cert, validated)
→ cloudflared tunnel → cloudflared tunnel
→ cloudflared (running on your machine) → cloudflared (running on your machine)
→ https://localhost:443 (Host: api.localhost, mkcert cert) → http://gateway:8080 (Host preserved)
→ DorkNet.Server HostFiltering + subdomain-aware handlers → DorkNet.Gateway reverse proxy
→ dedicated service slice / monolith fallback
``` ```
The leg from cloudflared → localhost has `noTLSVerify: true` because The leg from cloudflared → localhost has `noTLSVerify: true` because