Reject a second live WebSocket for the same session name.

Two tabs with the cookie already set never POST /api/session, so name-online on login did not cover the design rule. Claim the name under the same lock as the online check.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 11:04:56 +03:00
co-authored by Cursor
parent d3396b5b8f
commit 1c3ae9ab7e
7 changed files with 219 additions and 6 deletions
+30 -3
View File
@@ -7,6 +7,7 @@ namespace HSchool.Server.Net;
internal sealed class ClientRegistry
{
private readonly ConcurrentDictionary<uint, GameClient> _clients = new();
private readonly object _names = new();
private uint _nextPlayerId;
public int Count => _clients.Count;
@@ -28,14 +29,40 @@ internal sealed class ClientRegistry
public bool IsUserNameOnline(string normalizedUserName)
{
foreach (var client in _clients.Values)
lock (_names)
{
if (client.UserName is null)
return HasUserName(normalizedUserName);
}
}
/// <summary>
/// One live socket per name. Checked under the same lock as <see cref="IsUserNameOnline"/>
/// so a second tab cannot sneak a Welcome in between the HTTP check and SetUserName.
/// </summary>
public bool TryClaimUserName(GameClient client, string userName)
{
lock (_names)
{
if (HasUserName(userName))
{
return false;
}
client.SetUserName(userName);
return true;
}
}
private bool HasUserName(string normalizedUserName)
{
foreach (var existing in _clients.Values)
{
if (existing.UserName is null)
{
continue;
}
if (string.Equals(client.NormalizedUserName, normalizedUserName, StringComparison.OrdinalIgnoreCase))
if (string.Equals(existing.NormalizedUserName, normalizedUserName, StringComparison.OrdinalIgnoreCase))
{
return true;
}