Publish Ludarium source
Public source validation / source (push) Successful in 2m16s

This commit is contained in:
Ludarium release export
2026-09-03 02:08:58 +02:00
commit df869819ce
339 changed files with 46455 additions and 0 deletions
@@ -0,0 +1,350 @@
using System.Net;
using Ludarium.Api;
using Ludarium.Application;
using Microsoft.AspNetCore.Http;
namespace Ludarium.E2E;
/// <summary>
/// The shared transport behind every isolated native remote player. These assertions previously
/// existed once per emulator; keeping them here is what guarantees a new player inherits the same
/// pinned TLS, opaque capability, cookie scope and revocation behaviour.
/// </summary>
public sealed class NativeRemotePlayerTests
{
private const string CertificateHash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
private static NativeRemotePlayer Switch => NativeRemotePlayerRegistry.Switch;
private static NativeRemotePlayer Dolphin => NativeRemotePlayerRegistry.Dolphin;
[Theory]
[InlineData("switch", "eden", "/switch-player", "ludarium-switch-session", 1231)]
[InlineData("dolphin", "dolphin", "/dolphin-player", "ludarium-dolphin-session", 1232)]
public void RegistryDrivesEveryPublicIdentifier(string key, string host, string proxyPath,
string cookie, int port)
{
var player = NativeRemotePlayerRegistry.ForKey(key)!;
Assert.Equal(host, player.ProxyHost);
Assert.Equal(proxyPath, player.ProxyPath);
Assert.Equal(cookie, player.CookieName);
Assert.Equal(port, player.DefaultPublicPort);
Assert.Equal($"{key}-player-sessions", player.SessionRoute);
}
[Fact]
public void EveryNativePlatformResolvesToExactlyOnePlayer()
{
Assert.Same(Switch, NativeRemotePlayerRegistry.ForPlatform("switch"));
Assert.Same(Dolphin, NativeRemotePlayerRegistry.ForPlatform("GameCube"));
Assert.Same(Dolphin, NativeRemotePlayerRegistry.ForPlatform("wii"));
Assert.Null(NativeRemotePlayerRegistry.ForPlatform("ps2"));
var owners = NativeRemotePlayerRegistry.Platforms
.Select(platform => NativeRemotePlayerRegistry.All.Count(player => player.Owns(platform)));
Assert.All(owners, count => Assert.Equal(1, count));
}
[Fact]
public void AGameNamingTwoNativePlatformsIsDeliberatelyUnresolvable()
{
Assert.Equal("wii", NativeRemotePlayerRegistry.ResolvePlatform(["wii", "wii", null]));
Assert.Null(NativeRemotePlayerRegistry.ResolvePlatform(["wii", "gamecube"]));
Assert.Null(NativeRemotePlayerRegistry.ResolvePlatform(["windows", "ps2"]));
}
[Fact]
public void ProxyRequiresHttpsPinnedCertificateServerSideCredentialsAndBoundedPort()
{
Assert.False(Options(Switch, "http://eden:3001/").Configured);
Assert.False(Options(Switch, "https://eden:3001/", username: null).Configured);
Assert.False(Options(Switch, "https://eden:3001/", certificate: null).Configured);
Assert.False(Options(Switch, "https://eden:3001/path").Configured);
Assert.True(Options(Switch, "https://eden:3001/").Configured);
}
[Fact]
public void ProxyOnlyAcceptsItsOwnInternalSidecarHost()
{
Assert.False(Options(Dolphin, "https://other:3001/").Configured);
Assert.False(Options(Dolphin, "https://eden:3001/").Configured);
Assert.True(Options(Dolphin, "https://dolphin:3001/").Configured);
}
[Fact]
public void LaunchOriginUsesTheRequestHostAndDedicatedPort()
{
var options = Options(Switch, "https://eden:3001/", port: 4321);
var request = new DefaultHttpContext().Request;
request.Scheme = "https";
request.Host = new HostString("games.example.test", 1230);
Assert.Equal("https://games.example.test:4321/switch-player/", options.BuildLaunchUrl(request));
}
[Fact]
public void PublicPlayerOriginMustBeHttpsAndAChildOfTheRequestHost()
{
var request = new DefaultHttpContext().Request;
request.Scheme = "http";
request.Host = new HostString("ludarium.example");
var options = Options(Switch, "https://eden:3001/", port: 4321,
origin: "https://player.ludarium.example/");
Assert.Equal("https://player.ludarium.example/switch-player/", options.BuildLaunchUrl(request));
Assert.Equal("https://player.ludarium.example/switch-player/", options.BuildExternalUrl(request));
Assert.Equal("ludarium.example", options.CookieDomain(request));
Assert.True(options.AcceptsProxyRequest(RequestFor("player.ludarium.example")));
Assert.False(options.AcceptsProxyRequest(RequestFor("ludarium.example")));
Assert.Null(Options(Switch, "https://eden:3001/", origin: "http://player.ludarium.example/").PublicOrigin);
Assert.Throws<InvalidOperationException>(() =>
Options(Switch, "https://eden:3001/", origin: "https://player.example.net/").BuildLaunchUrl(request));
}
[Fact]
public void DolphinPublicOriginMustBeAChildOfTheMainHost()
{
var options = Options(Dolphin, "https://dolphin:3001/", origin: "https://dolphin.ludarium.test/");
var request = new DefaultHttpContext().Request;
request.Scheme = "https";
request.Host = new HostString("ludarium.test");
Assert.Equal("https://dolphin.ludarium.test/dolphin-player/", options.BuildLaunchUrl(request));
}
[Fact]
public void SessionsAreOpaqueBoundedExpiringAndExplicitlyEnded()
{
var time = new MutableTimeProvider(new DateTimeOffset(2026, 8, 11, 20, 0, 0, TimeSpan.Zero));
var sessions = new NativeRemotePlayerSessionStore(Switch, time);
var first = sessions.Create(Guid.NewGuid(), "http://host:1231/switch-player/", null);
var second = sessions.Create(Guid.NewGuid(), "http://host:1231/switch-player/", null);
Assert.True(sessions.Authorizes(first.Token));
Assert.False(sessions.Authorizes(first.Token + "changed"));
Assert.Throws<NativeRemotePlayerCapacityException>(() =>
sessions.Create(Guid.NewGuid(), "http://host:1231/switch-player/", null));
Assert.False(sessions.End(Guid.NewGuid()));
Assert.True(sessions.End(first.Session.Id));
Assert.False(sessions.Authorizes(first.Token));
time.Advance(NativeRemotePlayerSessionStore.SessionLifetime.Add(TimeSpan.FromSeconds(1)));
Assert.False(sessions.Authorizes(second.Token));
Assert.False(sessions.HasActiveSession);
Assert.NotNull(sessions.Create(Guid.NewGuid(), "http://host:1231/switch-player/", null));
}
[Fact]
public void CapacityFollowsThePlayerDescription()
{
var sessions = new NativeRemotePlayerSessionStore(Dolphin, TimeProvider.System);
var ticket = sessions.Create(Guid.NewGuid(), "https://ludarium.test:1232/dolphin-player/", null, "wii");
Assert.Equal("Dolphin", ticket.Session.Emulator);
Assert.Equal("wii", ticket.Session.Platform);
Assert.True(ticket.Session.ExactGameLaunch);
Assert.Equal("native-and-savestate-persistent", ticket.Session.SaveMode);
Assert.Throws<NativeRemotePlayerCapacityException>(() =>
sessions.Create(Guid.NewGuid(), "https://ludarium.test:1232/dolphin-player/", null, "wii"));
Assert.True(sessions.End(ticket.Session.Id));
Assert.False(sessions.Authorizes(ticket.Token));
}
[Fact]
public void SessionCookieIsHttpOnlyProxyScopedAndStrict()
{
var sessions = new NativeRemotePlayerSessionStore(Switch, TimeProvider.System);
var cookie = sessions.Cookie(DateTimeOffset.UtcNow.AddHours(1), secure: true);
Assert.True(cookie.HttpOnly);
Assert.True(cookie.Secure);
Assert.Equal(SameSiteMode.Strict, cookie.SameSite);
Assert.Equal("/switch-player", cookie.Path);
Assert.Null(cookie.Domain);
Assert.Equal(DateTimeOffset.UnixEpoch, sessions.ExpiredCookie(true).Expires);
}
[Theory]
[InlineData(null, false)]
[InlineData("not-a-hash", false)]
[InlineData("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", true)]
public void RuntimeQualificationRequiresAnExactFixtureDigest(string? digest, bool expected) =>
Assert.Equal(expected, NativeRemotePlayerControlClient.IsValidSha256(digest));
[Fact]
public void ControlConfigurationOnlyAcceptsTheInternalSidecarOrigin()
{
Assert.True(Control(Switch, "http://eden:8765/", "secret").Configured);
Assert.False(Control(Switch, "https://eden:8765/", "secret").Configured);
Assert.False(Control(Switch, "http://other:8765/", "secret").Configured);
Assert.False(Control(Switch, "http://eden:8765/control/", "secret").Configured);
Assert.False(Control(Switch, "http://eden:8765/", null).Configured);
}
[Fact]
public void APlayerThatRequiresAFixtureStaysUnconfiguredWithoutOne()
{
Assert.False(Control(Dolphin, "http://dolphin:8765/", "secret").Configured);
Assert.True(Control(Dolphin, "http://dolphin:8765/", "secret", CertificateHash).Configured);
// Switch has no fixture gate, so a digest is neither required nor consulted.
Assert.True(Control(Switch, "http://eden:8765/", "secret").Configured);
}
[Fact]
public async Task LaunchUsesTheAuthenticatedFixedControllerEndpoint()
{
Uri? observedUri = null;
string? observedToken = null;
string? observedBody = null;
var control = Control(Switch, "http://eden:8765/", "separate-secret", handler: request =>
{
observedUri = request.RequestUri;
observedToken = request.Headers.GetValues("X-Ludarium-Control-Token").Single();
observedBody = request.Content!.ReadAsStringAsync().GetAwaiter().GetResult();
return new HttpResponseMessage(HttpStatusCode.Accepted);
});
await control.LaunchAsync("Animal Crossing/base.xci", CancellationToken.None);
Assert.Equal(new Uri("http://eden:8765/v1/launch"), observedUri);
Assert.Equal("separate-secret", observedToken);
Assert.Contains("Animal Crossing/base.xci", observedBody);
}
[Fact]
public void CatalogPrefixVariablesMatchTheDeployedConfiguration()
{
Assert.Equal("CATALOG_PREFIX", NativeRemotePlayerControlClient.CatalogPrefixVariable(Switch, "switch"));
Assert.Equal("GAMECUBE_CATALOG_PREFIX",
NativeRemotePlayerControlClient.CatalogPrefixVariable(Dolphin, "gamecube"));
Assert.Equal("WII_CATALOG_PREFIX", NativeRemotePlayerControlClient.CatalogPrefixVariable(Dolphin, "wii"));
Assert.Equal("LUDARIUM_SWITCH", Switch.EnvironmentPrefix);
Assert.Equal("LUDARIUM_DOLPHIN", Dolphin.EnvironmentPrefix);
}
[Fact]
public async Task AConfiguredButUnreachableRuntimeIsNotReported()
{
var control = Control(Dolphin, "http://dolphin:8765/", "secret", CertificateHash,
_ => throw new HttpRequestException("connection refused"));
var status = await control.GetStatusAsync(CancellationToken.None);
Assert.False(status.Reachable);
Assert.False(status.Running);
Assert.False(status.SaveDataSupported);
}
[Fact]
public async Task ALiveRuntimeReportsItsRunningTitleAndSaveDataSupport()
{
var control = Control(Dolphin, "http://dolphin:8765/", "secret", CertificateHash,
_ => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(
"""{"running":true,"pid":42,"saveMode":"native-and-savestate-persistent","saveData":true}""")
});
var status = await control.GetStatusAsync(CancellationToken.None);
Assert.True(status.Reachable);
Assert.True(status.Running);
Assert.True(status.SaveDataSupported);
Assert.Equal("native-and-savestate-persistent", status.SaveMode);
}
[Fact]
public async Task AHangingRuntimeCannotStallACapabilityRead()
{
var control = Control(Dolphin, "http://dolphin:8765/", "secret", CertificateHash, _ =>
{
// A sidecar that accepts the connection and never answers must not hold up a catalog page.
Thread.Sleep(NativeRemotePlayerControlClient.HealthProbeTimeout + TimeSpan.FromSeconds(3));
return new HttpResponseMessage(HttpStatusCode.OK);
});
var started = System.Diagnostics.Stopwatch.StartNew();
var status = await control.GetStatusAsync(CancellationToken.None);
started.Stop();
Assert.False(status.Reachable);
Assert.True(started.Elapsed < TimeSpan.FromSeconds(10), $"probe took {started.Elapsed}");
}
[Fact]
public async Task AnUnconfiguredRuntimeIsNeverProbed()
{
var probes = 0;
var control = Control(Dolphin, "http://dolphin:8765/", "secret", fixture: null, handler: _ =>
{
probes++;
return new HttpResponseMessage(HttpStatusCode.OK);
});
Assert.False((await control.GetStatusAsync(CancellationToken.None)).Reachable);
Assert.Equal(0, probes);
}
[Fact]
public async Task EndingASessionStopsTheTitleBestEffort()
{
var actions = new List<string>();
var control = Control(Switch, "http://eden:8765/", "secret", handler: request =>
{
actions.Add(request.Content!.ReadAsStringAsync().GetAwaiter().GetResult());
return new HttpResponseMessage(HttpStatusCode.Accepted);
});
Assert.True(await control.TryStopAsync(CancellationToken.None));
Assert.Contains("stop", Assert.Single(actions));
}
[Fact]
public async Task AFailingStopNeverBreaksSessionRevocation()
{
var control = Control(Switch, "http://eden:8765/", "secret",
handler: _ => new HttpResponseMessage(HttpStatusCode.Conflict));
Assert.False(await control.TryStopAsync(CancellationToken.None));
}
[Fact]
public void EveryPlayerActionIsAllowlisted()
{
Assert.Contains("stop", Switch.Actions);
Assert.DoesNotContain("save-state", Switch.Actions);
Assert.False(Switch.SupportsSavestates);
Assert.Contains("save-state", Dolphin.Actions);
Assert.Contains("load-state", Dolphin.Actions);
Assert.True(Dolphin.SupportsSavestates);
}
private static NativeRemotePlayerProxyOptions Options(NativeRemotePlayer player, string destination,
string? username = "user", string? certificate = CertificateHash, int? port = null, string? origin = null) =>
new(player, new Uri(destination), null, username, "pass", port, certificate,
origin is null ? null : new Uri(origin));
private static NativeRemotePlayerControlClient Control(NativeRemotePlayer player, string address,
string? token, string? fixture = null, Func<HttpRequestMessage, HttpResponseMessage>? handler = null) =>
new(player, new HttpClient(new StubHandler(handler ?? (_ => new HttpResponseMessage(HttpStatusCode.Accepted))))
{ BaseAddress = new Uri(address) }, token,
player.Platforms.ToDictionary(platform => platform, platform => $"roms/{platform}"), fixture);
private static HttpRequest RequestFor(string host)
{
var request = new DefaultHttpContext().Request;
request.Host = new HostString(host);
return request;
}
private sealed class MutableTimeProvider(DateTimeOffset now) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => now;
public void Advance(TimeSpan amount) => now = now.Add(amount);
}
private sealed class StubHandler(Func<HttpRequestMessage, HttpResponseMessage> response) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken) => Task.FromResult(response(request));
}
}