Files
h-school/src/HSchool.Server/Net/ClientRegistry.cs
T
Leonid PershinandCursor 1c3ae9ab7e 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>
2026-08-20 11:04:56 +03:00

74 lines
2.0 KiB
C#

using System.Collections.Concurrent;
using System.Net.WebSockets;
namespace HSchool.Server.Net;
/// <summary>Tracks live connections and hands out client ids.</summary>
internal sealed class ClientRegistry
{
private readonly ConcurrentDictionary<uint, GameClient> _clients = new();
private readonly object _names = new();
private uint _nextPlayerId;
public int Count => _clients.Count;
/// <summary>Snapshot-free enumeration; safe because the dictionary is concurrent.</summary>
public IEnumerable<GameClient> All => _clients.Values;
public GameClient Add(WebSocket socket)
{
var playerId = Interlocked.Increment(ref _nextPlayerId);
var client = new GameClient(playerId, socket);
_clients[playerId] = client;
return client;
}
public GameClient? Find(uint playerId) => _clients.GetValueOrDefault(playerId);
public void Remove(uint playerId) => _clients.TryRemove(playerId, out _);
public bool IsUserNameOnline(string normalizedUserName)
{
lock (_names)
{
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(existing.NormalizedUserName, normalizedUserName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}