diff --git a/Application/Manager.cs b/Application/Manager.cs index 509b0403f..04c914c79 100644 --- a/Application/Manager.cs +++ b/Application/Manager.cs @@ -268,7 +268,7 @@ namespace IW4MAdmin.Application Running = true; #region DATABASE - using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString, GetApplicationSettings().Configuration().DatabaseProvider)) + using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString, GetApplicationSettings().Configuration()?.DatabaseProvider)) { await new ContextSeed(db).Seed(); } diff --git a/Application/RconParsers/T6MRConParser.cs b/Application/RconParsers/T6MRConParser.cs index 485d1a64d..86efa74de 100644 --- a/Application/RconParsers/T6MRConParser.cs +++ b/Application/RconParsers/T6MRConParser.cs @@ -16,42 +16,6 @@ namespace IW4MAdmin.Application.RconParsers { public class T6MRConParser : IRConParser { - class T6MResponse - { - public class SInfo - { - public short Com_maxclients { get; set; } - public string Game { get; set; } - public string Gametype { get; set; } - public string Mapname { get; set; } - public short NumBots { get; set; } - public short NumClients { get; set; } - public short Round { get; set; } - public string Sv_hostname { get; set; } - } - - public class PInfo - { - public short Assists { get; set; } - public string Clan { get; set; } - public short Deaths { get; set; } - public short Downs { get; set; } - public short Headshots { get; set; } - public short Id { get; set; } - public bool IsBot { get; set; } - public short Kills { get; set; } - public string Name { get; set; } - public short Ping { get; set; } - public short Revives { get; set; } - public int Score { get; set; } - public long Xuid { get; set; } - public string Ip { get; set; } - } - - public SInfo Info { get; set; } - public PInfo[] Players { get; set; } - } - private static readonly CommandPrefix Prefixes = new CommandPrefix() { Tell = "tell {0} {1}", @@ -73,7 +37,6 @@ namespace IW4MAdmin.Application.RconParsers { string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"get {dvarName}"); - if (LineSplit.Length < 2) { var e = new DvarException($"DVAR \"{dvarName}\" does not exist"); @@ -103,8 +66,6 @@ namespace IW4MAdmin.Application.RconParsers { string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status"); return ClientsFromStatus(response); - - //return ClientsFromResponse(connection); } public async Task SetDvarAsync(Connection connection, string dvarName, object dvarValue) @@ -114,41 +75,6 @@ namespace IW4MAdmin.Application.RconParsers return true; } - private async Task> ClientsFromResponse(Connection conn) - { - using (var client = new HttpClient()) - { - client.BaseAddress = new Uri($"http://{conn.Endpoint.Address}:{conn.Endpoint.Port}/"); - - try - { - var parameters = new FormUrlEncodedContent(new[] - { - new KeyValuePair("rcon_password", conn.RConPassword) - }); - - var serverResponse = await client.PostAsync("/info", parameters); - var serverResponseObject = Newtonsoft.Json.JsonConvert.DeserializeObject(await serverResponse.Content.ReadAsStringAsync()); - - return serverResponseObject.Players.Select(p => new Player() - { - Name = p.Name, - NetworkId = p.Xuid, - ClientNumber = p.Id, - IPAddress = p.Ip.Split(':')[0].ConvertToIP(), - Ping = p.Ping, - Score = p.Score, - IsBot = p.IsBot, - }).ToList(); - } - - catch (HttpRequestException e) - { - throw new NetworkException(e.Message); - } - } - } - private List ClientsFromStatus(string[] status) { List StatusPlayers = new List(); @@ -174,9 +100,6 @@ namespace IW4MAdmin.Application.RconParsers #endif int ipAddress = regex.Value.Split(':')[0].ConvertToIP(); regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+"); - int score = 0; - // todo: fix this when T6M score is valid ;) - //int score = Int32.Parse(playerInfo[1]); var p = new Player() { Name = name, @@ -184,7 +107,7 @@ namespace IW4MAdmin.Application.RconParsers ClientNumber = clientId, IPAddress = ipAddress, Ping = Ping, - Score = score, + Score = 0, State = Player.ClientState.Connecting, IsBot = networkId == 0 }; diff --git a/Application/Server.cs b/Application/Server.cs index 8ebb86ba3..a1025ffa4 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -855,7 +855,6 @@ namespace IW4MAdmin return; } - Target.Warnings++; String message = $"^1{loc["SERVER_WARNING"]} ^7[^3{Target.Warnings}^7]: ^3{Target.Name}^7, {Reason}"; Target.CurrentServer.Broadcast(message); } diff --git a/Plugins/Tests/ClientTests.cs b/Plugins/Tests/ClientTests.cs index 83960cf58..17c0ed70e 100644 --- a/Plugins/Tests/ClientTests.cs +++ b/Plugins/Tests/ClientTests.cs @@ -1,13 +1,26 @@ -using SharedLibraryCore.Objects; +using IW4MAdmin.Application; +using SharedLibraryCore; +using SharedLibraryCore.Objects; using System; using System.Collections.Generic; +using System.Linq; using System.Text; +using System.Threading; using Xunit; namespace Tests { + [Collection("ManagerCollection")] public class ClientTests { + readonly ApplicationManager Manager; + const int TestTimeout = 5000; + + public ClientTests(ManagerFixture fixture) + { + Manager = fixture.Manager; + } + [Fact] public void SetAdditionalPropertyShouldSucceed() { @@ -25,5 +38,68 @@ namespace Tests Assert.True(client.GetAdditionalProperty("NewProp") == 5, "added property does not match retrieved property"); } + + [Fact] + public void WarnPlayerShouldSucceed() + { + while (!Manager.IsInitialized) + { + Thread.Sleep(100); + } + + var client = Manager.Servers.First().GetPlayersAsList().FirstOrDefault(); + + Assert.False(client == null, "no client found to warn"); + + var warnEvent = client.Warn("test warn", new Player() { ClientId = 1, Level = Player.Permission.Console }); + warnEvent.OnProcessed.Wait(TestTimeout); + + Assert.True(client.Warnings == 1 || + warnEvent.Failed, "warning did not get applied"); + + warnEvent = client.Warn("test warn", new Player() { ClientId = 1, Level = Player.Permission.Banned }); + warnEvent.OnProcessed.Wait(TestTimeout); + + Assert.True(warnEvent.FailReason == GameEvent.EventFailReason.Permission && + client.Warnings == 1, "warning was applied without proper permissions"); + } + + [Fact] + public void ReportPlayerShouldSucceed() + { + while (!Manager.IsInitialized) + { + Thread.Sleep(100); + } + + var client = Manager.Servers.First().GetPlayersAsList().FirstOrDefault(); + + Assert.False(client == null, "no client found to report"); + + // succeed + var reportEvent = client.Report("test report", new Player() { ClientId = 1, Level = Player.Permission.Console }); + reportEvent.OnProcessed.Wait(TestTimeout); + + Assert.True(!reportEvent.Failed && + client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1, $"report was not applied [{reportEvent.FailReason.ToString()}]"); + + // fail + reportEvent = client.Report("test report", new Player() { ClientId = 2, Level = Player.Permission.Banned }); + + Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Permission && + client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1, $"report was applied without proper permission"); + + // fail + reportEvent = client.Report("test report", client); + + Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Invalid && + client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1, $"report was applied to self"); + + // fail + reportEvent = client.Report("test report", new Player() { ClientId = 1, Level = Player.Permission.Console}); + + Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Exception && + client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1, $"duplicate report was applied"); + } } } diff --git a/Plugins/Tests/ManagerFixture.cs b/Plugins/Tests/ManagerFixture.cs index 66e806a68..48d5f50cb 100644 --- a/Plugins/Tests/ManagerFixture.cs +++ b/Plugins/Tests/ManagerFixture.cs @@ -16,7 +16,6 @@ namespace Tests public ManagerFixture() { - File.WriteAllText("test_mp.log", "TEST_LOG_FILE"); Manager = Program.ServerManager; @@ -32,7 +31,7 @@ namespace Tests Password = "test", Port = 28963, Rules = new List(), - ManualLogPath = "https://raidmax.org/IW4MAdmin/getlog.php" + ManualLogPath = "http://google.com" } }, AutoMessages = new List(), diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index 0c7d486de..4dfb6ccb3 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -69,12 +69,14 @@ namespace SharedLibraryCore.Commands }) { } - public override async Task ExecuteAsync(GameEvent E) + public override Task ExecuteAsync(GameEvent E) { - if (!await E.Target.Warn(E.Data, E.Origin).WaitAsync()) + if (E.Target.Warn(E.Data, E.Origin).Failed) { E.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_WARN_FAIL"]} {E.Target.Name}"); } + + return Task.CompletedTask; } } @@ -91,12 +93,14 @@ namespace SharedLibraryCore.Commands }) { } - public override async Task ExecuteAsync(GameEvent E) + public override Task ExecuteAsync(GameEvent E) { - if (await E.Target.WarnClear(E.Origin).WaitAsync()) + if (!E.Target.WarnClear(E.Origin).Failed) { E.Owner.Broadcast($"{Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_WARNCLEAR_SUCCESS"]} {E.Target.Name}"); } + + return Task.CompletedTask; } } @@ -839,8 +843,6 @@ namespace SharedLibraryCore.Commands else { - commandEvent.Owner.Reports.Add(new Report(commandEvent.Target, commandEvent.Origin, commandEvent.Data)); - Penalty newReport = new Penalty() { Type = Penalty.PenaltyType.Report, diff --git a/SharedLibraryCore/Objects/Player.cs b/SharedLibraryCore/Objects/Player.cs index e15400c4a..1b4f79868 100644 --- a/SharedLibraryCore/Objects/Player.cs +++ b/SharedLibraryCore/Objects/Player.cs @@ -128,6 +128,8 @@ namespace SharedLibraryCore.Objects return e; } + this.Warnings++; + CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } @@ -150,7 +152,7 @@ namespace SharedLibraryCore.Objects Owner = this.CurrentServer }; - if (this.Level < sender.Level) + if (this.Level > sender.Level) { e.FailReason = GameEvent.EventFailReason.Permission; return e; @@ -162,13 +164,14 @@ namespace SharedLibraryCore.Objects return e; } - if (CurrentServer.Reports.Count(rep => (rep.Origin == sender && + if (CurrentServer.Reports.Count(rep => (rep.Origin.NetworkId == sender.NetworkId && rep.Target.NetworkId == this.NetworkId)) > 0) { e.FailReason = GameEvent.EventFailReason.Exception; return e; } + CurrentServer.Reports.Add(new Report(this, sender, reportReason)); CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } diff --git a/SharedLibraryCore/RCon/Connection.cs b/SharedLibraryCore/RCon/Connection.cs index 6dc4dd36f..e721430a8 100644 --- a/SharedLibraryCore/RCon/Connection.cs +++ b/SharedLibraryCore/RCon/Connection.cs @@ -17,6 +17,7 @@ namespace SharedLibraryCore.RCon const int BufferSize = 4096; public readonly byte[] ReceiveBuffer = new byte[BufferSize]; public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1); + public DateTime LastQuery { get; set; } = DateTime.Now; } public class Connection @@ -42,9 +43,19 @@ namespace SharedLibraryCore.RCon var connectionState = ActiveQueries[this.Endpoint]; + var timeLeft = (DateTime.Now - connectionState.LastQuery).TotalMilliseconds; + + if (timeLeft > 0) + { + await Task.Delay((int)timeLeft); + } + + connectionState.LastQuery = DateTime.Now; + #if DEBUG == true Log.WriteDebug($"Waiting for semaphore to be released [${this.Endpoint}]"); #endif + // enter the semaphore so only one query is sent at a time per server. await connectionState.OnComplete.WaitAsync(); #if DEBUG == true @@ -73,40 +84,41 @@ namespace SharedLibraryCore.RCon byte[] response = null; retrySend: #if DEBUG == true - Log.WriteDebug($"Sending {payload.Length} bytes to [${this.Endpoint}] ({connectionState.ConnectionAttempts++}/{StaticHelpers.AllowedConnectionFails})"); + Log.WriteDebug($"Sending {payload.Length} bytes to [{this.Endpoint}] ({connectionState.ConnectionAttempts++}/{StaticHelpers.AllowedConnectionFails})"); #endif try { response = await SendPayloadAsync(payload); + connectionState.OnComplete.Release(1); } catch (Exception ex) { - if(connectionState.ConnectionAttempts < StaticHelpers.AllowedConnectionFails) + if (connectionState.ConnectionAttempts < StaticHelpers.AllowedConnectionFails) { connectionState.ConnectionAttempts++; Log.WriteWarning($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]} [{this.Endpoint}] ({connectionState.ConnectionAttempts++}/{StaticHelpers.AllowedConnectionFails})"); - await Task.Delay(StaticHelpers.SocketTimeout.Milliseconds); + await Task.Delay(StaticHelpers.FloodProtectionInterval); goto retrySend; } - ActiveQueries.TryRemove(this.Endpoint, out _); + // the next thread can go ahead and enter + connectionState.OnComplete.Release(1); + Log.WriteDebug(ex.GetExceptionInfo()); throw new NetworkException($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]} [{this.Endpoint}]"); } - ActiveQueries.TryRemove(this.Endpoint, out _); - + connectionState.ConnectionAttempts = 0; string responseString = Utilities.EncodingType.GetString(response, 0, response.Length).TrimEnd('\0') + '\n'; if (responseString.Contains("Invalid password")) { - // todo: localize this - throw new NetworkException("RCON password is invalid"); + throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]); } if (responseString.ToString().Contains("rcon_password")) { - throw new NetworkException("RCON password has not been set"); + throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_NOTSET"]); } string[] splitResponse = responseString.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) @@ -147,6 +159,10 @@ namespace SharedLibraryCore.RCon if (!await connectionState.OnComplete.WaitAsync(StaticHelpers.SocketTimeout.Milliseconds)) { + // we no longer care about the data because the server is being too slow + incomingDataArgs.Completed -= OnDataReceived; + // the next thread can go ahead and make a query + connectionState.OnComplete.Release(1); throw new NetworkException("Timed out waiting for response", rconSocket); } @@ -159,7 +175,10 @@ namespace SharedLibraryCore.RCon #if DEBUG == true Log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}"); #endif - ActiveQueries[this.Endpoint].OnComplete.Release(1); + if (ActiveQueries[this.Endpoint].OnComplete.CurrentCount == 0) + { + ActiveQueries[this.Endpoint].OnComplete.Release(1); + } } private void OnDataSent(object sender, SocketAsyncEventArgs e) diff --git a/SharedLibraryCore/RCon/StaticHelpers.cs b/SharedLibraryCore/RCon/StaticHelpers.cs index 2d63227d9..378a5c9e4 100644 --- a/SharedLibraryCore/RCon/StaticHelpers.cs +++ b/SharedLibraryCore/RCon/StaticHelpers.cs @@ -39,7 +39,7 @@ namespace SharedLibraryCore.RCon /// /// timeout in seconds to wait for a socket send or receive before giving up /// - public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 0, 0, 150); + public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 0, 0,150); /// /// interval in milliseconds to wait before sending the next RCon request ///