Skip to content

feat(MMS): add WebSocket endpoints and restructure service layer#41

Open
Liparakis wants to merge 4 commits intoExtremelyd1:mainfrom
Liparakis:MMS-WebSockets
Open

feat(MMS): add WebSocket endpoints and restructure service layer#41
Liparakis wants to merge 4 commits intoExtremelyd1:mainfrom
Liparakis:MMS-WebSockets

Conversation

@Liparakis
Copy link
Copy Markdown
Contributor

Matchmaking changes (the important part)

The main change is introducing JoinSession.

Before, a join attempt was spread across multiple places:

  • lobby state
  • various tokens
  • no socket for clients

There was no single place responsible for a join attempt.

Now there is.

A JoinSession represents a single join attempt and keeps everything together:

  • join ID
  • target lobby
  • client IP
  • discovery token
  • external port once resolved
  • WebSocket
  • expiry

Bootstrap

Pulled all the startup logic out of Program.cs and split it into focused pieces:

  • ServiceCollectionExtensions handles all Dependencies injection registration.
  • WebApplicationExtensions sets up the middleware
  • HttpsCertificateConfigurator deals with HTTPS and Kestrel config
  • ProgramState holds shared runtime state that used to be scattered

Program.cs is now just wiring things together instead of doing everything itself.

Features

Routing and handler logic are no longer mixed:

  • EndpointRouteBuilderExtensions registers routes
  • HealthEndpoints exposes /health
  • LobbyEndpoints defines routes
  • LobbyEndpointHandlers contains the actual logic

Version validation is now centralized:

  • MatchmakingVersionValidation

New WebSocket endpoints are:

  • /ws/{token}
  • /ws/join/{joinId}

Contracts

All request and response DTOs are grouped:

  • Requests.cs
  • Responses.cs

HTTP helpers

  • EndpointBuilder wraps common MapGet and MapPost patterns to reduce repetition and keep validation consistent

Network

  • UdpDiscoveryService handles UDP discovery and feeds into join sessions
  • WebSocketManager tracks active connections

Lobby

Lobby responsibilities are split cleanly:

  • LobbyService contains core logic
  • LobbyCleanupService handles background cleanup
  • LobbyNameService manages naming

Note

AI helped with extracting parts of the code and documentation, but the design and decisions were done manually.
Some comments might be a bit misleading but i am happy to correct them any.

- Swapped to join sessions
- Add WebSocket endpoint and connection management
- Reorganize lobby/matchmaking services into subfolders
- Update bootstrap, contracts, and endpoint routing
- Remove obsolete top-level service files (moved to subfolders)
@Liparakis Liparakis marked this pull request as draft March 19, 2026 00:38
@Liparakis Liparakis marked this pull request as ready for review March 19, 2026 00:38
…eanup logs

feat: add PrivacyFormatter utility class
chore: redact sensitive data from logs in production
Copy link
Copy Markdown
Owner

@Extremelyd1 Extremelyd1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, I haven't tested it functionally yet, but have left a bunch of comments for either requested changes or questions on why some things are the way they are.

Didn't we discuss you were going to deliver the changes in 3 PRs? If so, is this only the first PR or is this just everything. Because it feels like everything is in here.

