IW4M-Admin/SharedLibraryCore/PartialEntities/EFClient.cs
RaidMax b086190ab0 renable weapon name in anticheat snapshot list
update migrations for unique index
fix missing total connection time
include total connection time in get client query
2019-11-25 12:05:12 -06:00

710 lines
24 KiB
C#

using SharedLibraryCore.Localization;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace SharedLibraryCore.Database.Models
{
public partial class EFClient
{
public enum ClientState
{
/// <summary>
/// represents when the client has been detected as joining
/// by the log file, but has not be authenticated by RCon
/// </summary>
Connecting,
/// <summary>
/// represents when the client has been authenticated by RCon
/// and validated by the database
/// </summary>
Connected,
/// <summary>
/// represents when the client is leaving (either through RCon or log file)
/// </summary>
Disconnecting,
}
public enum Permission
{
/// <summary>
/// client has been banned
/// </summary>
Banned = -1,
/// <summary>
/// default client state upon first connect
/// </summary>
User = 0,
/// <summary>
/// client has been flagged
/// </summary>
Flagged = 1,
/// <summary>
/// client is trusted
/// </summary>
Trusted = 2,
/// <summary>
/// client is a moderator
/// </summary>
Moderator = 3,
/// <summary>
/// client is an administrator
/// </summary>
Administrator = 4,
/// <summary>
/// client is a senior administrator
/// </summary>
SeniorAdmin = 5,
/// <summary>
/// client is a owner
/// </summary>
Owner = 6,
/// <summary>
/// not used
/// </summary>
Creator = 7,
/// <summary>
/// reserved for default account
/// </summary>
Console = 8
}
public EFClient()
{
ConnectionTime = DateTime.UtcNow;
ClientNumber = -1;
_additionalProperties = new Dictionary<string, object>
{
{ "_reportCount", 0 }
};
ReceivedPenalties = new List<EFPenalty>();
ProcessingEvent = new SemaphoreSlim(1, 1);
}
~EFClient()
{
ProcessingEvent.Dispose();
}
public override string ToString()
{
return $"{CurrentAlias?.Name ?? "--"}::{NetworkId}";
}
[NotMapped]
public virtual string Name
{
get { return CurrentAlias?.Name ?? "--"; }
set { if (CurrentAlias != null) CurrentAlias.Name = value; }
}
[NotMapped]
public string CleanedName => Name?.StripColors();
[NotMapped]
public virtual int? IPAddress
{
get { return CurrentAlias.IPAddress; }
set { CurrentAlias.IPAddress = value; }
}
[NotMapped]
public string IPAddressString => IPAddress.ConvertIPtoString();
[NotMapped]
public virtual IDictionary<int, long> LinkedAccounts { get; set; }
/// <summary>
/// send a message directly to the connected client
/// </summary>
/// <param name="message">message content to send to client</param>
public GameEvent Tell(String message)
{
var e = new GameEvent()
{
Message = message,
Target = this,
Owner = CurrentServer,
Type = GameEvent.EventType.Tell,
Data = message
};
CurrentServer?.Manager.GetEventHandler().AddEvent(e);
return e;
}
/// <summary>
/// warn a client with given reason
/// </summary>
/// <param name="warnReason">reason for warn</param>
/// <param name="sender">client performing the warn</param>
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 (Level > sender.Level)
{
e.FailReason = GameEvent.EventFailReason.Permission;
}
else
{
Warnings++;
}
sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
return e;
}
/// <summary>
/// clear all warnings for a client
/// </summary>
/// <param name="sender">client performing the warn clear</param>
/// <returns></returns>
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 <= Level)
{
e.FailReason = GameEvent.EventFailReason.Permission;
return e;
}
Warnings = 0;
sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
return e;
}
/// <summary>
/// report a client for a given reason
/// </summary>
/// <param name="reportReason">reason for the report</param>
/// <param name="sender">client performing the report</param>
/// <returns></returns>
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<int>("_reportCount");
if (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 == NetworkId)) > 0)
{
e.FailReason = GameEvent.EventFailReason.Exception;
}
sender.SetAdditionalProperty("_reportCount", reportCount + 1);
sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
return e;
}
/// <summary>
/// flag a client for a given reason
/// </summary>
/// <param name="flagReason">reason for flagging</param>
/// <param name="sender">client performing the flag</param>
/// <returns>game event for the flag</returns>
public GameEvent Flag(string flagReason, EFClient sender, TimeSpan? flagLength = null)
{
var e = new GameEvent()
{
Type = GameEvent.EventType.Flag,
Origin = sender,
Data = flagReason,
Message = flagReason,
Extra = flagLength,
Target = this,
Owner = sender.CurrentServer
};
if (Level >= sender.Level)
{
e.FailReason = GameEvent.EventFailReason.Permission;
}
else if (Level == Permission.Flagged || Level == Permission.Banned)
{
e.FailReason = GameEvent.EventFailReason.Invalid;
}
sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
return e;
}
/// <summary>
/// unflag a client for a given reason
/// </summary>
/// <param name="unflagReason">reason to unflag a player for</param>
/// <param name="sender">client performing the unflag</param>
/// <returns>game event for the un flug</returns>
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 <= Level)
{
e.FailReason = GameEvent.EventFailReason.Permission;
}
else if (Level != Permission.Flagged || Level == Permission.Banned)
{
e.FailReason = GameEvent.EventFailReason.Invalid;
}
sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
return e;
}
/// <summary>
/// kick a client for the given reason
/// </summary>
/// <param name="kickReason">reason to kick for</param>
/// <param name="sender">client performing the kick</param>
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 (Level > sender.Level)
{
e.FailReason = GameEvent.EventFailReason.Permission;
}
sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
return e;
}
/// <summary>
/// temporarily ban a client for the given time span
/// </summary>
/// <param name="tempbanReason">reason for the temp ban</param>
/// <param name="banLength">how long the temp ban lasts</param>
/// <param name="sender">client performing the tempban</param>
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 <= Level)
{
e.FailReason = GameEvent.EventFailReason.Permission;
}
sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
return e;
}
/// <summary>
/// permanently ban a client
/// </summary>
/// <param name="banReason">reason for the ban</param>
/// <param name="sender">client performing the ban</param>
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 <= Level)
{
e.FailReason = GameEvent.EventFailReason.Permission;
}
if (Level == Permission.Banned)
{
e.FailReason = GameEvent.EventFailReason.Invalid;
}
sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
return e;
}
/// <summary>
/// unban a client
/// </summary>
/// <param name="unbanReason">reason for the unban</param>
/// <param name="sender">client performing the unban</param>
/// <returns></returns>
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 (Level > sender.Level)
{
e.FailReason = GameEvent.EventFailReason.Permission;
}
sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
return e;
}
/// <summary>
/// sets the level of the client
/// </summary>
/// <param name="newPermission">new permission to set client to</param>
/// <param name="sender">user performing the set level</param>
/// <returns></returns>
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 (Level > sender.Level)
{
e.FailReason = GameEvent.EventFailReason.Permission;
}
else if (Level == newPermission)
{
e.FailReason = GameEvent.EventFailReason.Invalid;
}
else
{
Level = newPermission;
}
sender.CurrentServer.Manager.GetEventHandler().AddEvent(e);
return e;
}
/// <summary>
/// Handles any client related logic on connection
/// </summary>
public bool IsAbleToConnectSimple()
{
var loc = Utilities.CurrentLocalization.LocalizationIndex;
if (string.IsNullOrWhiteSpace(Name) || CleanedName.Replace(" ", "").Length < 3)
{
CurrentServer.Logger.WriteDebug($"Kicking {this} because their name is too short");
Kick(loc["SERVER_KICK_MINNAME"], Utilities.IW4MAdminClient(CurrentServer));
return false;
}
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 false;
}
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 false;
}
// 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 false;
}
return true;
}
public async Task OnDisconnect()
{
State = ClientState.Disconnecting;
TotalConnectionTime += ConnectionLength;
LastConnection = DateTime.UtcNow;
try
{
await CurrentServer.Manager.GetClientService().Update(this);
}
catch (Exception e)
{
CurrentServer.Logger.WriteWarning($"Could not update disconnected player {this}");
CurrentServer.Logger.WriteDebug(e.GetExceptionInfo());
}
}
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);
CurrentServer.Logger.WriteDebug($"Updated alias for {this}");
await CurrentServer.Manager.GetClientService().Update(this);
CurrentServer.Logger.WriteDebug($"Updated client for {this}");
bool canConnect = await CanConnect(ipAddress);
if (!canConnect)
{
CurrentServer.Logger.WriteDebug($"Client {this} is not allowed to join the server");
}
else
{
CurrentServer.Logger.WriteDebug($"Creating join event for {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} does not have an IP yet");
}
CurrentServer.Logger.WriteDebug($"OnJoin finished for {this}");
}
private async Task<bool> CanConnect(int? ipAddress)
{
var loc = Utilities.CurrentLocalization.LocalizationIndex;
var autoKickClient = Utilities.IW4MAdminClient(CurrentServer);
bool isAbleToConnectSimple = IsAbleToConnectSimple();
if (!isAbleToConnectSimple)
{
return false;
}
// 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);
var banPenalty = activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.Ban);
var tempbanPenalty = activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.TempBan);
var flagPenalty = activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.Flag);
// we want to kick them if any account is banned
if (banPenalty != null)
{
if (Level == Permission.Banned)
{
CurrentServer.Logger.WriteDebug($"Kicking {this} because they are banned");
Kick(loc["SERVER_BAN_PREV"].FormatExt(banPenalty?.Offense), autoKickClient);
return false;
}
else
{
CurrentServer.Logger.WriteDebug($"Client {this} is banned, but using a new GUID, we we're updating their level and kicking them");
await SetLevel(Permission.Banned, autoKickClient).WaitAsync(Utilities.DefaultCommandTimeout, CurrentServer.Manager.CancellationToken);
Kick(loc["SERVER_BAN_PREV"].FormatExt(banPenalty?.Offense), autoKickClient);
return false;
}
}
// we want to kick them if any account is tempbanned
if (tempbanPenalty != null)
{
CurrentServer.Logger.WriteDebug($"Kicking {this} because their GUID is temporarily banned");
Kick($"{loc["SERVER_TB_REMAIN"]} ({(tempbanPenalty.Expires.Value - DateTime.UtcNow).TimeSpanText()} {loc["WEBFRONT_PENALTY_TEMPLATE_REMAINING"]})", autoKickClient);
return false;
}
// if we found a flag, we need to make sure all the accounts are flagged
if (flagPenalty != null && Level != Permission.Flagged)
{
CurrentServer.Logger.WriteDebug($"Flagged client {this} joining with new GUID, so we are changing their level to flagged");
await SetLevel(Permission.Flagged, autoKickClient).WaitAsync(Utilities.DefaultCommandTimeout, CurrentServer.Manager.CancellationToken);
}
// remove their auto flag
if (Level == Permission.Flagged && !activePenalties.Any(_penalty => _penalty.Type == EFPenalty.PenaltyType.Flag))
{
// remove their auto flag status after a week
CurrentServer.Logger.WriteInfo($"Unflagging {this} because the auto flag time has expired");
Unflag(Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTOFLAG_UNFLAG"], autoKickClient);
}
return true;
}
[NotMapped]
readonly Dictionary<string, object> _additionalProperties;
public T GetAdditionalProperty<T>(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 => NetworkId == -1;
[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()}"]
};
[NotMapped]
public SemaphoreSlim ProcessingEvent;
public async Task Lock()
{
bool result = await ProcessingEvent.WaitAsync(Utilities.DefaultCommandTimeout);
#if DEBUG
if (!result)
{
throw new InvalidOperationException();
}
#endif
}
public void Unlock()
{
if (ProcessingEvent.CurrentCount == 0)
{
ProcessingEvent.Release(1);
}
}
public override bool Equals(object obj)
{
return ((EFClient)obj).NetworkId == this.NetworkId;
}
public override int GetHashCode()
{
return IsBot ? ClientNumber : (int)NetworkId;
}
}
}