using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using GameCult.Caching; using GameCult.Logging; using Isopoh.Cryptography.Argon2; using LiteNetLib; using MessagePack; using R3; namespace GameCult.Networking { public class Server { private const string EmailPattern = @"^([0-9a-zA-Z]([\+\-_\.][0-9a-zA-Z]+)*)+@(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]*\.)+[a-zA-Z0-9]{2,17})$"; private const string UsernamePattern = @"^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$"; private const int ServerPort = 3075; private const float SessionTimeoutSeconds = 1800; // 30 minutes private const int MaxLoginAttemptsPerMinute = 5; private readonly ConcurrentDictionary _messageDelegates = new(); // Aggregated multicast Action per type private readonly ConcurrentDictionary _users = new(); private readonly ConcurrentDictionary _sessions = new(); private readonly ConcurrentDictionary _sessionToUserId = new(); private readonly ConcurrentDictionary> _loginAttempts = new(); // IP to attempt timestamps private NetManager _netManager; private Stopwatch _timer; private readonly CultCache _database; private ILogger _logger = new NullLogger(); public ILogger Logger { get => _logger; set => _logger = value ?? new NullLogger(); } private float Time => (float)_timer.Elapsed.TotalSeconds; public Server(CultCache cache) { _database = cache; _database.RegisterIndex("Email"); Observable.Timer(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60)) .Subscribe(_ => CleanupExpiredSessions()); } public bool IsValidEmail(string email) => Regex.IsMatch(email, EmailPattern); public bool IsValidUsername(string name) => Regex.IsMatch(name, UsernamePattern); public void ClearMessageListeners() { _messageDelegates.Clear(); } public void AddMessageListener(Action callback) where T : Message { var type = typeof(T); // Atomically aggregate into multicast delegate (thread-safe via CAS) _messageDelegates.AddOrUpdate(type, _ => callback, // First callback (t, current) => { // Combine with existing (supports multiple callbacks) var combined = Delegate.Combine(current, callback) as Action; return combined ?? throw new InvalidOperationException($"Failed to combine delegates for {t.Name}"); }); } public void RemoveMessageListener(Action callback) where T : Message { if (_messageDelegates.TryGetValue(typeof(T), out var currentDelegate)) { var newDelegate = Delegate.Remove(currentDelegate, callback) as Action; _messageDelegates[typeof(T)] = newDelegate; // Null if empty } } public void Stop() { _netManager.Stop(); } public void Start() { _timer = Stopwatch.StartNew(); var listener = new EventBasedNetListener(); _netManager = new NetManager(listener) { UnsyncedEvents = true, NatPunchEnabled = true }; _netManager.Start(ServerPort); listener.NetworkErrorEvent += (point, code) => Logger.LogInfo($"{point.Address}: Error {code}"); listener.ConnectionRequestEvent += request => { if (CheckRateLimit(request.RemoteEndPoint.Address.ToString())) request.AcceptIfKey(Secret.ConnectionKey); else request.Reject(); }; listener.PeerConnectedEvent += peer => { Logger.LogInfo($"User Connected: {peer.Address}"); _users.TryAdd(peer.Id, new User { Peer = peer }); }; listener.PeerDisconnectedEvent += (peer, info) => { Logger.LogInfo($"User Disconnected: {peer.Address}"); if (_users.TryRemove(peer.Id, out var user) && user.SessionGuid != Guid.Empty) { _sessions.TryRemove(user.SessionGuid, out _); _sessionToUserId.TryRemove(user.SessionGuid, out _); } }; listener.NetworkLatencyUpdateEvent += (peer, latency) => { if (_users.TryGetValue(peer.Id, out var user)) user.Latency = latency; }; listener.NetworkReceiveEvent += async (peer, reader, channel, method) => { try { var bytes = reader.GetRemainingBytes(); var message = MessagePackSerializer.Deserialize(bytes); Logger.LogDebug($"Received message: {MessagePackSerializer.ConvertToJson(new ReadOnlyMemory(bytes))}"); if (message == null) return; message.Peer = peer; var user = _users.GetOrAdd(peer.Id, _ => new User { Peer = peer }); if (message is LoginMessage or RegisterMessage or VerifyMessage) { if (IsVerified(user)) { var nonce = Secret.NewNonce; peer.Send(new LoginSuccessMessage { Nonce = nonce, Session = Secret.EncryptBytes(user.SessionGuid.ToByteArray(), nonce) }); return; } Guid sessionGuid; if (message is LoginMessage or RegisterMessage && !CheckRateLimit(peer.Address.ToString())) { peer.Send(new ErrorMessage { Error = "Too Many Attempts" }); return; } switch (message) { case RegisterMessage register: { var name = Secret.DecryptString(register.Name, register.Nonce); var email = Secret.DecryptString(register.Email, register.Nonce); if (!IsValidUsername(name)) { peer.Send(new ErrorMessage { Error = "Username Invalid" }); return; } if (!IsValidEmail(email)) { peer.Send(new ErrorMessage { Error = "Email Invalid" }); return; } if (_database.GetIdByIndex("Email", email) != null) { peer.Send(new ErrorMessage { Error = "Email Taken" }); return; } if (_database.GetIdByName(name) != null) { peer.Send(new ErrorMessage { Error = "Username Taken" }); return; } sessionGuid = GenerateSessionGuid(); var password = Secret.DecryptString(register.Password, register.Nonce); var newUserData = new PlayerData { ID = Guid.NewGuid(), Email = email, PasswordHash = Argon2.Hash(password, memoryCost: 16384), Username = name }; await _database.AddAsync(newUserData); // Immediate cache + async file write _sessions.TryAdd(sessionGuid, new Session { Data = newUserData, LastUpdate = DateTime.Now }); _sessionToUserId[sessionGuid] = peer.Id; var nonce = Secret.NewNonce; peer.Send(new LoginSuccessMessage { Nonce = nonce, Session = Secret.EncryptBytes(sessionGuid.ToByteArray(), nonce) }); if (_users.TryGetValue(peer.Id, out var u1)) u1.SessionGuid = sessionGuid; break; } case VerifyMessage verify: var session = new Guid(Secret.DecryptBytes(verify.Session, verify.Nonce)); if (!_sessions.ContainsKey(session)) { peer.Send(new ErrorMessage { Error = "Session Not Found" }); return; } if (_users.TryGetValue(peer.Id, out var u2)) { u2.SessionGuid = session; var nonce = Secret.NewNonce; peer.Send(new LoginSuccessMessage { Nonce = nonce, Session = Secret.EncryptBytes(session.ToByteArray(), nonce) }); } break; case LoginMessage login: { var auth = Secret.DecryptString(login.Auth, login.Nonce); var isEmail = IsValidEmail(auth); var userData = isEmail ? _database.GetByIndex("Email", auth) : _database.GetByName(auth); if (userData == null) { peer.Send(new ErrorMessage { Error = isEmail ? "Email Not Found" : "Username Not Found" }); return; } var password = Secret.DecryptString(login.Password, login.Nonce); if (!Argon2.Verify(userData.PasswordHash, password)) { peer.Send(new ErrorMessage { Error = "Password Incorrect" }); return; } sessionGuid = GenerateSessionGuid(); _sessions.TryAdd(sessionGuid, new Session { Data = userData, LastUpdate = DateTime.Now }); _sessionToUserId[sessionGuid] = peer.Id; var nonce = Secret.NewNonce; peer.Send(new LoginSuccessMessage { Nonce = nonce, Session = Secret.EncryptBytes(sessionGuid.ToByteArray(), nonce) }); if (_users.TryGetValue(peer.Id, out var u3)) u3.SessionGuid = sessionGuid; break; } } } else if (IsVerified(user)) { // Zero-reflection dispatch via cached multicast delegate if (_messageDelegates.TryGetValue(message.GetType(), out var del) && del != null) del.DynamicInvoke(message); else Logger.LogWarning($"No listener for {message.GetType().Name}"); if (_sessions.TryGetValue(user.SessionGuid, out var session)) session.LastUpdate = DateTime.Now; } else { peer.Send(new ErrorMessage { Error = "User Not Verified" }); } } catch (Exception ex) { Logger.LogError($"Message processing failed: {ex.Message}"); } }; AddMessageListener(async (message) => { if (!IsValidUsername(message.Name)) { message.Peer.Send(new ErrorMessage { Error = "Username Invalid" }); return; } if (_database.GetIdByName(message.Name) != null) { message.Peer.Send(new ErrorMessage { Error = "Username Taken" }); return; } if (_users.TryGetValue(message.Peer.Id, out var user)) { var data = SessionData(user); data.Username = message.Name; await _database.AddAsync(data); } }); Logger.LogInfo($"Server started on port {ServerPort}."); } private bool IsVerified(User u) => u != null && _sessions.ContainsKey(u.SessionGuid); private PlayerData SessionData(User user) => IsVerified(user) ? _sessions[user.SessionGuid].Data : null; private bool CheckRateLimit(string ip) { var now = DateTime.Now; var windowStart = now.AddMinutes(-1); _loginAttempts.AddOrUpdate( ip, _ => new Queue(new[] { now }), (_, queue) => { // Remove attempts older than 1 minute while (queue.Count > 0 && queue.Peek() < windowStart) queue.Dequeue(); queue.Enqueue(now); return queue; }); return _loginAttempts[ip].Count <= MaxLoginAttemptsPerMinute; } private Guid GenerateSessionGuid() { var guid = Guid.NewGuid(); // TODO: Implement HMAC signing for session validation return guid; } private void CleanupExpiredSessions() { foreach (var session in _sessions.ToArray()) { if ((DateTime.Now - session.Value.LastUpdate).TotalSeconds > SessionTimeoutSeconds) { _sessions.TryRemove(session.Key, out _); if (_sessionToUserId.TryRemove(session.Key, out var userId) && _users.TryGetValue(userId, out var user)) { user.Peer.Disconnect(); _users.TryRemove(userId, out _); } } } } } public class Session { public DateTime LastUpdate; public PlayerData Data; } public class User { public NetPeer Peer; public int Latency; public Guid SessionGuid; } }