private static bool TryCreateCertificate(string pem, string key, out X509Certificate2? certificate) {
certificate = null;
try {
using var ephemeralCertificate = X509Certificate2.CreateFromPem(pem, key);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This already returns a X509Certificate2, which is the type we need for the certificate. Why do we then export it and load it again through another class?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateFromPem already returns the right X509Certificate2. The reason we export and re-import is not to change the type, but to force the private key into Windows-backed key storage. On Windows PEM-created certificates can have an ephemeral private key that works in code but is unreliable for server TLS. Re-importing as PKCS#12 with key storage flags transforms the key in a form Kestrel without potential issues. Sources: CreateFromPemFile docs, LoadPkcs12 docs, dotnet/runtime#93319.

/// Initializes static members of the <see cref="HttpsCertificateConfigurator"/> class.
/// </summary>
static HttpsCertificateConfigurator() {
using var loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(o => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we use the logger from ProgramState? It is not set yet when this class is used to configure the HTTPS certificate, but see the other comments about how to can be better centralized.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but see the other comments about how this can be better centralized.

I’m not entirely sure what you mean here.

I’ve moved the small factory from HttpCert... into a helper in Program.cs. We can't use the "final" logger anyway, since it’s only assigned after the app is built.

At this point, this approach felt like the simplest and most practical fix, rather than introducing a more complex solution for something this small.

Comment on lines +87 to +98
foreach (var proxy in configuration.GetSection("ForwardedHeaders:KnownProxies").Get<string[]>() ?? []) {
if (IPAddress.TryParse(proxy, out var address))
options.KnownProxies.Add(address);
}

foreach (var network in configuration.GetSection("ForwardedHeaders:KnownNetworks").Get<string[]>() ??
[]) {
if (!TryParseNetwork(network, out var ipNetwork))
continue;

options.KnownNetworks.Add(ipNetwork);
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for trusting forwarded headers only from your proxy (i assumed there is one.. perhaps NginX). Since MMS sits behind proxy, ASP.NET Core needs to know which proxy IPs/networks are allowed to supply X-Forwarded-For and related headers, so client IP detection, HTTPS handling and IP-based rate limiting work correctly and cannot be spoofed by direct requests.

Comment on lines +6 to +7
internal static class WebApplicationExtensions
{
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect style formatting for brackets in this class.

Comment on lines +8 to +9
internal static class Requests
{
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect style formatting for brackets.


/// <summary>
/// Secondary index for efficient lookup of join sessions by lobby connection data.
/// Valus are dummy bytes to use <see cref="ConcurrentDictionary{TKey, TValue}"/> as a set.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Valus are dummy bytes to use <see cref="ConcurrentDictionary{TKey, TValue}"/> as a set.
/// Values are dummy bytes to use <see cref="ConcurrentDictionary{TKey, TValue}"/> as a set.

/// <summary>
/// Secondary index for efficient lookup of expired sessions.
/// </summary>
private readonly SortedSet<(DateTime expiresAtUtc, string joinId)> _expiryIndex = new();
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private readonly SortedSet<(DateTime expiresAtUtc, string joinId)> _expiryIndex = new();
private readonly SortedSet<(DateTime expiresAtUtc, string joinId)> _expiryIndex = [];


/// <summary>
/// Binds a <see cref="UdpClient"/> to <see cref="Port"/> and enters a receive loop
/// until <paramref name="stoppingToken"/> is cancelled by the hosting infrastructure.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// until <paramref name="stoppingToken"/> is cancelled by the hosting infrastructure.
/// until <paramref name="stoppingToken"/> is canceled by the hosting infrastructure.

Comment on lines +13 to +14
internal static class WebSocketMessenger
{
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect style formatting for brackets in this file.

Comment on lines +16 to +87
/// <summary>
/// A collection of humorous placeholder strings used as redaction substitutes.
/// </summary>
private static readonly string[] RedactedVariants = [
"[None of your business]",
"[Nice try]",
"[A place far, far away]",
"[A galaxy far, far away]",
"[Over the rainbow]",
"[Somewhere out there]",
"[If I told you, I'd have to delete you]",
"[Ask your mom]",
"[Classified]",
"[Behind you]",
"[¯\\_(ツ)_/¯]",
"[What are you looking for?]",
"[Error 404: Address Not Found]",
"[It's a secret... to everybody]",
"[Have you tried turning it off and on again?]",
"[SomewhereOnEarth™]",
"[Undisclosed location]",
"[The void]",
"[Mind your own business.exe]",
"[This information self-destructed]",
"[The FBI knows, but they won't tell you either]",
"[Schrodinger's Endpoint: both here and not here]",
"[Currently unavailable due to something idk]",
"[My other endpoint is a Porsche]",
"[Would you like to know more? Too bad :)]",
"[Somewhere between 0.0.0.0 and 255.255.255.255]",
"[Location redacted by the order of the cats]",
"[Lost in the abyss that lies in-between the couch cushions]",
"[In a parallel universe, slightly to the left]",
"[sudo show address -> Permission denied]",
"[This endpoint does not exist. Never did. Move along.]",
"[Carrier pigeon lost en route]",
"[The address is a lie]",
"[Currently on vacation. Please try again never.]",
"[I am not the endpoint you are looking for]",
"[¿Qué? No hablo 'your concern'.]",
"[It's giving... nothing. IT'S GIVING NOTHING!]",
"[Endpoint entered witness protection]",
"[We asked it nicely to stay hidden]",
"[Loading... just kidding]",
"[no]",
"[This message will self-destruct in... oh wait, it already did]",
"[Somewhere, where the WiFi is better]",
"[Behind seven proxies]",
"[In the cloud (no, not that cloud, the fluffy kind)]",
"[Ask Clippy]",
"[Encrypted with vibes]",
"[This field intentionally left blank - lol no it isn't]",
"[IP? More like... never mind.]",
"[Gone fishing 🎣]",
"[The address got up and walked away]",
"[We hid it really well this time]",
"[Not on this network. Possibly not on this planet.]",
"[git blame won't help you here]",
"[According to my lawyer, no comment]",
"[Why do you ask? 👀]",
"[Hidden in plain sight... except not plain, and not in sight]",
"[Bounced through 47 VPNs and counting...48..49]"
];

/// <summary>
/// Gets a redaction placeholder string. Returns a random humorous variant with
/// 1% probability; otherwise returns <c>[Redacted]</c>.
/// </summary>
private static string RedactedPlaceholder =>
Random.Shared.NextDouble() < 0.50
? RedactedVariants[Random.Shared.Next(RedactedVariants.Length)]
: "[Redacted]";
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, this only clutters up log files. Currently the probability isn't even set to 1%, but still I don't like this.
If this was anything client-facing, where users would see this I'd appreciate the effort and allowed the change, but most likely, I'm the only one going to see this.

@Liparakis Liparakis requested a review from Extremelyd1 March 22, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants