From 104bdf590b200db95cacdcd1c4f13176e13611c7 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sat, 30 Jun 2018 20:55:16 -0500 Subject: [PATCH] moved validate command into shared library. reworked connection system to read from log file for join/quits and authenticate later with polling --- Application/Core/ClientAuthentication.cs | 74 ++++++ Application/EventParsers/IW4EventParser.cs | 56 ++-- Application/GameEventHandler.cs | 65 ++--- Application/Main.cs | 2 +- Application/Manager.cs | 72 ++--- Application/Server.cs | 250 ++++++------------ .../Commands/CommandProcessing.cs | 145 ++++++++++ .../Interfaces/IClientAuthentication.cs | 27 ++ SharedLibraryCore/Interfaces/IEventHandler.cs | 3 +- SharedLibraryCore/Objects/ClientStats.cs | 10 + SharedLibraryCore/Objects/Player.cs | 11 + SharedLibraryCore/RCon/StaticHelpers.cs | 2 +- SharedLibraryCore/Server.cs | 8 - 13 files changed, 437 insertions(+), 288 deletions(-) create mode 100644 Application/Core/ClientAuthentication.cs create mode 100644 SharedLibraryCore/Commands/CommandProcessing.cs create mode 100644 SharedLibraryCore/Interfaces/IClientAuthentication.cs create mode 100644 SharedLibraryCore/Objects/ClientStats.cs diff --git a/Application/Core/ClientAuthentication.cs b/Application/Core/ClientAuthentication.cs new file mode 100644 index 00000000..f0a214af --- /dev/null +++ b/Application/Core/ClientAuthentication.cs @@ -0,0 +1,74 @@ +using SharedLibraryCore.Interfaces; +using SharedLibraryCore.Objects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace IW4MAdmin.Application.Core +{ + class ClientAuthentication : IClientAuthentication + { + private Queue ClientAuthenticationQueue; + private Dictionary AuthenticatedClients; + + public ClientAuthentication() + { + ClientAuthenticationQueue = new Queue(); + AuthenticatedClients = new Dictionary(); + } + + public void AuthenticateClients(IList clients) + { + // we need to un-auth all the clients that have disconnected + var clientNetworkIds = clients.Select(c => c.NetworkId); + var clientsToRemove = AuthenticatedClients.Keys.Where(c => !clientNetworkIds.Contains(c)); + // remove them + foreach (long Id in clientsToRemove.ToList()) + { + AuthenticatedClients.Remove(Id); + } + + // loop through the polled clients to see if they've been authenticated yet + foreach (var client in clients) + { + // they've not been authenticated + if (!AuthenticatedClients.TryGetValue(client.NetworkId, out Player value)) + { + // authenticate them + client.IsAuthenticated = true; + AuthenticatedClients.Add(client.NetworkId, client); + } + else + { + // this update their ping + value.Ping = client.Ping; + } + } + + // empty out the queue of clients detected through log + while (ClientAuthenticationQueue.Count > 0) + { + // grab each client that's connected via log + var clientToAuthenticate = ClientAuthenticationQueue.Dequeue(); + // if they're not already authed, auth them + if (!AuthenticatedClients.TryGetValue(clientToAuthenticate.NetworkId, out Player value)) + { + // authenticate them + clientToAuthenticate.IsAuthenticated = true; + AuthenticatedClients.Add(clientToAuthenticate.NetworkId, clientToAuthenticate); + } + } + } + + public IList GetAuthenticatedClients() + { + return AuthenticatedClients.Values.ToList(); + } + + public void RequestClientAuthentication(Player client) + { + ClientAuthenticationQueue.Enqueue(client); + } + } +} diff --git a/Application/EventParsers/IW4EventParser.cs b/Application/EventParsers/IW4EventParser.cs index 4219a19d..39cc81f3 100644 --- a/Application/EventParsers/IW4EventParser.cs +++ b/Application/EventParsers/IW4EventParser.cs @@ -12,11 +12,12 @@ namespace IW4MAdmin.Application.EventParsers { public virtual GameEvent GetEvent(Server server, string logLine) { + logLine = Regex.Replace(logLine, @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim(); string[] lineSplit = logLine.Split(';'); - string cleanedEventLine = Regex.Replace(lineSplit[0], @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim(); + string eventType = lineSplit[0]; // kill - if (cleanedEventLine[0] == 'K') + if (eventType == "K") { if (!server.CustomCallback) { @@ -31,18 +32,18 @@ namespace IW4MAdmin.Application.EventParsers } } - if (cleanedEventLine.Contains("JoinTeam")) + if (eventType == "JoinTeam") { return new GameEvent() { Type = GameEvent.EventType.JoinTeam, - Data = cleanedEventLine, + Data = eventType, Origin = server.GetPlayersAsList().FirstOrDefault(c => c.NetworkId == lineSplit[1].ConvertLong()), Owner = server }; } - if (cleanedEventLine == "say" || cleanedEventLine == "sayteam") + if (eventType == "say" || eventType == "sayteam") { string message = lineSplit[4].Replace("\x15", ""); @@ -68,7 +69,7 @@ namespace IW4MAdmin.Application.EventParsers }; } - if (cleanedEventLine.Contains("ScriptKill")) + if (eventType == "ScriptKill") { return new GameEvent() { @@ -80,7 +81,7 @@ namespace IW4MAdmin.Application.EventParsers }; } - if (cleanedEventLine.Contains("ScriptDamage")) + if (eventType == "ScriptDamage") { return new GameEvent() { @@ -93,14 +94,14 @@ namespace IW4MAdmin.Application.EventParsers } // damage - if (cleanedEventLine[0] == 'D') + if (eventType == "D") { - if (Regex.Match(cleanedEventLine, @"^(D);((?:bot[0-9]+)|(?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$").Success) + if (Regex.Match(eventType, @"^(D);((?:bot[0-9]+)|(?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$").Success) { return new GameEvent() { Type = GameEvent.EventType.Damage, - Data = cleanedEventLine, + Data = eventType, Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[5].ConvertLong()), Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()), Owner = server @@ -109,19 +110,19 @@ namespace IW4MAdmin.Application.EventParsers } // join - if (cleanedEventLine[0] == 'J') + if (eventType == "J") { - var regexMatch = Regex.Match(cleanedEventLine, @"^(J;)(.{4,32});([0-9]+);(.*)$"); + var regexMatch = Regex.Match(logLine, @"^(J;)(.{4,32});([0-9]+);(.*)$"); if (regexMatch.Success) { return new GameEvent() { Type = GameEvent.EventType.Join, - Data = cleanedEventLine, + Data = logLine, Owner = server, Origin = new Player() { - Name = regexMatch.Groups[4].ToString(), + Name = regexMatch.Groups[4].ToString().StripColors(), NetworkId = regexMatch.Groups[2].ToString().ConvertLong(), ClientNumber = Convert.ToInt32(regexMatch.Groups[3].ToString()) } @@ -129,7 +130,28 @@ namespace IW4MAdmin.Application.EventParsers } } - if (cleanedEventLine.Contains("ExitLevel")) + if (eventType == "Q") + { + var regexMatch = Regex.Match(logLine, @"^(Q;)(.{4,32});([0-9]+);(.*)$"); + if (regexMatch.Success) + { + return new GameEvent() + { + Type = GameEvent.EventType.Quit, + Data = logLine, + Owner = server, + Origin = new Player() + { + Name = regexMatch.Groups[4].ToString().StripColors(), + NetworkId = regexMatch.Groups[2].ToString().ConvertLong(), + ClientNumber = Convert.ToInt32(regexMatch.Groups[3].ToString()), + State = Player.ClientState.Connecting + } + }; + } + } + + if (eventType.Contains("ExitLevel")) { return new GameEvent() { @@ -147,9 +169,9 @@ namespace IW4MAdmin.Application.EventParsers }; } - if (cleanedEventLine.Contains("InitGame")) + if (eventType.Contains("InitGame")) { - string dump = cleanedEventLine.Replace("InitGame: ", ""); + string dump = eventType.Replace("InitGame: ", ""); return new GameEvent() { diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs index b6120942..f13af1f5 100644 --- a/Application/GameEventHandler.cs +++ b/Application/GameEventHandler.cs @@ -12,40 +12,28 @@ namespace IW4MAdmin.Application class GameEventHandler : IEventHandler { private ConcurrentQueue EventQueue; - private Queue StatusSensitiveQueue; + private Queue DelayedEventQueue; private IManager Manager; + private const int DelayAmount = 5000; + private DateTime LastDelayedEvent; public GameEventHandler(IManager mgr) { EventQueue = new ConcurrentQueue(); - StatusSensitiveQueue = new Queue(); + DelayedEventQueue = new Queue(); Manager = mgr; } - public void AddEvent(GameEvent gameEvent) + public void AddEvent(GameEvent gameEvent, bool delayedExecution = false) { #if DEBUG Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner}"); #endif - // we need this to keep accurate track of the score - if (gameEvent.Type == GameEvent.EventType.Kill || - gameEvent.Type == GameEvent.EventType.Damage || - gameEvent.Type == GameEvent.EventType.ScriptDamage || - gameEvent.Type == GameEvent.EventType.ScriptKill || - gameEvent.Type == GameEvent.EventType.MapChange || - gameEvent.Type == GameEvent.EventType.JoinTeam) + if (delayedExecution) { -#if DEBUG - Manager.GetLogger().WriteDebug($"Added sensitive event to queue"); -#endif - lock (StatusSensitiveQueue) - { - StatusSensitiveQueue.Enqueue(gameEvent); - } - return; + DelayedEventQueue.Enqueue(gameEvent); } - else { EventQueue.Enqueue(gameEvent); @@ -61,27 +49,6 @@ namespace IW4MAdmin.Application throw new NotImplementedException(); } - public GameEvent GetNextSensitiveEvent() - { - if (StatusSensitiveQueue.Count > 0) - { - lock (StatusSensitiveQueue) - { - if (!StatusSensitiveQueue.TryDequeue(out GameEvent newEvent)) - { - Manager.GetLogger().WriteWarning("Could not dequeue time sensitive event for processing"); - } - - else - { - return newEvent; - } - } - } - - return null; - } - public GameEvent GetNextEvent() { if (EventQueue.Count > 0) @@ -100,6 +67,24 @@ namespace IW4MAdmin.Application } } + if (DelayedEventQueue.Count > 0 && + (DateTime.Now - LastDelayedEvent).TotalMilliseconds > DelayAmount) + { + LastDelayedEvent = DateTime.Now; +#if DEBUG + Manager.GetLogger().WriteDebug("Getting next delayed event to be processed"); +#endif + if (!DelayedEventQueue.TryDequeue(out GameEvent newEvent)) + { + Manager.GetLogger().WriteWarning("Could not dequeue delayed event for processing"); + } + + else + { + return newEvent; + } + } + return null; } } diff --git a/Application/Main.cs b/Application/Main.cs index ea8ff5cc..d8a4787c 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -36,7 +36,7 @@ namespace IW4MAdmin.Application Console.WriteLine("====================================================="); Console.WriteLine(" IW4M ADMIN"); Console.WriteLine(" by RaidMax "); - Console.WriteLine($" Version {Version.ToString("0.0")}"); + Console.WriteLine($" Version {Version.ToString("0.00")}"); Console.WriteLine("====================================================="); Index loc = null; diff --git a/Application/Manager.cs b/Application/Manager.cs index 8e781516..cb46c8cd 100644 --- a/Application/Manager.cs +++ b/Application/Manager.cs @@ -81,18 +81,14 @@ namespace IW4MAdmin.Application public async Task UpdateStatus(object state) { - var taskList = new Dictionary(); + var taskList = new List(); while (Running) { - var tasksToRemove = taskList.Where(t => t.Value.Status == TaskStatus.RanToCompletion) - .Select(t => t.Key).ToList(); - - tasksToRemove.ForEach(t => taskList.Remove(t)); - + taskList.Clear(); foreach (var server in Servers) { - var newTask = Task.Run(async () => + taskList.Add(Task.Run(async () => { try { @@ -105,12 +101,7 @@ namespace IW4MAdmin.Application Logger.WriteDebug($"Exception: {e.Message}"); Logger.WriteDebug($"StackTrace: {e.StackTrace}"); } - }); - - if (!taskList.ContainsKey(server.GetHashCode())) - { - taskList.Add(server.GetHashCode(), newTask); - } + })); } #if DEBUG Logger.WriteDebug($"{taskList.Count} servers queued for stats updates"); @@ -119,35 +110,7 @@ namespace IW4MAdmin.Application Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks"); #endif - await Task.WhenAny(taskList.Values.ToArray()); - - GameEvent sensitiveEvent; - while ((sensitiveEvent = Handler.GetNextSensitiveEvent()) != null) - { - try - { - await sensitiveEvent.Owner.ExecuteEvent(sensitiveEvent); -#if DEBUG - Logger.WriteDebug($"Processed Sensitive Event {sensitiveEvent.Type}"); -#endif - } - - catch (NetworkException e) - { - Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]); - Logger.WriteDebug(e.Message); - } - - catch (Exception E) - { - Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {sensitiveEvent.Owner}"); - Logger.WriteDebug("Error Message: " + E.Message); - Logger.WriteDebug("Error Trace: " + E.StackTrace); - } - - sensitiveEvent.OnProcessed.Set(); - } - + await Task.WhenAny(taskList); await Task.Delay(ConfigHandler.Configuration().RConPollRate); } } @@ -460,15 +423,34 @@ namespace IW4MAdmin.Application { // wait for new event to be added OnEvent.Wait(); - + var taskList = new List(); // todo: sequencially or parallelize? while ((queuedEvent = Handler.GetNextEvent()) != null) { + if (queuedEvent.Origin != null && + !queuedEvent.Origin.IsAuthenticated && + // we want to allow join events + queuedEvent.Type != GameEvent.EventType.Join && + queuedEvent.Type != GameEvent.EventType.Quit && + // we don't care about unknown events + queuedEvent.Origin.NetworkId != 0) + { + Logger.WriteDebug($"Delaying execution of event type {queuedEvent.Type} for {queuedEvent.Origin} because they are not authed"); + // update the event origin for possible authed client + queuedEvent.Origin = queuedEvent.Owner.Players.FirstOrDefault(p => p != null && p.NetworkId == queuedEvent.Origin.NetworkId); + queuedEvent.Target = queuedEvent.Target == null ? null : queuedEvent.Owner.Players.FirstOrDefault(p => p != null && p.NetworkId == queuedEvent.Target.NetworkId); + // add it back to the queue for reprocessing + Handler.AddEvent(queuedEvent, true); + continue; + } await processEvent(queuedEvent); } - // this should allow parallel processing of events - // await Task.WhenAll(eventList); + if (taskList.Count > 0) + { + // this should allow parallel processing of events + await Task.WhenAny(taskList); + } // signal that all events have been processed OnEvent.Reset(); diff --git a/Application/Server.cs b/Application/Server.cs index 3462e539..d0a64d4c 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -20,6 +20,7 @@ using Application.RconParsers; using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.IO; using SharedLibraryCore.Localization; +using IW4MAdmin.Application.Core; namespace IW4MAdmin { @@ -27,8 +28,12 @@ namespace IW4MAdmin { private static Index loc = Utilities.CurrentLocalization.LocalizationIndex; private GameLogEvent LogEvent; + private ClientAuthentication AuthQueue; - public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg) { } + public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg) + { + AuthQueue = new ClientAuthentication(); + } public override int GetHashCode() { @@ -49,6 +54,17 @@ namespace IW4MAdmin return id; } + public async Task OnPlayerJoined(Player logClient) + { + Logger.WriteDebug($"Log detected {logClient} joining"); + if (Players[logClient.ClientNumber] == null || Players[logClient.ClientNumber].NetworkId != logClient.NetworkId) + { + Players[logClient.ClientNumber] = logClient; + } + + await Task.CompletedTask; + } + override public async Task AddPlayer(Player polledPlayer) { if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot) || @@ -60,7 +76,9 @@ namespace IW4MAdmin } if (Players[polledPlayer.ClientNumber] != null && - Players[polledPlayer.ClientNumber].NetworkId == polledPlayer.NetworkId) + Players[polledPlayer.ClientNumber].NetworkId == polledPlayer.NetworkId && + // only update if they're unauthenticated + Players[polledPlayer.ClientNumber].IsAuthenticated) { // update their ping & score Players[polledPlayer.ClientNumber].Ping = polledPlayer.Ping; @@ -76,7 +94,7 @@ namespace IW4MAdmin return false; } - if (Players.FirstOrDefault(p => p != null && p.Name == polledPlayer.Name) != null) + if (Players.FirstOrDefault(p => p != null && p.Name == polledPlayer.Name && p.NetworkId != polledPlayer.NetworkId) != null) { Logger.WriteDebug($"Kicking {polledPlayer} because their name is already in use"); string formattedKick = String.Format(RconParser.GetCommandPrefixes().Kick, polledPlayer.ClientNumber, loc["SERVER_KICK_NAME_INUSE"]); @@ -154,7 +172,9 @@ namespace IW4MAdmin player.ClientNumber = polledPlayer.ClientNumber; player.IsBot = polledPlayer.IsBot; player.Score = polledPlayer.Score; + player.IsAuthenticated = true; player.CurrentServer = this; + player.State = Player.ClientState.Connected; Players[player.ClientNumber] = player; var activePenalties = await Manager.GetPenaltyService().GetActivePenaltiesAsync(player.AliasLinkId, player.IPAddress); @@ -230,149 +250,19 @@ namespace IW4MAdmin Player Leaving = Players[cNum]; Logger.WriteInfo($"Client {Leaving} disconnecting..."); - Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds; - Leaving.LastConnection = DateTime.UtcNow; - await Manager.GetClientService().Update(Leaving); - Players[cNum] = null; - - var e = new GameEvent(GameEvent.EventType.Disconnect, "", Leaving, null, this); - Manager.GetEventHandler().AddEvent(e); - - // wait until the disconnect event is complete - e.OnProcessed.Wait(); - } - } - - //Process requested command correlating to an event - // todo: this needs to be removed out of here - override public async Task ValidateCommand(GameEvent E) - { - string CommandString = E.Data.Substring(1, E.Data.Length - 1).Split(' ')[0]; - E.Message = E.Data; - - Command C = null; - foreach (Command cmd in Manager.GetCommands()) - { - if (cmd.Name == CommandString.ToLower() || cmd.Alias == CommandString.ToLower()) - C = cmd; - } - - if (C == null) - { - await E.Origin.Tell(loc["COMMAND_UNKNOWN"]); - throw new CommandException($"{E.Origin} entered unknown command \"{CommandString}\""); - } - - E.Data = E.Data.RemoveWords(1); - String[] Args = E.Data.Trim().Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - - if (E.Origin.Level < C.Permission) - { - await E.Origin.Tell(loc["COMMAND_NOACCESS"]); - throw new CommandException($"{E.Origin} does not have access to \"{C.Name}\""); - } - - if (Args.Length < (C.RequiredArgumentCount)) - { - await E.Origin.Tell(loc["COMMAND_MISSINGARGS"]); - await E.Origin.Tell(C.Syntax); - throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\""); - } - - if (C.RequiresTarget || Args.Length > 0) - { - if (!Int32.TryParse(Args[0], out int cNum)) - cNum = -1; - - if (Args[0][0] == '@') // user specifying target by database ID + if (!Leaving.IsAuthenticated) { - int dbID = -1; - int.TryParse(Args[0].Substring(1, Args[0].Length - 1), out dbID); - - var found = await Manager.GetClientService().Get(dbID); - if (found != null) - { - E.Target = found.AsPlayer(); - E.Target.CurrentServer = this as IW4MServer; - E.Owner = this as IW4MServer; - E.Data = String.Join(" ", Args.Skip(1)); - } + Players[cNum] = null; } - else if (Args[0].Length < 3 && cNum > -1 && cNum < MaxClients) // user specifying target by client num + else { - if (Players[cNum] != null) - { - E.Target = Players[cNum]; - E.Data = String.Join(" ", Args.Skip(1)); - } - } - - List matchingPlayers; - - if (E.Target == null && C.RequiresTarget) // Find active player including quotes (multiple words) - { - matchingPlayers = GetClientByName(E.Data.Trim()); - if (matchingPlayers.Count > 1) - { - await E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]); - throw new CommandException($"{E.Origin} had multiple players found for {C.Name}"); - } - else if (matchingPlayers.Count == 1) - { - E.Target = matchingPlayers.First(); - - string escapedName = Regex.Escape(E.Target.Name); - var reg = new Regex($"(\"{escapedName}\")|({escapedName})", RegexOptions.IgnoreCase); - E.Data = reg.Replace(E.Data, "", 1).Trim(); - - if (E.Data.Length == 0 && C.RequiredArgumentCount > 1) - { - await E.Origin.Tell(loc["COMMAND_MISSINGARGS"]); - await E.Origin.Tell(C.Syntax); - throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\""); - } - } - } - - if (E.Target == null && C.RequiresTarget) // Find active player as single word - { - matchingPlayers = GetClientByName(Args[0]); - if (matchingPlayers.Count > 1) - { - await E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]); - foreach (var p in matchingPlayers) - await E.Origin.Tell($"[^3{p.ClientNumber}^7] {p.Name}"); - throw new CommandException($"{E.Origin} had multiple players found for {C.Name}"); - } - else if (matchingPlayers.Count == 1) - { - E.Target = matchingPlayers.First(); - - string escapedName = Regex.Escape(E.Target.Name); - string escapedArg = Regex.Escape(Args[0]); - var reg = new Regex($"({escapedName})|({escapedArg})", RegexOptions.IgnoreCase); - E.Data = reg.Replace(E.Data, "", 1).Trim(); - - if ((E.Data.Trim() == E.Target.Name.ToLower().Trim() || - E.Data == String.Empty) && - C.RequiresTarget) - { - await E.Origin.Tell(loc["COMMAND_MISSINGARGS"]); - await E.Origin.Tell(C.Syntax); - throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\""); - } - } - } - - if (E.Target == null && C.RequiresTarget) - { - await E.Origin.Tell(loc["COMMAND_TARGET_NOTFOUND"]); - throw new CommandException($"{E.Origin} specified invalid player for \"{C.Name}\""); + Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds; + Leaving.LastConnection = DateTime.UtcNow; + await Manager.GetClientService().Update(Leaving); + Players.RemoveAt(cNum); } } - E.Data = E.Data.Trim(); - return C; } public override async Task ExecuteEvent(GameEvent E) @@ -381,13 +271,12 @@ namespace IW4MAdmin await ProcessEvent(E); Manager.GetEventApi().OnServerEvent(this, E); - Command C = null; if (E.Type == GameEvent.EventType.Command) { try { - C = await ValidateCommand(E); + C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E); } catch (CommandException e) @@ -471,22 +360,21 @@ namespace IW4MAdmin else if (E.Type == GameEvent.EventType.Join) { - // special case for IW5 when connect is from the log - if (E.Extra != null && GameName == Game.IW5) - { - var logClient = (Player)E.Extra; - var client = (await this.GetStatusAsync()) - .Single(c => c.ClientNumber == logClient.ClientNumber && - c.Name == logClient.Name); - client.NetworkId = logClient.NetworkId; + await OnPlayerJoined(E.Origin); + } - await AddPlayer(client); - } - - /*else + else if (E.Type == GameEvent.EventType.Quit) + { + var e = new GameEvent() { - await AddPlayer(E.Origin); - }*/ + Type = GameEvent.EventType.Disconnect, + Origin = Players.FirstOrDefault(p => p != null && p.NetworkId == E.Origin.NetworkId), + Owner = this + }; + + e.Origin.State = Player.ClientState.Disconnecting; + + Manager.GetEventHandler().AddEvent(e); } else if (E.Type == GameEvent.EventType.Disconnect) @@ -501,6 +389,8 @@ namespace IW4MAdmin Time = DateTime.UtcNow }); } + + await RemovePlayer(E.Origin.ClientNumber); } if (E.Type == GameEvent.EventType.Say) @@ -574,7 +464,10 @@ namespace IW4MAdmin if (E.Type == GameEvent.EventType.Broadcast) { // this is a little ugly but I don't want to change the abstract class - await E.Owner.ExecuteCommandAsync(E.Message); + if (E.Message != null) + { + await E.Owner.ExecuteCommandAsync(E.Message); + } } while (ChatHistory.Count > Math.Ceiling((double)ClientNum / 2)) @@ -611,24 +504,33 @@ namespace IW4MAdmin var clients = GetPlayersAsList(); foreach (var client in clients) { - if (GameName == Game.IW5) + // remove players that have disconnected + if (!CurrentPlayers.Select(c => c.NetworkId).Contains(client.NetworkId)) { - if (!CurrentPlayers.Select(c => c.ClientNumber).Contains(client.ClientNumber)) - await RemovePlayer(client.ClientNumber); - } + // the log should already have started a disconnect event + if (client.State == Player.ClientState.Disconnecting) + continue; - else - { - if (!CurrentPlayers.Select(c => c.NetworkId).Contains(client.NetworkId)) - await RemovePlayer(client.ClientNumber); + var e = new GameEvent() + { + Type = GameEvent.EventType.Disconnect, + Origin = client, + Owner = this + }; + + Manager.GetEventHandler().AddEvent(e); + // todo: needed? + // wait until the disconnect event is complete + e.OnProcessed.Wait(); } } - for (int i = 0; i < CurrentPlayers.Count; i++) + AuthQueue.AuthenticateClients(CurrentPlayers); + + // all polled players should be authenticated + foreach (var client in AuthQueue.GetAuthenticatedClients()) { - // todo: wait til GUID is included in status to fix this - if (GameName != Game.IW5) - await AddPlayer(CurrentPlayers[i]); + await AddPlayer(client); } return CurrentPlayers.Count; @@ -645,8 +547,9 @@ namespace IW4MAdmin { if (Manager.ShutdownRequested()) { - for (int i = 0; i < Players.Count; i++) - await RemovePlayer(i); + // todo: fix up disconnect + //for (int i = 0; i < Players.Count; i++) + // await RemovePlayer(i); foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins) await plugin.OnUnloadAsync(); @@ -816,9 +719,6 @@ namespace IW4MAdmin this.MaxClients = maxplayers; this.FSGame = game; this.Gametype = gametype; - - //wait this.SetDvarAsync("sv_kickbantime", 60); - if (logsync.Value == 0 || logfile.Value == string.Empty) { // this DVAR isn't set until the a map is loaded @@ -833,7 +733,7 @@ namespace IW4MAdmin CustomCallback = await ScriptLoaded(); string mainPath = EventParser.GetGameDir(); #if DEBUG - basepath.Value = @"\\192.168.88.253\logs\games_mp.log"; + basepath.Value = @"D:\"; #endif string logPath; if (GameName == Game.IW5) diff --git a/SharedLibraryCore/Commands/CommandProcessing.cs b/SharedLibraryCore/Commands/CommandProcessing.cs new file mode 100644 index 00000000..a18188a0 --- /dev/null +++ b/SharedLibraryCore/Commands/CommandProcessing.cs @@ -0,0 +1,145 @@ +using SharedLibraryCore.Exceptions; +using SharedLibraryCore.Objects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace SharedLibraryCore.Commands +{ + public class CommandProcessing + { + public static async Task ValidateCommand(GameEvent E) + { + var loc = Utilities.CurrentLocalization.LocalizationIndex; + var Manager = E.Owner.Manager; + + string CommandString = E.Data.Substring(1, E.Data.Length - 1).Split(' ')[0]; + E.Message = E.Data; + + Command C = null; + foreach (Command cmd in Manager.GetCommands()) + { + if (cmd.Name == CommandString.ToLower() || cmd.Alias == CommandString.ToLower()) + C = cmd; + } + + if (C == null) + { + await E.Origin.Tell(loc["COMMAND_UNKNOWN"]); + throw new CommandException($"{E.Origin} entered unknown command \"{CommandString}\""); + } + + E.Data = E.Data.RemoveWords(1); + String[] Args = E.Data.Trim().Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (E.Origin.Level < C.Permission) + { + await E.Origin.Tell(loc["COMMAND_NOACCESS"]); + throw new CommandException($"{E.Origin} does not have access to \"{C.Name}\""); + } + + if (Args.Length < (C.RequiredArgumentCount)) + { + await E.Origin.Tell(loc["COMMAND_MISSINGARGS"]); + await E.Origin.Tell(C.Syntax); + throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\""); + } + + if (C.RequiresTarget || Args.Length > 0) + { + if (!Int32.TryParse(Args[0], out int cNum)) + cNum = -1; + + if (Args[0][0] == '@') // user specifying target by database ID + { + int dbID = -1; + int.TryParse(Args[0].Substring(1, Args[0].Length - 1), out dbID); + + var found = await Manager.GetClientService().Get(dbID); + if (found != null) + { + E.Target = found.AsPlayer(); + E.Target.CurrentServer = E.Owner; + E.Data = String.Join(" ", Args.Skip(1)); + } + } + + else if (Args[0].Length < 3 && cNum > -1 && cNum < E.Owner.MaxClients) // user specifying target by client num + { + if (E.Owner.Players[cNum] != null) + { + E.Target = E.Owner.Players[cNum]; + E.Data = String.Join(" ", Args.Skip(1)); + } + } + + List matchingPlayers; + + if (E.Target == null && C.RequiresTarget) // Find active player including quotes (multiple words) + { + matchingPlayers = E.Owner.GetClientByName(E.Data.Trim()); + if (matchingPlayers.Count > 1) + { + await E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]); + throw new CommandException($"{E.Origin} had multiple players found for {C.Name}"); + } + else if (matchingPlayers.Count == 1) + { + E.Target = matchingPlayers.First(); + + string escapedName = Regex.Escape(E.Target.Name); + var reg = new Regex($"(\"{escapedName}\")|({escapedName})", RegexOptions.IgnoreCase); + E.Data = reg.Replace(E.Data, "", 1).Trim(); + + if (E.Data.Length == 0 && C.RequiredArgumentCount > 1) + { + await E.Origin.Tell(loc["COMMAND_MISSINGARGS"]); + await E.Origin.Tell(C.Syntax); + throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\""); + } + } + } + + if (E.Target == null && C.RequiresTarget) // Find active player as single word + { + matchingPlayers = E.Owner.GetClientByName(Args[0]); + if (matchingPlayers.Count > 1) + { + await E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]); + foreach (var p in matchingPlayers) + await E.Origin.Tell($"[^3{p.ClientNumber}^7] {p.Name}"); + throw new CommandException($"{E.Origin} had multiple players found for {C.Name}"); + } + else if (matchingPlayers.Count == 1) + { + E.Target = matchingPlayers.First(); + + string escapedName = Regex.Escape(E.Target.Name); + string escapedArg = Regex.Escape(Args[0]); + var reg = new Regex($"({escapedName})|({escapedArg})", RegexOptions.IgnoreCase); + E.Data = reg.Replace(E.Data, "", 1).Trim(); + + if ((E.Data.Trim() == E.Target.Name.ToLower().Trim() || + E.Data == String.Empty) && + C.RequiresTarget) + { + await E.Origin.Tell(loc["COMMAND_MISSINGARGS"]); + await E.Origin.Tell(C.Syntax); + throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\""); + } + } + } + + if (E.Target == null && C.RequiresTarget) + { + await E.Origin.Tell(loc["COMMAND_TARGET_NOTFOUND"]); + throw new CommandException($"{E.Origin} specified invalid player for \"{C.Name}\""); + } + } + E.Data = E.Data.Trim(); + return C; + } + } +} diff --git a/SharedLibraryCore/Interfaces/IClientAuthentication.cs b/SharedLibraryCore/Interfaces/IClientAuthentication.cs new file mode 100644 index 00000000..7354c34a --- /dev/null +++ b/SharedLibraryCore/Interfaces/IClientAuthentication.cs @@ -0,0 +1,27 @@ +using SharedLibraryCore.Objects; +using System; +using System.Collections.Generic; +using System.Text; + +namespace SharedLibraryCore.Interfaces +{ + public interface IClientAuthentication + { + /// + /// request authentication when a client join event + /// occurs in the log, as no IP is given + /// + /// client that has joined from the log + void RequestClientAuthentication(Player client); + /// + /// get all clients that have been authenticated by the status poll + /// + /// list of all authenticated clients + IList GetAuthenticatedClients(); + /// + /// authenticate a list of clients from status poll + /// + /// list of clients to authenticate + void AuthenticateClients(IList clients); + } +} diff --git a/SharedLibraryCore/Interfaces/IEventHandler.cs b/SharedLibraryCore/Interfaces/IEventHandler.cs index 7fe2efc5..5bf54fc7 100644 --- a/SharedLibraryCore/Interfaces/IEventHandler.cs +++ b/SharedLibraryCore/Interfaces/IEventHandler.cs @@ -13,7 +13,8 @@ namespace SharedLibraryCore.Interfaces /// Add a game event event to the queue to be processed /// /// Game event - void AddEvent(GameEvent gameEvent); + /// don't signal that an event has been aded + void AddEvent(GameEvent gameEvent, bool delayedExecution = false); /// /// Get the next event to be processed /// diff --git a/SharedLibraryCore/Objects/ClientStats.cs b/SharedLibraryCore/Objects/ClientStats.cs new file mode 100644 index 00000000..e90b3800 --- /dev/null +++ b/SharedLibraryCore/Objects/ClientStats.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SharedLibraryCore.Objects +{ + class ClientStats + { + } +} diff --git a/SharedLibraryCore/Objects/Player.cs b/SharedLibraryCore/Objects/Player.cs index 188a1dba..9261e6e4 100644 --- a/SharedLibraryCore/Objects/Player.cs +++ b/SharedLibraryCore/Objects/Player.cs @@ -8,6 +8,13 @@ namespace SharedLibraryCore.Objects { public class Player : Database.Models.EFClient { + public enum ClientState + { + Connecting, + Connected, + Disconnecting, + } + public enum Permission { Banned = -1, @@ -109,6 +116,10 @@ namespace SharedLibraryCore.Objects get { return _name; } set { _name = value; } } + [NotMapped] + public bool IsAuthenticated { get; set; } + [NotMapped] + public ClientState State { get; set; } public override bool Equals(object obj) { diff --git a/SharedLibraryCore/RCon/StaticHelpers.cs b/SharedLibraryCore/RCon/StaticHelpers.cs index 0782d039..52dec73d 100644 --- a/SharedLibraryCore/RCon/StaticHelpers.cs +++ b/SharedLibraryCore/RCon/StaticHelpers.cs @@ -13,6 +13,6 @@ namespace SharedLibraryCore.RCon } public static char SeperatorChar = (char)int.Parse("0a", System.Globalization.NumberStyles.AllowHexSpecifier); - public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 10); + public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 5); } } diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 5d4b5fe5..069ca930 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -100,14 +100,6 @@ namespace SharedLibraryCore return Players.Where(p => p != null && p.Name.ToLower().Contains(pName.ToLower())).ToList(); } - /// - /// Process requested command correlating to an event - /// - /// Event parameter - /// Command requested from the event - /// - abstract public Task ValidateCommand(GameEvent E); - virtual public Task ProcessUpdatesAsync(CancellationToken cts) => (Task)Task.CompletedTask; ///