using SharedLibraryCore.Objects; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace SharedLibraryCore.Database.Models { public partial class EFClient { public enum ClientState { /// /// represents when the client has been detected as joining /// by the log file, but has not be authenticated by RCon /// Connecting, /// /// represents when the client has been authenticated by RCon /// and validated by the database /// Connected, /// /// represents when the client is leaving (either through RCon or log file) /// Disconnecting, } public enum Permission { /// /// client has been banned /// Banned = -1, /// /// default client state upon first connect /// User = 0, /// /// client has been flagged /// Flagged = 1, /// /// client is trusted /// Trusted = 2, /// /// client is a moderator /// Moderator = 3, /// /// client is an administrator /// Administrator = 4, /// /// client is a senior administrator /// SeniorAdmin = 5, /// /// client is a owner /// Owner = 6, /// /// not used /// Creator = 7, /// /// reserved for default account /// Console = 8 } public EFClient() { ConnectionTime = DateTime.UtcNow; ClientNumber = -1; _additionalProperties = new Dictionary { { "_reportCount", 0 } }; ReceivedPenalties = new List(); } public override string ToString() { return $"{CurrentAlias?.Name ?? "--"}::{NetworkId}"; } /// /// send a message directly to the connected client /// /// message content to send to client public GameEvent Tell(String message) { var e = new GameEvent() { Message = message, Target = this, Owner = CurrentServer, Type = GameEvent.EventType.Tell, Data = message }; this.CurrentServer?.Manager.GetEventHandler().AddEvent(e); return e; } /// /// warn a client with given reason /// /// reason for warn /// client performing the warn public GameEvent Warn(String warnReason, EFClient sender) { var e = new GameEvent() { Type = GameEvent.EventType.Warn, Message = warnReason, Data = warnReason, Origin = sender, Target = this, Owner = sender.CurrentServer }; // enforce level restrictions if (this.Level > sender.Level) { e.FailReason = GameEvent.EventFailReason.Permission; } else { this.Warnings++; } sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } /// /// clear all warnings for a client /// /// client performing the warn clear /// public GameEvent WarnClear(EFClient sender) { var e = new GameEvent() { Type = GameEvent.EventType.WarnClear, Origin = sender, Target = this, Owner = sender.CurrentServer }; // enforce level restrictions if (sender.Level <= this.Level) { e.FailReason = GameEvent.EventFailReason.Permission; return e; } this.Warnings = 0; sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } /// /// report a client for a given reason /// /// reason for the report /// client performing the report /// public GameEvent Report(string reportReason, EFClient sender) { var e = new GameEvent() { Type = GameEvent.EventType.Report, Message = reportReason, Data = reportReason, Origin = sender, Target = this, Owner = sender.CurrentServer }; int reportCount = sender.GetAdditionalProperty("_reportCount"); if (this.Level > sender.Level) { e.FailReason = GameEvent.EventFailReason.Permission; } else if (this.Equals(sender)) { e.FailReason = GameEvent.EventFailReason.Invalid; } else if (reportCount > 2) { e.FailReason = GameEvent.EventFailReason.Throttle; } else if (CurrentServer.Reports.Count(report => (report.Origin.NetworkId == sender.NetworkId && report.Target.NetworkId == this.NetworkId)) > 0) { e.FailReason = GameEvent.EventFailReason.Exception; } sender.SetAdditionalProperty("_reportCount", reportCount + 1); sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } /// /// flag a client for a given reason /// /// reason for flagging /// client performing the flag /// game event for the flag public GameEvent Flag(string flagReason, EFClient sender) { var e = new GameEvent() { Type = GameEvent.EventType.Flag, Origin = sender, Data = flagReason, Message = flagReason, Target = this, Owner = sender.CurrentServer }; if (this.Level >= sender.Level) { e.FailReason = GameEvent.EventFailReason.Permission; } else if (this.Level == Permission.Flagged) { e.FailReason = GameEvent.EventFailReason.Invalid; } sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } /// /// unflag a client for a given reason /// /// reason to unflag a player for /// client performing the unflag /// game event for the un flug public GameEvent Unflag(string unflagReason, EFClient sender) { var e = new GameEvent() { Type = GameEvent.EventType.Unflag, Origin = sender, Target = this, Data = unflagReason, Message = unflagReason, Owner = sender.CurrentServer }; if (sender.Level <= this.Level) { e.FailReason = GameEvent.EventFailReason.Permission; } else if (this.Level != Permission.Flagged) { e.FailReason = GameEvent.EventFailReason.Invalid; } sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } /// /// kick a client for the given reason /// /// reason to kick for /// client performing the kick public GameEvent Kick(String kickReason, EFClient sender) { var e = new GameEvent() { Type = GameEvent.EventType.Kick, Message = kickReason, Target = this, Origin = sender, Data = kickReason, Owner = sender.CurrentServer }; // enforce level restrictions if (this.Level > sender.Level) { e.FailReason = GameEvent.EventFailReason.Permission; } sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } /// /// temporarily ban a client for the given time span /// /// reason for the temp ban /// how long the temp ban lasts /// client performing the tempban public GameEvent TempBan(String tempbanReason, TimeSpan banLength, EFClient sender) { var e = new GameEvent() { Type = GameEvent.EventType.TempBan, Message = tempbanReason, Data = tempbanReason, Origin = sender, Target = this, Extra = banLength, Owner = sender.CurrentServer }; // enforce level restrictions if (sender.Level <= this.Level) { e.FailReason = GameEvent.EventFailReason.Permission; } sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } /// /// permanently ban a client /// /// reason for the ban /// client performing the ban public GameEvent Ban(String banReason, EFClient sender, bool isEvade) { var e = new GameEvent() { Type = GameEvent.EventType.Ban, Message = banReason, Data = banReason, Origin = sender, Target = this, Owner = sender.CurrentServer, Extra = isEvade }; // enforce level restrictions if (sender.Level <= this.Level) { e.FailReason = GameEvent.EventFailReason.Permission; } sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } /// /// unban a client /// /// reason for the unban /// client performing the unban /// public GameEvent Unban(string unbanReason, EFClient sender) { var e = new GameEvent() { Type = GameEvent.EventType.Unban, Message = unbanReason, Data = unbanReason, Origin = sender, Target = this, Owner = sender.CurrentServer }; // enforce level restrictions if (this.Level > sender.Level) { e.FailReason = GameEvent.EventFailReason.Permission; } sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } /// /// sets the level of the client /// /// new permission to set client to /// user performing the set level /// public GameEvent SetLevel(Permission newPermission, EFClient sender) { var e = new GameEvent() { Type = GameEvent.EventType.ChangePermission, Extra = newPermission, Origin = sender, Target = this, Owner = sender.CurrentServer }; if (this.Level > sender.Level) { e.FailReason = GameEvent.EventFailReason.Permission; } sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); return e; } /// /// Handles any client related logic on connection /// public void OnConnect() { var loc = Utilities.CurrentLocalization.LocalizationIndex; LastConnection = DateTime.UtcNow; Connections += 1; if (Name.Length < 3) { CurrentServer.Logger.WriteDebug($"Kicking {this} because their name is too short"); Kick(loc["SERVER_KICK_MINNAME"], Utilities.IW4MAdminClient(CurrentServer)); return; } if (CurrentServer.Manager.GetApplicationSettings().Configuration() .DisallowedClientNames ?.Any(_name => Regex.IsMatch(Name, _name)) ?? false) { CurrentServer.Logger.WriteDebug($"Kicking {this} because their name is generic"); Kick(loc["SERVER_KICK_GENERICNAME"], Utilities.IW4MAdminClient(CurrentServer)); return; } if (Name.Where(c => char.IsControl(c)).Count() > 0) { CurrentServer.Logger.WriteDebug($"Kicking {this} because their name contains control characters"); Kick(loc["SERVER_KICK_CONTROLCHARS"], Utilities.IW4MAdminClient(CurrentServer)); return; } // reserved slots stuff // todo: bots don't seem to honor party_maxplayers/sv_maxclients if (CurrentServer.MaxClients - (CurrentServer.GetClientsAsList().Count(_client => !_client.IsPrivileged() && !_client.IsBot)) < CurrentServer.ServerConfig.ReservedSlotNumber && !this.IsPrivileged() && CurrentServer.GetClientsAsList().Count <= CurrentServer.MaxClients && CurrentServer.MaxClients != 0) { CurrentServer.Logger.WriteDebug($"Kicking {this} their spot is reserved"); Kick(loc["SERVER_KICK_SLOT_IS_RESERVED"], Utilities.IW4MAdminClient(CurrentServer)); return; } } public async Task OnDisconnect() { State = ClientState.Disconnecting; TotalConnectionTime += ConnectionLength; LastConnection = DateTime.UtcNow; await CurrentServer.Manager.GetClientService().Update(this); } public async Task OnJoin(int? ipAddress) { CurrentServer.Logger.WriteDebug($"Start join for {this}::{ipAddress}::{Level.ToString()}"); if (ipAddress != null) { IPAddress = ipAddress; await CurrentServer.Manager.GetClientService().UpdateAlias(this); } // we want to run any non GUID based logic here OnConnect(); if (await CanConnect(ipAddress)) { if (IPAddress != null) { await CurrentServer.Manager.GetClientService().Update(this); var e = new GameEvent() { Type = GameEvent.EventType.Join, Origin = this, Target = this, Owner = CurrentServer }; CurrentServer.Manager.GetEventHandler().AddEvent(e); } } else { CurrentServer.Logger.WriteDebug($"Client {this} is not allowed to join the server"); } CurrentServer.Logger.WriteDebug($"OnJoin finished for {this}"); } private async Task CanConnect(int? ipAddress) { var loc = Utilities.CurrentLocalization.LocalizationIndex; var autoKickClient = Utilities.IW4MAdminClient(CurrentServer); #region CLIENT_GUID_BAN // kick them as their level is banned if (Level == Permission.Banned) { var profileBan = ReceivedPenalties.FirstOrDefault(_penalty => _penalty.Expires == null && _penalty.Active); if (profileBan == null) { // this is from the old system before bans were applied to all accounts profileBan = (await CurrentServer.Manager .GetPenaltyService() .GetActivePenaltiesAsync(AliasLinkId)) .FirstOrDefault(_penalty => _penalty.Type == Penalty.PenaltyType.Ban); CurrentServer.Logger.WriteWarning($"Client {this} is GUID banned, but no previous penalty exists for their ban"); if (profileBan == null) { profileBan = new EFPenalty() { Offense = loc["SERVER_BAN_UNKNOWN"] }; CurrentServer.Logger.WriteWarning($"Client {this} is GUID banned, but we could not find the penalty on any linked accounts"); } // hack: re apply the automated offense to the reban if (profileBan.AutomatedOffense != null) { autoKickClient.AdministeredPenalties?.Add(new EFPenalty() { AutomatedOffense = profileBan.AutomatedOffense }); } // this is a reban of the new GUID and IP Ban($"{profileBan.Offense}", autoKickClient, false); return false; } CurrentServer.Logger.WriteDebug($"Kicking {this} because they are banned"); Kick($"{loc["SERVER_BAN_PREV"]} {profileBan?.Offense}", autoKickClient); return false; } #endregion #region CLIENT_GUID_TEMPBAN else { var profileTempBan = ReceivedPenalties.FirstOrDefault(_penalty => _penalty.Type == Penalty.PenaltyType.TempBan && _penalty.Active && _penalty.Expires > DateTime.UtcNow); // they have an active tempban tied to their GUID if (profileTempBan != null) { CurrentServer.Logger.WriteDebug($"Kicking {this} because their GUID is temporarily banned"); Kick($"{loc["SERVER_TB_REMAIN"]} ({(profileTempBan.Expires.Value - DateTime.UtcNow).TimeSpanText()} {loc["WEBFRONT_PENALTY_TEMPLATE_REMAINING"]})", autoKickClient); return false; } } #endregion // we want to get any penalties that are tied to their IP or AliasLink (but not necessarily their GUID) var activePenalties = await CurrentServer.Manager.GetPenaltyService().GetActivePenaltiesAsync(AliasLinkId, ipAddress); #region CLIENT_LINKED_TEMPBAN var tempBan = activePenalties.FirstOrDefault(_penalty => _penalty.Type == Penalty.PenaltyType.TempBan); // they have an active tempban tied to their AliasLink if (tempBan != null) { CurrentServer.Logger.WriteDebug($"Tempbanning {this} because their AliasLink is temporarily banned, but they are not"); TempBan(tempBan.Offense, DateTime.UtcNow - (tempBan.Expires ?? DateTime.UtcNow), autoKickClient); return false; } #endregion #region CLIENT_LINKED_BAN var currentBan = activePenalties.FirstOrDefault(p => p.Type == Penalty.PenaltyType.Ban); // they have a perm ban tied to their AliasLink/profile if (currentBan != null) { CurrentServer.Logger.WriteInfo($"Banned client {this} trying to evade..."); // reban the "evading" guid if (Level != Permission.Banned) { CurrentServer.Logger.WriteInfo($"Banned client {this} connected using a new GUID"); // hack: re apply the automated offense to the reban if (currentBan.AutomatedOffense != null) { autoKickClient.AdministeredPenalties?.Add(new EFPenalty() { AutomatedOffense = currentBan.AutomatedOffense }); } // this is a reban of the new GUID and IP Ban($"{currentBan.Offense}", autoKickClient, true); } else { CurrentServer.Logger.WriteError($"Banned client {this} is banned but, no ban penalty was found (2)"); } return false; } #endregion #region CLIENT_LINKED_FLAG if (Level != Permission.Flagged) { var currentFlag = activePenalties.FirstOrDefault(_penalty => _penalty.Type == Penalty.PenaltyType.Flag); if (currentFlag != null) { CurrentServer.Logger.WriteDebug($"Flagging {this} because their AliasLink is flagged, but they are not"); Flag(currentFlag.Offense, autoKickClient); } } #endregion if (Level == Permission.Flagged) { var currentAutoFlag = activePenalties .Where(p => p.Type == Penalty.PenaltyType.Flag && p.PunisherId == 1) .OrderByDescending(p => p.When) .FirstOrDefault(); // remove their auto flag status after a week if (currentAutoFlag != null && (DateTime.UtcNow - currentAutoFlag.When).TotalDays > 7) { CurrentServer.Logger.WriteInfo($"Unflagging {this} because the auto flag time has expired"); Unflag(Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTOFLAG_UNFLAG"], autoKickClient); } } return true; } [NotMapped] Dictionary _additionalProperties; public T GetAdditionalProperty(string name) { return _additionalProperties.ContainsKey(name) ? (T)_additionalProperties[name] : default(T); } public void SetAdditionalProperty(string name, object value) { if (_additionalProperties.ContainsKey(name)) { _additionalProperties[name] = value; } else { _additionalProperties.Add(name, value); } } [NotMapped] public int ClientNumber { get; set; } [NotMapped] public int Ping { get; set; } [NotMapped] public int Warnings { get; set; } [NotMapped] public DateTime ConnectionTime { get; set; } [NotMapped] public int ConnectionLength => (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds; [NotMapped] public Server CurrentServer { get; set; } [NotMapped] public int Score { get; set; } [NotMapped] public bool IsBot { get; set; } [NotMapped] public ClientState State { get; set; } [NotMapped] // this is kinda dirty, but I need localizable level names public ClientPermission ClientPermission => new ClientPermission() { Level = Level, Name = Utilities.CurrentLocalization .LocalizationIndex[$"GLOBAL_PERMISSION_{Level.ToString().ToUpper()}"] }; public override bool Equals(object obj) { return ((EFClient)obj).NetworkId == this.NetworkId; } public override int GetHashCode() { return (int)NetworkId; } } }