diff --git a/Projects/Server/Network/IPLimiter.cs b/Projects/Server/Network/IPLimiter.cs index c0f1aec10..046ccd639 100644 --- a/Projects/Server/Network/IPLimiter.cs +++ b/Projects/Server/Network/IPLimiter.cs @@ -16,100 +16,103 @@ using System; using System.Collections.Generic; using System.Net; -using System.Runtime.InteropServices; namespace Server.Misc; public static class IPLimiter { - private static readonly Dictionary _connectionAttempts = new(128); - private static readonly HashSet _throttledAddresses = new(); + private static readonly SortedSet _connectionAttempts = []; + private static readonly SortedSet _throttledAddresses = []; - private static long _lastClearedThrottles; - private static long _lastClearedAttempts; + private static readonly IPAddress _localHost = IPAddress.Parse("127.0.0.1"); - public static readonly IPAddress[] Exemptions = - { - IPAddress.Parse( "127.0.0.1" ) - }; - - public static TimeSpan ClearConnectionAttemptsDuration { get; private set; } - - public static TimeSpan ClearThrottledDuration { get; private set; } + public static TimeSpan ConnectionAttemptsDuration { get; private set; } + public static TimeSpan ConnectionThrottleDuration { get; private set; } public static bool Enabled { get; private set; } - public static int MaxAddresses { get; private set; } + public static int MaxConnections { get; private set; } public static void Configure() { Enabled = ServerConfiguration.GetOrUpdateSetting("ipLimiter.enable", true); - MaxAddresses = ServerConfiguration.GetOrUpdateSetting("ipLimiter.maxConnectionsPerIP", 10); - ClearConnectionAttemptsDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearConnectionAttemptsDuration", TimeSpan.FromSeconds(10)); - ClearThrottledDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearThrottledDuration", TimeSpan.FromMinutes(2)); + MaxConnections = ServerConfiguration.GetOrUpdateSetting("ipLimiter.maxConnectionsPerIP", 5); + ConnectionAttemptsDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearConnectionAttemptsDuration", TimeSpan.FromSeconds(10)); + ConnectionThrottleDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.connectionThrottleDuration", TimeSpan.FromMinutes(5)); } - public static bool IsExempt(IPAddress ip) - { - for (int i = 0; i < Exemptions.Length; i++) - { - if (ip.Equals(Exemptions[i])) - { - return true; - } - } - - return false; - } + private static readonly IPAccessLog _accessCheck = new(IPAddress.None, DateTime.MinValue); public static bool Verify(IPAddress ourAddress) { - if (!Enabled || IsExempt(ourAddress)) + if (!Enabled || ourAddress.Equals(_localHost)) { return true; } - var now = Core.TickCount; + var now = Core.Now; - if (_throttledAddresses.Count > 0) + CheckThrottledAddresses(now); + + _accessCheck.IPAddress = ourAddress; + + if (_connectionAttempts.TryGetValue(_accessCheck, out var accessLog)) { - if (now - _lastClearedThrottles > ClearThrottledDuration.TotalMilliseconds) - { - _lastClearedThrottles = now; - ClearThrottledAddresses(); - } - else if (_throttledAddresses.Contains(ourAddress)) + _connectionAttempts.Remove(accessLog); + accessLog.Count++; + + if (now <= accessLog.Expiration && accessLog.Count >= MaxConnections) { + BlockConnection(now, accessLog); return false; } - } - if (_connectionAttempts.Count > 0 && now - _lastClearedAttempts > ClearConnectionAttemptsDuration.TotalMilliseconds) + accessLog.Expiration = now + ConnectionAttemptsDuration; + } + else { - _lastClearedAttempts = now; - ClearConnectionAttempts(); + accessLog = new IPAccessLog(ourAddress, now + ConnectionAttemptsDuration); } - ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_connectionAttempts, ourAddress, out _); - count++; - - if (count > MaxAddresses) - { - _connectionAttempts.Remove(ourAddress); - _throttledAddresses.Add(ourAddress); - return false; - } + // Add it back so it is sorted properly + _connectionAttempts.Add(accessLog); return true; } - private static void ClearThrottledAddresses() + private static void BlockConnection(DateTime now, IPAccessLog accessLog) { - _throttledAddresses.Clear(); + accessLog.Expiration = now + ConnectionAttemptsDuration; + _throttledAddresses.Add(accessLog); } - private static void ClearConnectionAttempts() + private static void CheckThrottledAddresses(DateTime now) { - _connectionAttempts.Clear(); - _connectionAttempts.TrimExcess(128); + while (_throttledAddresses.Count > 0) + { + var accessLog = _throttledAddresses.Min; + if (now <= accessLog.Expiration) + { + break; + } + + _throttledAddresses.Remove(accessLog); + } + } + + private class IPAccessLog : IComparable + { + public IPAddress IPAddress; + public DateTime Expiration; + public int Count; + + public IPAccessLog(IPAddress ipAddress, DateTime expiration) + { + IPAddress = ipAddress; + Expiration = expiration; + Count = 1; + } + + public int CompareTo(IPAccessLog other) => + IPAddress.Equals(other.IPAddress) ? 0 : Expiration.CompareTo(other.Expiration); } } diff --git a/Projects/Server/Network/MovementThrottle.cs b/Projects/Server/Network/MovementThrottle.cs index d544729e5..22ce9c1e4 100644 --- a/Projects/Server/Network/MovementThrottle.cs +++ b/Projects/Server/Network/MovementThrottle.cs @@ -33,15 +33,13 @@ public static class MovementThrottle IncomingPackets.RegisterThrottler(0x02, &Throttle); } - public static bool Throttle(int packetId, NetState ns, out bool drop) + public static bool Throttle(int packetId, NetState ns) { - drop = false; - var from = ns.Mobile; if (from?.Deleted != false || from.AccessLevel > AccessLevel.Player) { - return true; + return false; } long now = Core.TickCount; @@ -53,7 +51,7 @@ public static class MovementThrottle { ns._movementCredit = 0; ns._nextMovementTime = now; - return true; + return false; } long cost = nextMove - now; @@ -61,11 +59,11 @@ public static class MovementThrottle if (credit < cost) { // Not enough credit, therefore throttled - return false; + return true; } // On the next event loop, the player receives up to 400ms in grace latency ns._movementCredit = Math.Min(_throttleThreshold, credit - cost); - return true; + return false; } } diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index e4ce75cf2..5391b10d9 100755 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -823,9 +823,9 @@ public partial class NetState : IComparable, IValueLinkListNode OnReceive { get; } - public delegate* ThrottleCallback { get; set; } + public delegate* ThrottleCallback { get; set; } public bool Ingame { get; } } diff --git a/Projects/Server/Network/Packets/IncomingPackets.cs b/Projects/Server/Network/Packets/IncomingPackets.cs index db7098a07..a144b2d55 100644 --- a/Projects/Server/Network/Packets/IncomingPackets.cs +++ b/Projects/Server/Network/Packets/IncomingPackets.cs @@ -57,7 +57,7 @@ public static class IncomingPackets } } - public static unsafe void RegisterThrottler(int packetID, delegate* t) + public static unsafe void RegisterThrottler(int packetID, delegate* t) { var ph = GetHandler(packetID); diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index 27c30191f..8ec399bd0 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -223,7 +223,6 @@ public static class TcpServer private static void ProcessConnection(Socket socket) { - var ipLimiter = IPLimiter.Enabled; try { var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address; @@ -247,15 +246,6 @@ public static class TcpServer return; } - if (ipLimiter && !IPLimiter.Verify(remoteIP)) - { - TraceDisconnect("Past IP limit threshold", remoteIP); - logger.Debug("{Address} Past IP limit threshold", remoteIP); - - CloseSocket(socket); - return; - } - var firewalled = Firewall.IsBlocked(remoteIP); if (!firewalled) { @@ -273,6 +263,15 @@ public static class TcpServer return; } + if (!IPLimiter.Verify(remoteIP)) + { + TraceDisconnect("Past IP limit threshold", remoteIP); + logger.Debug("{Address} Past IP limit threshold", remoteIP); + + CloseSocket(socket); + return; + } + var ns = new NetState(socket); ConnectedQueue.Enqueue(ns); } diff --git a/Projects/UOContent/Accounting/Account.Migrations.cs b/Projects/UOContent/Accounting/Account.Migrations.cs index bbfc108f6..cd28d22d9 100644 --- a/Projects/UOContent/Accounting/Account.Migrations.cs +++ b/Projects/UOContent/Accounting/Account.Migrations.cs @@ -3,121 +3,136 @@ using System.IO; using System.Net; using Server.Accounting.Security; -namespace Server.Accounting +namespace Server.Accounting; + +public partial class Account { - public partial class Account + // Deleted IP Restrictions + private void MigrateFrom(V5Content content) { - // Username was not interned - private void MigrateFrom(V4Content content) + _username = content.Username; + _passwordAlgorithm = content.PasswordAlgorithm; + _password = content.Password; + _accessLevel = content.AccessLevel; + _flags = content.Flags; + _lastLogin = content.LastLogin; + _totalGold = content.TotalGold; + _totalPlat = content.TotalPlat; + _mobiles = content.Mobiles; + _comments = content.Comments; + _tags = content.Tags; + _loginIPs = content.LoginIPs; + _totalGameTime = content.TotalGameTime; + _email = content.Email; + } + + // Username was not interned + private void MigrateFrom(V4Content content) + { + _username = content.Username; + _passwordAlgorithm = content.PasswordAlgorithm; + _password = content.Password; + _accessLevel = content.AccessLevel; + _flags = content.Flags; + _lastLogin = content.LastLogin; + _totalGold = content.TotalGold; + _totalPlat = content.TotalPlat; + _mobiles = content.Mobiles; + _comments = content.Comments; + _tags = content.Tags; + _loginIPs = content.LoginIPs; + _totalGameTime = content.TotalGameTime; + _email = content.Email; + } + + private void MigrateFrom(V3Content content) + { + _username = content.Username; + _username.Intern(); + _passwordAlgorithm = content.PasswordAlgorithm; + _password = content.Password; + _accessLevel = content.AccessLevel; + _flags = content.Flags; + Created = content.Created; + _lastLogin = content.LastLogin; + _totalGold = content.TotalGold; + _totalPlat = content.TotalPlat; + _mobiles = content.Mobiles; + _comments = content.Comments; + _tags = content.Tags; + _loginIPs = content.LoginIPs; + _totalGameTime = content.TotalGameTime; + _email = content.Email; + } + + private void Deserialize(IGenericReader reader, int version) + { + if (version != 2) { - _username = content.Username; - _passwordAlgorithm = content.PasswordAlgorithm; - _password = content.Password; - _accessLevel = content.AccessLevel; - _flags = content.Flags; - _lastLogin = content.LastLogin; - _totalGold = content.TotalGold; - _totalPlat = content.TotalPlat; - _mobiles = content.Mobiles; - _comments = content.Comments; - _tags = content.Tags; - _loginIPs = content.LoginIPs; - _ipRestrictions = content.IpRestrictions; - _totalGameTime = content.TotalGameTime; - _email = content.Email; + // Due to a bug where we were not versioning at all, reset so we don't have an issue deserializing + reader.Seek(0, SeekOrigin.Begin); } - private void MigrateFrom(V3Content content) + _username = reader.ReadString(true); + _passwordAlgorithm = version < 2 ? (PasswordProtectionAlgorithm)reader.ReadInt() : reader.ReadEnum(); + _password = reader.ReadString(); + _accessLevel = version < 2 ? (AccessLevel)reader.ReadInt() : reader.ReadEnum(); + _flags = reader.ReadInt(); + Created = reader.ReadDateTime(); + _lastLogin = reader.ReadDateTime(); + + _totalGold = reader.ReadInt(); + _totalPlat = reader.ReadInt(); + + var length = reader.ReadInt(); + _mobiles = new Mobile[length]; + for (int i = 0; i < length; i++) { - _username = content.Username; - _username.Intern(); - _passwordAlgorithm = content.PasswordAlgorithm; - _password = content.Password; - _accessLevel = content.AccessLevel; - _flags = content.Flags; - Created = content.Created; - _lastLogin = content.LastLogin; - _totalGold = content.TotalGold; - _totalPlat = content.TotalPlat; - _mobiles = content.Mobiles; - _comments = content.Comments; - _tags = content.Tags; - _loginIPs = content.LoginIPs; - _ipRestrictions = content.IpRestrictions; - _totalGameTime = content.TotalGameTime; - _email = content.Email; + _mobiles[i] = reader.ReadEntity(); } - private void Deserialize(IGenericReader reader, int version) + length = reader.ReadInt(); + _comments = length > 0 ? new List(length) : null; + for (int i = 0; i < length; i++) { - if (version != 2) + _comments!.Add(new AccountComment(reader)); + } + + length = reader.ReadInt(); + _tags = length > 0 ? new List(length) : null; + for (int i = 0; i < length; i++) + { + _tags!.Add(new AccountTag(reader)); + } + + length = reader.ReadInt(); + _loginIPs = new IPAddress[length]; + for (int i = 0; i < length; i++) + { + if (version < 2) { - // Due to a bug where we were not versioning at all, reset so we don't have an issue deserializing - reader.Seek(0, SeekOrigin.Begin); - } - - _username = reader.ReadString(true); - _passwordAlgorithm = version < 2 ? (PasswordProtectionAlgorithm)reader.ReadInt() : reader.ReadEnum(); - _password = reader.ReadString(); - _accessLevel = version < 2 ? (AccessLevel)reader.ReadInt() : reader.ReadEnum(); - _flags = reader.ReadInt(); - Created = reader.ReadDateTime(); - _lastLogin = reader.ReadDateTime(); - - _totalGold = reader.ReadInt(); - _totalPlat = reader.ReadInt(); - - var length = reader.ReadInt(); - _mobiles = new Mobile[length]; - for (int i = 0; i < length; i++) - { - _mobiles[i] = reader.ReadEntity(); - } - - length = reader.ReadInt(); - _comments = length > 0 ? new List(length) : null; - for (int i = 0; i < length; i++) - { - _comments!.Add(new AccountComment(reader)); - } - - length = reader.ReadInt(); - _tags = length > 0 ? new List(length) : null; - for (int i = 0; i < length; i++) - { - _tags!.Add(new AccountTag(reader)); - } - - length = reader.ReadInt(); - _loginIPs = new IPAddress[length]; - for (int i = 0; i < length; i++) - { - if (version < 2) + if (IPAddress.TryParse(reader.ReadString(), out var address)) { - if (IPAddress.TryParse(reader.ReadString(), out var address)) - { - _loginIPs[i] = Utility.Intern(address); - } - } - else - { - _loginIPs[i] = reader.ReadIPAddress(); + _loginIPs[i] = Utility.Intern(address); } } - - length = reader.ReadInt(); - _ipRestrictions = new string[length]; - for (int i = 0; i < length; i++) + else { - _ipRestrictions[i] = reader.ReadString(); + _loginIPs[i] = reader.ReadIPAddress(); } + } - _totalGameTime = reader.ReadTimeSpan(); + length = reader.ReadInt(); + for (int i = 0; i < length; i++) + { + reader.ReadString(); // IP Restrictions + } - if (version > 1) - { - _email = reader.ReadString(); - } + _totalGameTime = reader.ReadTimeSpan(); + + if (version > 1) + { + _email = reader.ReadString(); } } } diff --git a/Projects/UOContent/Accounting/Account.cs b/Projects/UOContent/Accounting/Account.cs index 26615c303..46c2b0071 100644 --- a/Projects/UOContent/Accounting/Account.cs +++ b/Projects/UOContent/Accounting/Account.cs @@ -10,1200 +10,1166 @@ using Server.Mobiles; using Server.Multis; using Server.Network; -namespace Server.Accounting +namespace Server.Accounting; + +[SerializationGenerator(6)] +public partial class Account : IAccount, IComparable { - [SerializationGenerator(5)] - public partial class Account : IAccount, IComparable + public static readonly TimeSpan YoungDuration = TimeSpan.FromHours(40.0); + public static readonly TimeSpan InactiveDuration = TimeSpan.FromDays(180.0); + public static readonly TimeSpan EmptyInactiveDuration = TimeSpan.FromDays(30.0); + + [InternString] + [SerializableField(0)] + private string _username; + + [SerializableField(1)] + private PasswordProtectionAlgorithm _passwordAlgorithm; + + [SerializableField(2)] + private string _password; + + [SerializableField(3)] + private AccessLevel _accessLevel; + + [SerializableField(4)] + private int _flags; + + [SerializableField(5)] + private DateTime _lastLogin; + + /// + /// This amount represents the current amount of Gold owned by the player. + /// The value does not include the value of Platinum and ranges from + /// 0 to 999,999,999 by default. + /// + [SerializableField(6, setter: "private")] + [SerializedCommandProperty(AccessLevel.Administrator)] + public int _totalGold; + + /// + /// This amount represents the current amount of Platinum owned by the player. + /// The value does not include the value of Gold and ranges from + /// 0 to 2,147,483,647 by default. + /// One Platinum represents the value of CurrencyThreshold in Gold. + /// + [SerializableField(7, setter: "private")] + [SerializedCommandProperty(AccessLevel.Administrator)] + public int _totalPlat; + + private Mobile[] _mobiles; + + [SerializableProperty(9)] + public List Comments { - public static readonly TimeSpan YoungDuration = TimeSpan.FromHours(40.0); - public static readonly TimeSpan InactiveDuration = TimeSpan.FromDays(180.0); - public static readonly TimeSpan EmptyInactiveDuration = TimeSpan.FromDays(30.0); - - [InternString] - [SerializableField(0)] - private string _username; - - [SerializableField(1)] - private PasswordProtectionAlgorithm _passwordAlgorithm; - - [SerializableField(2)] - private string _password; - - [SerializableField(3)] - private AccessLevel _accessLevel; - - [SerializableField(4)] - private int _flags; - - [SerializableField(5)] - private DateTime _lastLogin; - - /// - /// This amount represents the current amount of Gold owned by the player. - /// The value does not include the value of Platinum and ranges from - /// 0 to 999,999,999 by default. - /// - [SerializableField(6, setter: "private")] - [SerializedCommandProperty(AccessLevel.Administrator)] - public int _totalGold; - - /// - /// This amount represents the current amount of Platinum owned by the player. - /// The value does not include the value of Gold and ranges from - /// 0 to 2,147,483,647 by default. - /// One Platinum represents the value of CurrencyThreshold in Gold. - /// - [SerializableField(7, setter: "private")] - [SerializedCommandProperty(AccessLevel.Administrator)] - public int _totalPlat; - - private Mobile[] _mobiles; - - [SerializableProperty(9)] - public List Comments + get => _comments ??= []; + private set { - get => _comments ??= new List(); - private set - { - _comments = value; - this.MarkDirty(); - } - } - - [SerializableProperty(10)] - public List Tags - { - get => _tags ??= new List(); - private set - { - _tags = value; - this.MarkDirty(); - } - } - - [SerializableField(11)] - private IPAddress[] _loginIPs; - - /// - /// List of IP addresses for restricted access. '*' wildcard supported. If the array contains zero entries, all IP addresses - /// are allowed. - /// - [SerializableField(12)] - private string[] _ipRestrictions; - - /// - /// Gets the total game time of this account, also considering the game time of characters - /// that have been deleted. - /// - [SerializableProperty(13)] - public TimeSpan TotalGameTime - { - get - { - for (var i = 0; i < _mobiles.Length; i++) - { - if (_mobiles[i] is PlayerMobile m && m.NetState != null) - { - return _totalGameTime + (Core.Now - m.SessionStart); - } - } - - return _totalGameTime; - } - private set - { - _totalGameTime = value; - this.MarkDirty(); - } - } - - [SerializableField(14)] - [SerializedCommandProperty(AccessLevel.Administrator)] - private string _email; - - private Timer m_YoungTimer; - - public Account(string username, string password) : this(Accounts.NewAccount) - { - _username = username; - - SetPassword(password); - - _accessLevel = AccessLevel.Player; - - _lastLogin = Core.Now; - _totalGameTime = TimeSpan.Zero; - - _mobiles = new Mobile[7]; - - _ipRestrictions = Array.Empty(); - _loginIPs = Array.Empty(); - - Accounts.Add(this); + _comments = value; this.MarkDirty(); } + } - public Account(XmlElement node) + [SerializableProperty(10)] + public List Tags + { + get => _tags ??= []; + private set { - Serial = Accounts.NewAccount; - - _username = Utility.GetText(node["username"], "empty"); - - Enum.TryParse(Utility.GetText(node["passwordAlgorithm"], null), true, out _passwordAlgorithm); - - // Backward compatibility with RunUO/ServUO - if (_passwordAlgorithm == PasswordProtectionAlgorithm.None) - { - var upgraded = - UpgradePassword( - Utility.GetText(node["newSecureCryptPassword"], null), - PasswordProtectionAlgorithm.SHA2 - ) || - UpgradePassword(Utility.GetText(node["newCryptPassword"], null), PasswordProtectionAlgorithm.SHA1) || - UpgradePassword(Utility.GetText(node["cryptPassword"], null), PasswordProtectionAlgorithm.MD5); - - // Automatically upgrade plain passwords to current algorithm. - if (!upgraded) - { - SetPassword(Utility.GetText(node["password"], null)); - } - } - else - { - _password = Utility.GetText(node["password"], null); - } - - Enum.TryParse(Utility.GetText(node["accessLevel"], "Player"), true, out _accessLevel); - _flags = Utility.GetXMLInt32(Utility.GetText(node["flags"], "0"), 0); - Created = Utility.GetXMLDateTime(Utility.GetText(node["created"], null), Core.Now); - _lastLogin = Utility.GetXMLDateTime(Utility.GetText(node["lastLogin"], null), Core.Now); - - _totalGold = Utility.GetXMLInt32(Utility.GetText(node["totalGold"], "0"), 0); - _totalPlat = Utility.GetXMLInt32(Utility.GetText(node["totalPlat"], "0"), 0); - - _mobiles = LoadMobiles(node); - _comments = LoadComments(node); - _tags = LoadTags(node); - _loginIPs = LoadAddressList(node); - _ipRestrictions = LoadAccessCheck(node); - - for (var i = 0; i < _mobiles.Length; ++i) - { - if (_mobiles[i] != null) - { - _mobiles[i].Account = this; - } - } - - var totalGameTime = Utility.GetXMLTimeSpan(Utility.GetText(node["totalGameTime"], null), TimeSpan.Zero); - if (totalGameTime == TimeSpan.Zero) - { - for (var i = 0; i < _mobiles.Length; i++) - { - if (_mobiles[i] is PlayerMobile m) - { - totalGameTime += m.GameTime; - } - } - } - - _totalGameTime = totalGameTime; - - if (Young) - { - CheckYoung(); - } - - Accounts.Add(this); + _tags = value; this.MarkDirty(); } + } - /// - /// Object detailing information about the hardware of the last person to log into this account - /// - public HardwareInfo HardwareInfo { get; set; } + [SerializableField(11)] + private IPAddress[] _loginIPs; - /// - /// Gets or sets a flag indicating if this account is banned. - /// - public bool Banned + /// + /// Gets the total game time of this account, also considering the game time of characters + /// that have been deleted. + /// + [SerializableProperty(12)] + public TimeSpan TotalGameTime + { + get { - get + for (var i = 0; i < _mobiles.Length; i++) { - var isBanned = GetFlag(0); - - if (!isBanned) + if (_mobiles[i] is PlayerMobile m && m.NetState != null) { + return _totalGameTime + (Core.Now - m.SessionStart); + } + } + + return _totalGameTime; + } + private set + { + _totalGameTime = value; + this.MarkDirty(); + } + } + + [SerializableField(13)] + [SerializedCommandProperty(AccessLevel.Administrator)] + private string _email; + + private Timer m_YoungTimer; + + public Account(string username, string password) : this(Accounts.NewAccount) + { + _username = username; + + SetPassword(password); + + _accessLevel = AccessLevel.Player; + + _lastLogin = Core.Now; + _totalGameTime = TimeSpan.Zero; + + _mobiles = new Mobile[7]; + + _loginIPs = []; + + Accounts.Add(this); + this.MarkDirty(); + } + + public Account(XmlElement node) + { + Serial = Accounts.NewAccount; + + _username = Utility.GetText(node["username"], "empty"); + + Enum.TryParse(Utility.GetText(node["passwordAlgorithm"], null), true, out _passwordAlgorithm); + + // Backward compatibility with RunUO/ServUO + if (_passwordAlgorithm == PasswordProtectionAlgorithm.None) + { + var upgraded = + UpgradePassword( + Utility.GetText(node["newSecureCryptPassword"], null), + PasswordProtectionAlgorithm.SHA2 + ) || + UpgradePassword(Utility.GetText(node["newCryptPassword"], null), PasswordProtectionAlgorithm.SHA1) || + UpgradePassword(Utility.GetText(node["cryptPassword"], null), PasswordProtectionAlgorithm.MD5); + + // Automatically upgrade plain passwords to current algorithm. + if (!upgraded) + { + SetPassword(Utility.GetText(node["password"], null)); + } + } + else + { + _password = Utility.GetText(node["password"], null); + } + + Enum.TryParse(Utility.GetText(node["accessLevel"], "Player"), true, out _accessLevel); + _flags = Utility.GetXMLInt32(Utility.GetText(node["flags"], "0"), 0); + Created = Utility.GetXMLDateTime(Utility.GetText(node["created"], null), Core.Now); + _lastLogin = Utility.GetXMLDateTime(Utility.GetText(node["lastLogin"], null), Core.Now); + + _totalGold = Utility.GetXMLInt32(Utility.GetText(node["totalGold"], "0"), 0); + _totalPlat = Utility.GetXMLInt32(Utility.GetText(node["totalPlat"], "0"), 0); + + _mobiles = LoadMobiles(node); + _comments = LoadComments(node); + _tags = LoadTags(node); + _loginIPs = LoadAddressList(node); + + for (var i = 0; i < _mobiles.Length; ++i) + { + if (_mobiles[i] != null) + { + _mobiles[i].Account = this; + } + } + + var totalGameTime = Utility.GetXMLTimeSpan(Utility.GetText(node["totalGameTime"], null), TimeSpan.Zero); + if (totalGameTime == TimeSpan.Zero) + { + for (var i = 0; i < _mobiles.Length; i++) + { + if (_mobiles[i] is PlayerMobile m) + { + totalGameTime += m.GameTime; + } + } + } + + _totalGameTime = totalGameTime; + + if (Young) + { + CheckYoung(); + } + + Accounts.Add(this); + this.MarkDirty(); + } + + /// + /// Object detailing information about the hardware of the last person to log into this account + /// + public HardwareInfo HardwareInfo { get; set; } + + /// + /// Gets or sets a flag indicating if this account is banned. + /// + public bool Banned + { + get + { + var isBanned = GetFlag(0); + + if (!isBanned) + { + return false; + } + + if (GetBanTags(out var banTime, out var banDuration)) + { + if (banDuration != TimeSpan.MaxValue && Core.Now >= banTime + banDuration) + { + SetUnspecifiedBan(null); // clear + Banned = false; return false; } - - if (GetBanTags(out var banTime, out var banDuration)) - { - if (banDuration != TimeSpan.MaxValue && Core.Now >= banTime + banDuration) - { - SetUnspecifiedBan(null); // clear - Banned = false; - return false; - } - } - - return true; } - set => SetFlag(0, value); + + return true; + } + set => SetFlag(0, value); + } + + /// + /// Gets or sets a flag indicating if the characters created on this account will have the young status. + /// + public bool Young + { + get => !GetFlag(1); + set + { + SetFlag(1, !value); + + m_YoungTimer?.Stop(); + m_YoungTimer = null; + } + } + + /// + /// An account is considered inactive based upon LastLogin and InactiveDuration. If the account is empty, it is based upon + /// EmptyInactiveDuration + /// + public bool Inactive + { + get + { + if (AccessLevel != AccessLevel.Player) + { + return false; + } + + var inactiveLength = Core.Now - _lastLogin; + + return inactiveLength > (Count == 0 ? EmptyInactiveDuration : InactiveDuration); + } + } + + public TimeSpan AccountAge => Core.Now - Created; + + [CommandProperty(AccessLevel.GameMaster, readOnly: true)] + public DateTime Created { get; set; } = Core.Now; + + public Serial Serial { get; set; } + + [AfterDeserialization(false)] + private void AfterDeserialization() + { + if (_comments?.Count == 0) + { + _comments = null; } - /// - /// Gets or sets a flag indicating if the characters created on this account will have the young status. - /// - public bool Young + if (_tags?.Count == 0) { - get => !GetFlag(1); - set - { - SetFlag(1, !value); + _tags = null; + } - m_YoungTimer?.Stop(); - m_YoungTimer = null; + for (var i = 0; i < _mobiles.Length; ++i) + { + if (_mobiles[i] != null) + { + _mobiles[i].Account = this; } } - /// - /// An account is considered inactive based upon LastLogin and InactiveDuration. If the account is empty, it is based upon - /// EmptyInactiveDuration - /// - public bool Inactive + if (_totalGameTime == TimeSpan.Zero) { - get + for (var i = 0; i < _mobiles.Length; i++) { - if (AccessLevel != AccessLevel.Player) + if (_mobiles[i] is PlayerMobile m) { - return false; + _totalGameTime += m.GameTime; } - - var inactiveLength = Core.Now - _lastLogin; - - return inactiveLength > (Count == 0 ? EmptyInactiveDuration : InactiveDuration); } } - public TimeSpan AccountAge => Core.Now - Created; - - [CommandProperty(AccessLevel.GameMaster, readOnly: true)] - public DateTime Created { get; set; } = Core.Now; - - public Serial Serial { get; set; } - - [AfterDeserialization(false)] - private void AfterDeserialization() + if (Young) { - if (_comments?.Count == 0) + CheckYoung(); + } + } + + /// + /// Deletes the account, all characters of the account, and all houses of those characters + /// + public void Delete() + { + for (var i = 0; i < Length; ++i) + { + var m = this[i]; + + if (m == null) { - _comments = null; + continue; } - if (_tags?.Count == 0) + var list = BaseHouse.GetHouses(m); + + for (var j = 0; j < list.Count; ++j) { - _tags = null; + list[j].Delete(); } - for (var i = 0; i < _mobiles.Length; ++i) - { - if (_mobiles[i] != null) - { - _mobiles[i].Account = this; - } - } + m.Delete(); - if (_totalGameTime == TimeSpan.Zero) - { - for (var i = 0; i < _mobiles.Length; i++) - { - if (_mobiles[i] is PlayerMobile m) - { - _totalGameTime += m.GameTime; - } - } - } - - if (Young) - { - CheckYoung(); - } + m.Account = null; + _mobiles[i] = null; } - /// - /// Deletes the account, all characters of the account, and all houses of those characters - /// - public void Delete() + if (_loginIPs.Length != 0 && AccountHandler.IPTable.ContainsKey(_loginIPs[0])) { - for (var i = 0; i < Length; ++i) + --AccountHandler.IPTable[_loginIPs[0]]; + } + + Deleted = true; + Accounts.Remove(this); + } + + public bool Deleted { get; private set; } + + public void SetPassword(string plainPassword) + { + Password = AccountSecurity.CurrentPasswordProtection.EncryptPassword(plainPassword); + PasswordAlgorithm = AccountSecurity.CurrentAlgorithm; + } + + public bool CheckPassword(string plainPassword) + { + var phrase = _passwordAlgorithm == PasswordProtectionAlgorithm.SHA1 + ? $"{_username}{plainPassword}" + : plainPassword; + + var ok = AccountSecurity.GetPasswordProtection(_passwordAlgorithm).ValidatePassword(Password, phrase); + if (!ok) + { + return false; + } + + // Upgrade the password protection in case we change the algorithm + if (_passwordAlgorithm != AccountSecurity.CurrentAlgorithm) + { + SetPassword(plainPassword); + } + + return true; + } + + /// + /// Gets the current number of characters on this account. + /// + public int Count + { + get + { + var count = 0; + + for (var i = 0; i < Length; i++) { - var m = this[i]; - - if (m == null) + if (this[i] != null) { - continue; + count++; + } + } + + return count; + } + } + + /// + /// Gets the maximum amount of characters allowed to be created on this account. Values other than 1, 5, 6, or 7 are not + /// supported by the client. + /// + public int Limit => Core.SA ? 7 : Core.AOS ? 6 : 5; + + /// + /// Gets the maximum amount of characters that this account can hold. + /// + public int Length => _mobiles.Length; + + /// + /// Gets or sets the character at a specified index for this account. Out of bound index values are handled; null returned + /// for get, ignored for set. + /// + public Mobile this[int index] + { + get + { + if (index >= 0 && index < _mobiles.Length) + { + var m = _mobiles[index]; + + if (m?.Deleted != true) + { + return m; } - var list = BaseHouse.GetHouses(m); - - for (var j = 0; j < list.Count; ++j) - { - list[j].Delete(); - } - - m.Delete(); - + // This is the only place that clears a mobile for garbage collection + // outside of an entire account deletion. m.Account = null; - _mobiles[i] = null; - } - - if (_loginIPs.Length != 0 && AccountHandler.IPTable.ContainsKey(_loginIPs[0])) - { - --AccountHandler.IPTable[_loginIPs[0]]; - } - - Deleted = true; - Accounts.Remove(this); - } - - public bool Deleted { get; private set; } - - public void SetPassword(string plainPassword) - { - Password = AccountSecurity.CurrentPasswordProtection.EncryptPassword(plainPassword); - PasswordAlgorithm = AccountSecurity.CurrentAlgorithm; - } - - public bool CheckPassword(string plainPassword) - { - var phrase = _passwordAlgorithm == PasswordProtectionAlgorithm.SHA1 - ? $"{_username}{plainPassword}" - : plainPassword; - - var ok = AccountSecurity.GetPasswordProtection(_passwordAlgorithm).ValidatePassword(Password, phrase); - if (!ok) - { - return false; - } - - // Upgrade the password protection in case we change the algorithm - if (_passwordAlgorithm != AccountSecurity.CurrentAlgorithm) - { - SetPassword(plainPassword); - } - - return true; - } - - /// - /// Gets the current number of characters on this account. - /// - public int Count - { - get - { - var count = 0; - - for (var i = 0; i < Length; i++) - { - if (this[i] != null) - { - count++; - } - } - - return count; - } - } - - /// - /// Gets the maximum amount of characters allowed to be created on this account. Values other than 1, 5, 6, or 7 are not - /// supported by the client. - /// - public int Limit => Core.SA ? 7 : Core.AOS ? 6 : 5; - - /// - /// Gets the maximum amount of characters that this account can hold. - /// - public int Length => _mobiles.Length; - - /// - /// Gets or sets the character at a specified index for this account. Out of bound index values are handled; null returned - /// for get, ignored for set. - /// - public Mobile this[int index] - { - get - { - if (index >= 0 && index < _mobiles.Length) - { - var m = _mobiles[index]; - - if (m?.Deleted != true) - { - return m; - } - - // This is the only place that clears a mobile for garbage collection - // outside of an entire account deletion. - m.Account = null; - _mobiles[index] = null; - this.MarkDirty(); - } - - return null; - } - set - { - if (index >= 0 && index < _mobiles.Length) - { - if (_mobiles[index] != null) - { - _mobiles[index].Account = null; - } - - _mobiles[index] = value; - this.MarkDirty(); - - if (_mobiles[index] != null) - { - _mobiles[index].Account = this; - } - } - } - } - - public int CompareTo(IAccount other) => string.CompareOrdinal(Username, other?.Username); - - /// - /// Attempts to deposit the given amount of Gold into this account. - /// If the given amount is greater than the CurrencyThreshold, - /// Platinum will be deposited to offset the difference. - /// - /// Amount to deposit. - /// True if successful, false if amount given is less than or equal to zero. - public bool DepositGold(int amount) - { - if (amount <= 0) - { - return false; - } - - var plat = Math.DivRem(amount, AccountGold.CurrencyThreshold, out var gold); - TotalPlat += plat; - TotalGold += gold; - - return true; - } - - /// - /// Attempts to deposit the given amount of Platinum into this account. - /// - /// Amount to deposit. - /// True if successful, false if amount given is less than or equal to zero. - public bool DepositPlat(int amount) - { - if (amount <= 0) - { - return false; - } - - TotalPlat += amount; - return true; - } - - /// - /// Attempts to withdraw the given amount of Gold from this account. - /// If the given amount is greater than the CurrencyThreshold, - /// Platinum will be withdrawn to offset the difference. - /// - /// Amount to withdraw. - /// True if successful, false if balance was too low. - public bool WithdrawGold(int amount) - { - if (amount <= 0) - { - return true; - } - - if (amount > _totalGold) - { - return false; - } - - TotalGold -= amount; - - return true; - } - - /// - /// Attempts to withdraw the given amount of Platinum from this account. - /// - /// Amount to withdraw. - /// True if successful, false if balance was too low. - public bool WithdrawPlat(int amount) - { - if (amount <= 0) - { - return true; - } - - if (amount > _totalPlat) - { - return false; - } - - TotalPlat -= amount; - - return true; - } - - /// - /// Returns total gold inclusive of platinum. - /// This is strictly for backwards compatibility - /// - /// Total gold, capped at Int32.MaxValue - public long GetTotalGold() => _totalGold + _totalPlat * AccountGold.CurrencyThreshold; - - public int CompareTo(Account other) => string.CompareOrdinal(_username, other?._username); - - /// - /// Gets the value of a specific flag in the Flags bitfield. - /// - /// The zero-based flag index. - public bool GetFlag(int index) => (_flags & (1 << index)) != 0; - - /// - /// Sets the value of a specific flag in the Flags bitfield. - /// - /// The zero-based flag index. - /// The value to set. - public void SetFlag(int index, bool value) - { - if (value) - { - Flags |= 1 << index; - } - else - { - Flags &= ~(1 << index); - } - } - - /// - /// Adds a new tag to this account. This method does not check for duplicate names. - /// - /// New tag name. - /// New tag value. - public void AddTag(string name, string value) - { - Tags.Add(new AccountTag(name, value)); - this.MarkDirty(); - } - - /// - /// Removes all tags with the specified name from this account. - /// - /// Tag name to remove. - public void RemoveTag(string name) - { - if (_tags == null) - { - return; - } - - for (var i = _tags.Count - 1; i >= 0; --i) - { - if (i >= _tags.Count) - { - continue; - } - - var tag = _tags[i]; - - if (tag.Name == name) - { - _tags.RemoveAt(i); - this.MarkDirty(); - } - } - } - - /// - /// Modifies an existing tag or adds a new tag if no tag exists. - /// - /// Tag name. - /// Tag value. - public void SetTag(string name, string value) - { - for (var i = 0; i < Tags.Count; ++i) - { - var tag = _tags[i]; - - if (tag.Name == name) - { - tag.Value = value; - this.MarkDirty(); - return; - } - } - - AddTag(name, value); - } - - /// - /// Gets the value of a tag -or- null if there are no tags with the specified name. - /// - /// Name of the desired tag value. - public string GetTag(string name) - { - for (var i = 0; i < Tags.Count; ++i) - { - var tag = _tags[i]; - - if (tag.Name == name) - { - return tag.Value; - } + _mobiles[index] = null; + this.MarkDirty(); } return null; } - - public void SetUnspecifiedBan(Mobile from) + set { - SetBanTags(from, DateTime.MinValue, TimeSpan.Zero); - } - - public void SetBanTags(Mobile from, DateTime banTime, TimeSpan banDuration) - { - if (from == null) + if (index >= 0 && index < _mobiles.Length) { - RemoveTag("BanDealer"); - } - else - { - SetTag("BanDealer", from.ToString()); - } - - if (banTime == DateTime.MinValue) - { - RemoveTag("BanTime"); - } - else - { - SetTag("BanTime", XmlConvert.ToString(banTime, XmlDateTimeSerializationMode.Utc)); - } - - if (banDuration == TimeSpan.Zero) - { - RemoveTag("BanDuration"); - } - else - { - SetTag("BanDuration", banDuration.ToString()); - } - } - - public bool GetBanTags(out DateTime banTime, out TimeSpan banDuration) - { - var tagDuration = GetTag("BanDuration"); - - banTime = Utility.GetXMLDateTime(GetTag("BanTime"), DateTime.MinValue); - - if (tagDuration == "Infinite") - { - banDuration = TimeSpan.MaxValue; - } - else if (tagDuration != null) - { - banDuration = Utility.ToTimeSpan(tagDuration); - } - else - { - banDuration = TimeSpan.Zero; - } - - return banTime != DateTime.MinValue && banDuration != TimeSpan.Zero; - } - - public static void Initialize() - { - EventSink.Connected += EventSink_Connected; - EventSink.Disconnected += EventSink_Disconnected; - } - - private static void EventSink_Connected(Mobile m) - { - if (m.Account is not Account acc) - { - return; - } - - if (acc.Young && acc.m_YoungTimer == null) - { - acc.m_YoungTimer = new YoungTimer(acc); - acc.m_YoungTimer.Start(); - } - } - - private static void EventSink_Disconnected(Mobile m) - { - if (m.Account is not Account acc) - { - return; - } - - if (acc.m_YoungTimer != null) - { - acc.m_YoungTimer.Stop(); - acc.m_YoungTimer = null; - } - - if (m is not PlayerMobile pm) - { - return; - } - - acc.TotalGameTime += Core.Now - pm.SessionStart; - } - - public static void OnLogin(Mobile m) - { - if (m is not PlayerMobile pm) - { - return; - } - - if (m.Account is not Account acc) - { - return; - } - - if (pm.Young && acc.Young) - { - var ts = YoungDuration - acc.TotalGameTime; - var hours = Math.Max((int)ts.TotalHours, 0); - - if (hours == 1) + if (_mobiles[index] != null) { - m.SendAsciiMessage($"You will enjoy the benefits and relatively safe status of a young player for {hours} more hour."); - } - else - { - m.SendAsciiMessage($"You will enjoy the benefits and relatively safe status of a young player for {hours} more hours."); - } - } - } - - public void RemoveYoungStatus(int message) - { - Young = false; - - for (var i = 0; i < _mobiles.Length; i++) - { - if (_mobiles[i] is PlayerMobile { Young: true } m) - { - m.Young = false; - - if (m.NetState != null) - { - if (message > 0) - { - m.SendLocalizedMessage(message); - } - - // You are no longer considered a young player of Ultima Online, - // and are no longer subject to the limitations and benefits of being in that caste. - m.SendLocalizedMessage(1019039); - } - } - } - } - - public void CheckYoung() - { - if (TotalGameTime >= YoungDuration) - { - // You are old enough to be considered an adult, and have outgrown your status as a young player! - RemoveYoungStatus(1019038); - } - } - - private bool UpgradePassword(string password, PasswordProtectionAlgorithm algorithm) - { - if (password == null || algorithm < _passwordAlgorithm) - { - return false; - } - - PasswordAlgorithm = algorithm; - Password = password.ReplaceOrdinal("-", string.Empty); - return true; - } - - /// - /// Deserializes a list of string values from an xml element. Null values are not added to the list. - /// - /// The XmlElement from which to deserialize. - /// String list. Value will never be null. - private static string[] LoadAccessCheck(XmlElement node) - { - string[] stringList; - var accessCheck = node["accessCheck"]; - - if (accessCheck != null) - { - var list = new List(); - - foreach (XmlElement ip in accessCheck.GetElementsByTagName("ip")) - { - var text = Utility.GetText(ip, null); - - if (text != null) - { - list.Add(text); - } + _mobiles[index].Account = null; } - stringList = list.ToArray(); - } - else - { - stringList = Array.Empty(); - } + _mobiles[index] = value; + this.MarkDirty(); - return stringList; - } - - /// - /// Deserializes a list of IPAddress values from an xml element. - /// - /// The XmlElement from which to deserialize. - /// Address list. Value will never be null. - private static IPAddress[] LoadAddressList(XmlElement node) - { - IPAddress[] list; - var addressList = node["addressList"]; - - if (addressList != null) - { - var count = Utility.GetXMLInt32(Utility.GetAttribute(addressList, "count", "0"), 0); - - list = new IPAddress[count]; - - count = 0; - - foreach (XmlElement ip in addressList.GetElementsByTagName("ip")) + if (_mobiles[index] != null) { - if (count < list.Length) - { - if (IPAddress.TryParse(Utility.GetText(ip, null), out var address)) - { - list[count] = Utility.Intern(address); - count++; - } - } + _mobiles[index].Account = this; } - - if (count != list.Length) - { - var old = list; - list = new IPAddress[count]; - - for (var i = 0; i < count && i < old.Length; ++i) - { - list[i] = old[i]; - } - } - } - else - { - list = Array.Empty(); - } - - return list; - } - - /// - /// Deserializes a list of Mobile instances from an xml element. - /// - /// The XmlElement instance from which to deserialize. - /// Mobile list. Value will never be null. - private static Mobile[] LoadMobiles(XmlElement node) - { - var list = new Mobile[7]; - var chars = node["chars"]; - - // int length = Accounts.GetInt32( Accounts.GetAttribute( chars, "length", "6" ), 6 ); - // list = new Mobile[length]; - // Above is legacy, no longer used - - if (chars != null) - { - foreach (XmlElement ele in chars.GetElementsByTagName("char")) - { - try - { - var index = Utility.GetXMLInt32(Utility.GetAttribute(ele, "index", "0"), 0); - var serial = (Serial)Utility.GetXMLUInt32(Utility.GetText(ele, "0"), 0); - - if (index >= 0 && index < list.Length) - { - list[index] = World.FindMobile(serial); - } - } - catch - { - // ignored - } - } - } - - return list; - } - - /// - /// Deserializes a list of AccountComment instances from an xml element. - /// - /// The XmlElement from which to deserialize. - /// Comment list. Value will never be null. - private static List LoadComments(XmlElement node) - { - List list = null; - var comments = node["comments"]; - - if (comments != null) - { - list = new List(); - - foreach (XmlElement comment in comments.GetElementsByTagName("comment")) - { - try - { - list.Add(new AccountComment(comment)); - } - catch - { - // ignored - } - } - } - - return list; - } - - /// - /// Deserializes a list of AccountTag instances from an xml element. - /// - /// The XmlElement from which to deserialize. - /// Tag list. Value will never be null. - private static List LoadTags(XmlElement node) - { - List list = null; - var tags = node["tags"]; - - if (tags != null) - { - list = new List(); - - foreach (XmlElement tag in tags.GetElementsByTagName("tag")) - { - try - { - list.Add(new AccountTag(tag)); - } - catch - { - // ignored - } - } - } - - return list; - } - - /// - /// Checks if a specific NetState is allowed access to this account. - /// - /// NetState instance to check. - /// True if allowed, false if not. - public bool HasAccess(NetState ns) => ns != null && HasAccess(ns.Address); - - public bool HasAccess(IPAddress ipAddress) - { - var level = AccountHandler.LockdownLevel; - - if (level > AccessLevel.Player) - { - var hasAccess = false; - - if (_accessLevel >= level) - { - hasAccess = true; - } - else - { - for (var i = 0; !hasAccess && i < Length; ++i) - { - var m = this[i]; - - if (m?.AccessLevel >= level) - { - hasAccess = true; - } - } - } - - if (!hasAccess) - { - return false; - } - } - - var accessAllowed = _ipRestrictions.Length == 0 || IPLimiter.IsExempt(ipAddress); - - for (var i = 0; !accessAllowed && i < _ipRestrictions.Length; ++i) - { - accessAllowed = IPAddress.Parse(_ipRestrictions[i]).Equals(ipAddress); - } - - return accessAllowed; - } - - /// - /// Records the IP address of 'ns' in its 'LoginIPs' list. - /// - /// NetState instance to record. - public void LogAccess(NetState ns) - { - if (ns != null) - { - LogAccess(ns.Address); - } - } - - public void LogAccess(IPAddress ipAddress) - { - if (IPLimiter.IsExempt(ipAddress)) - { - return; - } - - if (_loginIPs.Length == 0) - { - AccountHandler.IPTable.TryGetValue(ipAddress, out var result); - AccountHandler.IPTable[ipAddress] = result + 1; - } - - var contains = false; - - for (var i = 0; !contains && i < _loginIPs.Length; ++i) - { - contains = _loginIPs[i].Equals(ipAddress); - } - - if (contains) - { - return; - } - - var old = _loginIPs; - LoginIPs = new IPAddress[old.Length + 1]; - - for (var i = 0; i < old.Length; ++i) - { - LoginIPs[i] = old[i]; - } - - LoginIPs[old.Length] = ipAddress; - } - - /// - /// Checks if a specific NetState is allowed access to this account. If true, the NetState IPAddress is added to the address - /// list. - /// - /// NetState instance to check. - /// True if allowed, false if not. - public bool CheckAccess(NetState ns) => ns != null && CheckAccess(ns.Address); - - public bool CheckAccess(IPAddress ipAddress) - { - var hasAccess = HasAccess(ipAddress); - - if (hasAccess) - { - LogAccess(ipAddress); - } - - return hasAccess; - } - - public override string ToString() => _username; - - private class YoungTimer : Timer - { - private readonly Account m_Account; - - public YoungTimer(Account account) - : base(TimeSpan.FromMinutes(1.0), TimeSpan.FromMinutes(1.0)) - { - m_Account = account; - } - - protected override void OnTick() - { - m_Account.CheckYoung(); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Enumerator GetEnumerator() => new(_mobiles); - - [SerializableProperty(8, useField: nameof(_mobiles))] - public Enumerator Mobiles - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => GetEnumerator(); - } - - public ref struct Enumerator - { - private readonly Mobile[] _mobiles; - private int _index; - private Mobile _current; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal Enumerator(Mobile[] mobs) - { - _mobiles = mobs; - _index = 0; - _current = default; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() - { - Mobile[] localList = _mobiles; - - while ((uint)_index < (uint)localList.Length) - { - _current = localList[_index++]; - if (_current?.Deleted == false) - { - return true; - } - } - - return false; - } - - public Mobile Current - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _current; } } } + + public int CompareTo(IAccount other) => string.CompareOrdinal(Username, other?.Username); + + /// + /// Attempts to deposit the given amount of Gold into this account. + /// If the given amount is greater than the CurrencyThreshold, + /// Platinum will be deposited to offset the difference. + /// + /// Amount to deposit. + /// True if successful, false if amount given is less than or equal to zero. + public bool DepositGold(int amount) + { + if (amount <= 0) + { + return false; + } + + var plat = Math.DivRem(amount, AccountGold.CurrencyThreshold, out var gold); + TotalPlat += plat; + TotalGold += gold; + + return true; + } + + /// + /// Attempts to deposit the given amount of Platinum into this account. + /// + /// Amount to deposit. + /// True if successful, false if amount given is less than or equal to zero. + public bool DepositPlat(int amount) + { + if (amount <= 0) + { + return false; + } + + TotalPlat += amount; + return true; + } + + /// + /// Attempts to withdraw the given amount of Gold from this account. + /// If the given amount is greater than the CurrencyThreshold, + /// Platinum will be withdrawn to offset the difference. + /// + /// Amount to withdraw. + /// True if successful, false if balance was too low. + public bool WithdrawGold(int amount) + { + if (amount <= 0) + { + return true; + } + + if (amount > _totalGold) + { + return false; + } + + TotalGold -= amount; + + return true; + } + + /// + /// Attempts to withdraw the given amount of Platinum from this account. + /// + /// Amount to withdraw. + /// True if successful, false if balance was too low. + public bool WithdrawPlat(int amount) + { + if (amount <= 0) + { + return true; + } + + if (amount > _totalPlat) + { + return false; + } + + TotalPlat -= amount; + + return true; + } + + /// + /// Returns total gold inclusive of platinum. + /// This is strictly for backwards compatibility + /// + /// Total gold, capped at Int32.MaxValue + public long GetTotalGold() => _totalGold + _totalPlat * AccountGold.CurrencyThreshold; + + public int CompareTo(Account other) => string.CompareOrdinal(_username, other?._username); + + /// + /// Gets the value of a specific flag in the Flags bitfield. + /// + /// The zero-based flag index. + public bool GetFlag(int index) => (_flags & (1 << index)) != 0; + + /// + /// Sets the value of a specific flag in the Flags bitfield. + /// + /// The zero-based flag index. + /// The value to set. + public void SetFlag(int index, bool value) + { + if (value) + { + Flags |= 1 << index; + } + else + { + Flags &= ~(1 << index); + } + } + + /// + /// Adds a new tag to this account. This method does not check for duplicate names. + /// + /// New tag name. + /// New tag value. + public void AddTag(string name, string value) + { + Tags.Add(new AccountTag(name, value)); + this.MarkDirty(); + } + + /// + /// Removes all tags with the specified name from this account. + /// + /// Tag name to remove. + public void RemoveTag(string name) + { + if (_tags == null) + { + return; + } + + for (var i = _tags.Count - 1; i >= 0; --i) + { + if (i >= _tags.Count) + { + continue; + } + + var tag = _tags[i]; + + if (tag.Name == name) + { + _tags.RemoveAt(i); + this.MarkDirty(); + } + } + } + + /// + /// Modifies an existing tag or adds a new tag if no tag exists. + /// + /// Tag name. + /// Tag value. + public void SetTag(string name, string value) + { + for (var i = 0; i < Tags.Count; ++i) + { + var tag = _tags[i]; + + if (tag.Name == name) + { + tag.Value = value; + this.MarkDirty(); + return; + } + } + + AddTag(name, value); + } + + /// + /// Gets the value of a tag -or- null if there are no tags with the specified name. + /// + /// Name of the desired tag value. + public string GetTag(string name) + { + for (var i = 0; i < Tags.Count; ++i) + { + var tag = _tags[i]; + + if (tag.Name == name) + { + return tag.Value; + } + } + + return null; + } + + public void SetUnspecifiedBan(Mobile from) + { + SetBanTags(from, DateTime.MinValue, TimeSpan.Zero); + } + + public void SetBanTags(Mobile from, DateTime banTime, TimeSpan banDuration) + { + if (from == null) + { + RemoveTag("BanDealer"); + } + else + { + SetTag("BanDealer", from.ToString()); + } + + if (banTime == DateTime.MinValue) + { + RemoveTag("BanTime"); + } + else + { + SetTag("BanTime", XmlConvert.ToString(banTime, XmlDateTimeSerializationMode.Utc)); + } + + if (banDuration == TimeSpan.Zero) + { + RemoveTag("BanDuration"); + } + else + { + SetTag("BanDuration", banDuration.ToString()); + } + } + + public bool GetBanTags(out DateTime banTime, out TimeSpan banDuration) + { + var tagDuration = GetTag("BanDuration"); + + banTime = Utility.GetXMLDateTime(GetTag("BanTime"), DateTime.MinValue); + + if (tagDuration == "Infinite") + { + banDuration = TimeSpan.MaxValue; + } + else if (tagDuration != null) + { + banDuration = Utility.ToTimeSpan(tagDuration); + } + else + { + banDuration = TimeSpan.Zero; + } + + return banTime != DateTime.MinValue && banDuration != TimeSpan.Zero; + } + + public static void Initialize() + { + EventSink.Connected += EventSink_Connected; + EventSink.Disconnected += EventSink_Disconnected; + } + + private static void EventSink_Connected(Mobile m) + { + if (m.Account is not Account acc) + { + return; + } + + if (acc.Young && acc.m_YoungTimer == null) + { + acc.m_YoungTimer = new YoungTimer(acc); + acc.m_YoungTimer.Start(); + } + } + + private static void EventSink_Disconnected(Mobile m) + { + if (m.Account is not Account acc) + { + return; + } + + if (acc.m_YoungTimer != null) + { + acc.m_YoungTimer.Stop(); + acc.m_YoungTimer = null; + } + + if (m is not PlayerMobile pm) + { + return; + } + + acc.TotalGameTime += Core.Now - pm.SessionStart; + } + + public static void OnLogin(Mobile m) + { + if (m is not PlayerMobile pm) + { + return; + } + + if (m.Account is not Account acc) + { + return; + } + + if (pm.Young && acc.Young) + { + var ts = YoungDuration - acc.TotalGameTime; + var hours = Math.Max((int)ts.TotalHours, 0); + + if (hours == 1) + { + m.SendAsciiMessage($"You will enjoy the benefits and relatively safe status of a young player for {hours} more hour."); + } + else + { + m.SendAsciiMessage($"You will enjoy the benefits and relatively safe status of a young player for {hours} more hours."); + } + } + } + + public void RemoveYoungStatus(int message) + { + Young = false; + + for (var i = 0; i < _mobiles.Length; i++) + { + if (_mobiles[i] is PlayerMobile { Young: true } m) + { + m.Young = false; + + if (m.NetState != null) + { + if (message > 0) + { + m.SendLocalizedMessage(message); + } + + // You are no longer considered a young player of Ultima Online, + // and are no longer subject to the limitations and benefits of being in that caste. + m.SendLocalizedMessage(1019039); + } + } + } + } + + public void CheckYoung() + { + if (TotalGameTime >= YoungDuration) + { + // You are old enough to be considered an adult, and have outgrown your status as a young player! + RemoveYoungStatus(1019038); + } + } + + private bool UpgradePassword(string password, PasswordProtectionAlgorithm algorithm) + { + if (password == null || algorithm < _passwordAlgorithm) + { + return false; + } + + PasswordAlgorithm = algorithm; + Password = password.ReplaceOrdinal("-", string.Empty); + return true; + } + + /// + /// Deserializes a list of string values from an xml element. Null values are not added to the list. + /// + /// The XmlElement from which to deserialize. + /// String list. Value will never be null. + private static string[] LoadAccessCheck(XmlElement node) + { + string[] stringList; + var accessCheck = node["accessCheck"]; + + if (accessCheck != null) + { + var list = new List(); + + foreach (XmlElement ip in accessCheck.GetElementsByTagName("ip")) + { + var text = Utility.GetText(ip, null); + + if (text != null) + { + list.Add(text); + } + } + + stringList = list.ToArray(); + } + else + { + stringList = []; + } + + return stringList; + } + + /// + /// Deserializes a list of IPAddress values from an xml element. + /// + /// The XmlElement from which to deserialize. + /// Address list. Value will never be null. + private static IPAddress[] LoadAddressList(XmlElement node) + { + IPAddress[] list; + var addressList = node["addressList"]; + + if (addressList != null) + { + var count = Utility.GetXMLInt32(Utility.GetAttribute(addressList, "count", "0"), 0); + + list = new IPAddress[count]; + + count = 0; + + foreach (XmlElement ip in addressList.GetElementsByTagName("ip")) + { + if (count < list.Length) + { + if (IPAddress.TryParse(Utility.GetText(ip, null), out var address)) + { + list[count] = Utility.Intern(address); + count++; + } + } + } + + if (count != list.Length) + { + var old = list; + list = new IPAddress[count]; + + for (var i = 0; i < count && i < old.Length; ++i) + { + list[i] = old[i]; + } + } + } + else + { + list = []; + } + + return list; + } + + /// + /// Deserializes a list of Mobile instances from an xml element. + /// + /// The XmlElement instance from which to deserialize. + /// Mobile list. Value will never be null. + private static Mobile[] LoadMobiles(XmlElement node) + { + var list = new Mobile[7]; + var chars = node["chars"]; + + // int length = Accounts.GetInt32( Accounts.GetAttribute( chars, "length", "6" ), 6 ); + // list = new Mobile[length]; + // Above is legacy, no longer used + + if (chars != null) + { + foreach (XmlElement ele in chars.GetElementsByTagName("char")) + { + try + { + var index = Utility.GetXMLInt32(Utility.GetAttribute(ele, "index", "0"), 0); + var serial = (Serial)Utility.GetXMLUInt32(Utility.GetText(ele, "0"), 0); + + if (index >= 0 && index < list.Length) + { + list[index] = World.FindMobile(serial); + } + } + catch + { + // ignored + } + } + } + + return list; + } + + /// + /// Deserializes a list of AccountComment instances from an xml element. + /// + /// The XmlElement from which to deserialize. + /// Comment list. Value will never be null. + private static List LoadComments(XmlElement node) + { + List list = null; + var comments = node["comments"]; + + if (comments != null) + { + list = []; + + foreach (XmlElement comment in comments.GetElementsByTagName("comment")) + { + try + { + list.Add(new AccountComment(comment)); + } + catch + { + // ignored + } + } + } + + return list; + } + + /// + /// Deserializes a list of AccountTag instances from an xml element. + /// + /// The XmlElement from which to deserialize. + /// Tag list. Value will never be null. + private static List LoadTags(XmlElement node) + { + List list = null; + var tags = node["tags"]; + + if (tags != null) + { + list = []; + + foreach (XmlElement tag in tags.GetElementsByTagName("tag")) + { + try + { + list.Add(new AccountTag(tag)); + } + catch + { + // ignored + } + } + } + + return list; + } + + /// + /// Checks if a specific NetState is allowed access to this account. + /// + /// NetState instance to check. + /// True if allowed, false if not. + public bool HasAccess(NetState ns) => ns != null && HasAccess(ns.Address); + + public bool HasAccess(IPAddress ipAddress) + { + var level = AccountHandler.LockdownLevel; + + if (level <= AccessLevel.Player || _accessLevel >= level) + { + return true; + } + + for (var i = 0; i < Length; ++i) + { + var m = this[i]; + + if (m?.AccessLevel >= level) + { + return true; + } + } + + return false; + } + + /// + /// Records the IP address of 'ns' in its 'LoginIPs' list. + /// + /// NetState instance to record. + public void LogAccess(NetState ns) + { + if (ns != null) + { + LogAccess(ns.Address); + } + } + + public void LogAccess(IPAddress ipAddress) + { + if (_loginIPs.Length == 0) + { + AccountHandler.IPTable.TryGetValue(ipAddress, out var result); + AccountHandler.IPTable[ipAddress] = result + 1; + } + + var contains = false; + + for (var i = 0; !contains && i < _loginIPs.Length; ++i) + { + contains = _loginIPs[i].Equals(ipAddress); + } + + if (contains) + { + return; + } + + var old = _loginIPs; + LoginIPs = new IPAddress[old.Length + 1]; + + for (var i = 0; i < old.Length; ++i) + { + LoginIPs[i] = old[i]; + } + + LoginIPs[old.Length] = ipAddress; + } + + /// + /// Checks if a specific NetState is allowed access to this account. If true, the NetState IPAddress is added to the address + /// list. + /// + /// NetState instance to check. + /// True if allowed, false if not. + public bool CheckAccess(NetState ns) => ns != null && CheckAccess(ns.Address); + + public bool CheckAccess(IPAddress ipAddress) + { + var hasAccess = HasAccess(ipAddress); + + if (hasAccess) + { + LogAccess(ipAddress); + } + + return hasAccess; + } + + public override string ToString() => _username; + + private class YoungTimer : Timer + { + private readonly Account m_Account; + + public YoungTimer(Account account) + : base(TimeSpan.FromMinutes(1.0), TimeSpan.FromMinutes(1.0)) + { + m_Account = account; + } + + protected override void OnTick() + { + m_Account.CheckYoung(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new(_mobiles); + + [SerializableProperty(8, useField: nameof(_mobiles))] + public Enumerator Mobiles + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetEnumerator(); + } + + public ref struct Enumerator + { + private readonly Mobile[] _mobiles; + private int _index; + private Mobile _current; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Enumerator(Mobile[] mobs) + { + _mobiles = mobs; + _index = 0; + _current = default; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + Mobile[] localList = _mobiles; + + while ((uint)_index < (uint)localList.Length) + { + _current = localList[_index++]; + if (_current?.Deleted == false) + { + return true; + } + } + + return false; + } + + public Mobile Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _current; + } + } } diff --git a/Projects/UOContent/Accounting/AccountAttackLimiter.cs b/Projects/UOContent/Accounting/AccountAttackLimiter.cs deleted file mode 100644 index 53143327b..000000000 --- a/Projects/UOContent/Accounting/AccountAttackLimiter.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using Server.Network; - -namespace Server.Accounting -{ - public static class AccountAttackLimiter - { - public static bool Enabled; - - private static readonly List m_List = new(); - - public static void Configure() - { - Enabled = ServerConfiguration.GetOrUpdateSetting("accountAttackLimiter.enable", true); - } - - public static unsafe void Initialize() - { - if (!Enabled) - { - return; - } - - IncomingPackets.RegisterThrottler(0x80, &Throttle); - IncomingPackets.RegisterThrottler(0x91, &Throttle); - IncomingPackets.RegisterThrottler(0xCF, &Throttle); - } - - public static bool Throttle(int packetId, NetState ns, out bool drop) - { - var accessLog = FindAccessLog(ns); - - if (accessLog == null) - { - drop = false; - return true; - } - - var date = Core.Now; - var access = accessLog.LastAccessTime + ComputeThrottle(accessLog.Counts); - var allow = date >= access; - drop = !allow; - return allow; - } - - public static InvalidAccountAccessLog FindAccessLog(NetState ns) - { - if (ns == null) - { - return null; - } - - var ipAddress = ns.Address; - - for (var i = 0; i < m_List.Count; ++i) - { - var accessLog = m_List[i]; - - if (accessLog.HasExpired) - { - m_List.RemoveAt(i--); - } - else if (accessLog.Address.Equals(ipAddress)) - { - return accessLog; - } - } - - return null; - } - - public static void RegisterInvalidAccess(NetState ns) - { - if (ns == null || !Enabled) - { - return; - } - - var accessLog = FindAccessLog(ns); - - if (accessLog == null) - { - m_List.Add(accessLog = new InvalidAccountAccessLog(ns.Address)); - } - - accessLog.Counts += 1; - accessLog.RefreshAccessTime(); - - if (accessLog.Counts >= 3) - { - try - { - using var op = new StreamWriter("throttle.log", true); - op.WriteLine( - "{0}\t{1}\t{2}", - Core.Now, - ns, - accessLog.Counts - ); - } - catch - { - // ignored - } - } - } - - public static TimeSpan ComputeThrottle(int counts) - { - if (counts >= 15) - { - return TimeSpan.FromMinutes(5.0); - } - - if (counts >= 10) - { - return TimeSpan.FromMinutes(1.0); - } - - if (counts >= 5) - { - return TimeSpan.FromSeconds(20.0); - } - - if (counts >= 3) - { - return TimeSpan.FromSeconds(10.0); - } - - if (counts >= 1) - { - return TimeSpan.FromSeconds(2.0); - } - - return TimeSpan.Zero; - } - } - - public class InvalidAccountAccessLog - { - public InvalidAccountAccessLog(IPAddress address) - { - Address = address; - RefreshAccessTime(); - } - - public IPAddress Address { get; set; } - - public DateTime LastAccessTime { get; set; } - - public bool HasExpired => Core.Now >= LastAccessTime + TimeSpan.FromHours(1.0); - - public int Counts { get; set; } - - public void RefreshAccessTime() - { - LastAccessTime = Core.Now; - } - } -} diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index 1dc33cf6b..a4669fafa 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -369,11 +369,6 @@ public static class AccountHandler acct.LogAccess(e.State); } - - if (!e.Accepted) - { - AccountAttackLimiter.RegisterInvalidAccess(e.State); - } } public static void EventSink_GameLogin(GameLoginEventArgs e) @@ -409,11 +404,6 @@ public static class AccountHandler e.Accepted = true; e.CityInfo = CharacterCreation.GetStartingCities(acct.Young); } - - if (!e.Accepted) - { - AccountAttackLimiter.RegisterInvalidAccess(e.State); - } } public static bool CheckAccount(Mobile mobCheck, Mobile accCheck) diff --git a/Projects/UOContent/Gumps/AdminGump.cs b/Projects/UOContent/Gumps/AdminGump.cs index 51450d7c9..0b5d562cf 100644 --- a/Projects/UOContent/Gumps/AdminGump.cs +++ b/Projects/UOContent/Gumps/AdminGump.cs @@ -36,7 +36,6 @@ namespace Server.Gumps AccountDetails_Characters, AccountDetails_Access, AccountDetails_Access_ClientIPs, - AccountDetails_Access_Restrictions, AccountDetails_Comments, AccountDetails_Tags, AccountDetails_ChangePassword, @@ -132,7 +131,6 @@ namespace Server.Gumps AdminGumpPage.AccountDetails_Characters, AdminGumpPage.AccountDetails_Access, AdminGumpPage.AccountDetails_Access_ClientIPs, - AdminGumpPage.AccountDetails_Access_Restrictions, AdminGumpPage.AccountDetails_Comments, AdminGumpPage.AccountDetails_Tags, AdminGumpPage.AccountDetails_ChangeAccess, @@ -801,8 +799,7 @@ namespace Server.Gumps GetButtonID(5, 13), "Access", AdminGumpPage.AccountDetails_Access, - AdminGumpPage.AccountDetails_Access_ClientIPs, - AdminGumpPage.AccountDetails_Access_Restrictions + AdminGumpPage.AccountDetails_Access_ClientIPs ); AddPageButton(190, 70, GetButtonID(5, 2), "Comments", AdminGumpPage.AccountDetails_Comments); AddPageButton(190, 90, GetButtonID(5, 3), "Tags", AdminGumpPage.AccountDetails_Tags); @@ -968,13 +965,6 @@ namespace Server.Gumps "View client addresses", AdminGumpPage.AccountDetails_Access_ClientIPs ); - AddPageButton( - 20, - 170, - GetButtonID(5, 15), - "Manage restrictions", - AdminGumpPage.AccountDetails_Access_Restrictions - ); goto case AdminGumpPage.AccountDetails; } @@ -1038,67 +1028,6 @@ namespace Server.Gumps AddButton(190, 242 + i * 22, 0xFB1, 0xFB3, GetButtonID(10, index)); } - goto case AdminGumpPage.AccountDetails_Access; - } - case AdminGumpPage.AccountDetails_Access_Restrictions: - { - if (state is not Account a) - { - break; - } - - m_List ??= a.IpRestrictions.ToList(); - - AddHtml(10, 195, 400, 20, "Address Restrictions".Center(LabelColor32)); - - AddTextField(227, 225, 120, 20, 0); - - AddButtonLabeled(352, 225, GetButtonID(5, 19), "Add"); - - AddHtml( - 225, - 255, - 180, - 120, - "Any clients connecting from an address not in this list will be rejected. Or, if the list is empty, any client may connect.".Color( - LabelColor32 - ) - ); - - AddImageTiled(15, 219, 206, 156, 0xBBC); - AddBlackAlpha(16, 220, 204, 154); - - AddHtml(18, 221, 114, 20, "IP Address".Color(LabelColor32)); - - if (listPage > 0) - { - AddButton(184, 223, 0x15E3, 0x15E7, GetButtonID(1, 0)); - } - else - { - AddImage(184, 223, 0x25EA); - } - - if ((listPage + 1) * 6 < m_List.Count) - { - AddButton(201, 223, 0x15E1, 0x15E5, GetButtonID(1, 1)); - } - else - { - AddImage(201, 223, 0x25E6); - } - - if (m_List.Count == 0) - { - AddHtml(18, 243, 200, 60, "There are no addresses in this list.".Color(LabelColor32)); - } - - for (int i = 0, index = listPage * 6; i < 6 && index >= 0 && index < m_List.Count; ++i, ++index) - { - AddHtml(18, 243 + i * 22, 114, 20, ((string)m_List[index]).Color(LabelColor32)); - AddButton(190, 242 + i * 22, 0xFB1, 0xFB3, GetButtonID(8, index)); - } - goto case AdminGumpPage.AccountDetails_Access; } case AdminGumpPage.AccountDetails_Characters: @@ -2733,20 +2662,6 @@ namespace Server.Gumps ); break; } - case 15: - { - from.SendGump( - new AdminGump( - from, - AdminGumpPage.AccountDetails_Access_Restrictions, - 0, - null, - null, - m_State - ) - ); - break; - } case 4: { from.Prompt = new AddCommentPrompt(m_State as Account); @@ -3101,65 +3016,6 @@ namespace Server.Gumps ); } - break; - } - case 19: // add - { - if (m_State is not Account a) - { - break; - } - - var ip = info.GetTextEntry(0)?.Trim(); - - string notice; - - if (string.IsNullOrEmpty(ip)) - { - notice = "You must enter an address to add."; - } - else - { - var list = a.IpRestrictions; - - var contains = false; - for (var i = 0; !contains && i < list.Length; ++i) - { - contains = list[i] == ip; - } - - if (contains) - { - notice = "That address is already contained in the list."; - } - else - { - var newList = new string[list.Length + 1]; - - for (var i = 0; i < list.Length; ++i) - { - newList[i] = list[i]; - } - - newList[list.Length] = ip; - - a.IpRestrictions = newList; - - notice = $"{ip} : Added to restriction list."; - } - } - - from.SendGump( - new AdminGump( - from, - AdminGumpPage.AccountDetails_Access_Restrictions, - 0, - null, - notice, - m_State - ) - ); - break; } case 20: // Change access level @@ -3957,23 +3813,6 @@ namespace Server.Gumps ) ); } - else if (m_PageType == AdminGumpPage.AccountDetails_Access_Restrictions) - { - var list = a.IpRestrictions.ToList(); - list.Remove(m_List[index] as string); - a.IpRestrictions = list.ToArray(); - - from.SendGump( - new AdminGump( - from, - AdminGumpPage.AccountDetails_Access_Restrictions, - 0, - null, - $"{m_List[index]} : Removed from list.", - a - ) - ); - } } break; diff --git a/Projects/UOContent/Migrations/Server.Accounting.Account.v6.json b/Projects/UOContent/Migrations/Server.Accounting.Account.v6.json new file mode 100644 index 000000000..fe8cf434c --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Accounting.Account.v6.json @@ -0,0 +1,115 @@ +{ + "version": 6, + "type": "Server.Accounting.Account", + "properties": [ + { + "name": "Username", + "type": "string", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "InternString" + ] + }, + { + "name": "PasswordAlgorithm", + "type": "Server.Accounting.Security.PasswordProtectionAlgorithm", + "rule": "EnumMigrationRule" + }, + { + "name": "Password", + "type": "string", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "AccessLevel", + "type": "Server.AccessLevel", + "rule": "EnumMigrationRule" + }, + { + "name": "Flags", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "LastLogin", + "type": "System.DateTime", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "TotalGold", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "TotalPlat", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Mobiles", + "type": "Server.Mobile[]", + "rule": "ArrayMigrationRule", + "ruleArguments": [ + "Server.Mobile", + "SerializableInterfaceMigrationRule" + ] + }, + { + "name": "Comments", + "type": "System.Collections.Generic.List\u003CServer.Accounting.AccountComment\u003E", + "rule": "ListMigrationRule", + "ruleArguments": [ + "Server.Accounting.AccountComment", + "SerializationMethodSignatureMigrationRule", + "" + ] + }, + { + "name": "Tags", + "type": "System.Collections.Generic.List\u003CServer.Accounting.AccountTag\u003E", + "rule": "ListMigrationRule", + "ruleArguments": [ + "Server.Accounting.AccountTag", + "SerializationMethodSignatureMigrationRule", + "" + ] + }, + { + "name": "LoginIPs", + "type": "System.Net.IPAddress[]", + "rule": "ArrayMigrationRule", + "ruleArguments": [ + "System.Net.IPAddress", + "PrimitiveTypeMigrationRule" + ] + }, + { + "name": "TotalGameTime", + "type": "System.TimeSpan", + "rule": "PrimitiveTypeMigrationRule" + }, + { + "name": "Email", + "type": "string", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Misc/PacketThrottles.cs b/Projects/UOContent/Misc/PacketThrottles.cs index 8a475bf78..9bd76ee75 100644 --- a/Projects/UOContent/Misc/PacketThrottles.cs +++ b/Projects/UOContent/Misc/PacketThrottles.cs @@ -2,147 +2,140 @@ using System; using System.Collections.Generic; using System.IO; using Server.Json; -using Server.Mobiles; -namespace Server.Network +namespace Server.Network; + +public static class PacketThrottles { - public static class PacketThrottles + // Delay in milliseconds + private static readonly int[] Delays = new int[0x100]; + private const string ThrottlesConfiguration = "Configuration/throttles.json"; + + public static void Configure() { - // Delay in milliseconds - private static readonly int[] Delays = new int[0x100]; - private const string ThrottlesConfiguration = "Configuration/throttles.json"; - - public static void Configure() - { - CommandSystem.Register("GetThrottle", AccessLevel.Administrator, GetThrottle); - CommandSystem.Register("SetThrottle", AccessLevel.Administrator, SetThrottle); - } - - public static unsafe void Initialize() - { - var path = Path.Join(Core.BaseDirectory, ThrottlesConfiguration); - - if (File.Exists(path)) - { - var throttles = JsonConfig.Deserialize>(path); - foreach (var (k, v) in throttles) - { - if (!Utility.ToInt32(k, out var packetId)) - { - Utility.PushColor(ConsoleColor.DarkYellow); - Console.WriteLine("Packet Throttles: Error deserializing {0} from {1}", k, ThrottlesConfiguration); - Utility.PopColor(); - continue; - } - - Delays[packetId] = v; - } - } - else - { - Delays[0x03] = 25; // Speech - Delays[0xAD] = 25; // Speech - Delays[0x12] = 25; // Text Commands - Delays[0x75] = 500; // Rename request - } - - for (int i = 0; i < 0x100; i++) - { - if (Delays[i] > 0) - { - IncomingPackets.RegisterThrottler(i, &Throttle); - } - } - - SaveDelays(); - } - - [Usage("GetThrottle ")] - [Description("Gets throttle for the given packet.")] - public static void GetThrottle(CommandEventArgs e) - { - if (e.Length != 1) - { - e.Mobile.SendMessage("Invalid Command Format. Should be [GetThrottle "); - return; - } - - int packetID = e.GetInt32(0); - - if (packetID is < 0 or > 0x100) - { - e.Mobile.SendMessage("Invalid Command Format. PacketID must be between 0 and 0x100."); - return; - } - - e.Mobile.SendMessage($"Packet 0x{packetID:X} throttle is currently {Delays[packetID]}ms."); - } - - [Usage("SetThrottle ")] - [Description("Sets a throttle for the given packet.")] - public static unsafe void SetThrottle(CommandEventArgs e) - { - if (e.Length != 2) - { - e.Mobile.SendMessage("Invalid Command Format. Should be [SetThrottle "); - return; - } - - int packetID = e.GetInt32(0); - int delay = e.GetInt32(1); - - if (packetID is < 0 or > 0x100) - { - e.Mobile.SendMessage("Invalid Command Format. PacketID must be between 0 and 0x100."); - return; - } - - if (delay > 5000) - { - e.Mobile.SendMessage("Invalid Command Format. Delay cannot exceed 5000 milliseconds."); - return; - } - - long oldDelay = Delays[packetID]; - - if (oldDelay == 0 && delay > 0) - { - IncomingPackets.RegisterThrottler(packetID, &Throttle); - e.Mobile.SendMessage($"Set throttle for packet 0x{packetID:X2} to {delay}ms."); - } - else if (oldDelay > 0 && delay == 0) - { - IncomingPackets.RegisterThrottler(packetID, null); - e.Mobile.SendMessage($"Removed throttle for packet 0x{packetID:X2}"); - } - - Delays[packetID] = delay; - SaveDelays(); - } - - private static void SaveDelays() - { - SortedDictionary table = new(); - for (var i = 0; i < Delays.Length; i++) - { - var delay = Delays[i]; - - if (delay != 0) - { - table[$"0x{i:X2}"] = delay; - } - } - - var path = Path.Join(Core.BaseDirectory, ThrottlesConfiguration); - JsonConfig.Serialize(path, table); - } - - public static bool Throttle(int packetID, NetState ns, out bool drop) - { - drop = ns.Mobile is PlayerMobile { AccessLevel: < AccessLevel.Counselor } - && Core.TickCount - ns.GetPacketTime(packetID) < Delays[packetID]; - - return !drop; - } + CommandSystem.Register("GetThrottle", AccessLevel.Administrator, GetThrottle); + CommandSystem.Register("SetThrottle", AccessLevel.Administrator, SetThrottle); } + + public static unsafe void Initialize() + { + var path = Path.Join(Core.BaseDirectory, ThrottlesConfiguration); + + if (File.Exists(path)) + { + var throttles = JsonConfig.Deserialize>(path); + foreach (var (k, v) in throttles) + { + if (!Utility.ToInt32(k, out var packetId)) + { + Utility.PushColor(ConsoleColor.DarkYellow); + Console.WriteLine("Packet Throttles: Error deserializing {0} from {1}", k, ThrottlesConfiguration); + Utility.PopColor(); + continue; + } + + Delays[packetId] = v; + } + } + else + { + Delays[0x03] = 25; // Speech + Delays[0xAD] = 25; // Speech + Delays[0x12] = 25; // Text Commands + Delays[0x75] = 500; // Rename request + } + + for (int i = 0; i < 0x100; i++) + { + if (Delays[i] > 0) + { + IncomingPackets.RegisterThrottler(i, &Throttle); + } + } + + SaveDelays(); + } + + [Usage("GetThrottle ")] + [Description("Gets throttle for the given packet.")] + public static void GetThrottle(CommandEventArgs e) + { + if (e.Length != 1) + { + e.Mobile.SendMessage("Invalid Command Format. Should be [GetThrottle "); + return; + } + + int packetID = e.GetInt32(0); + + if (packetID is < 0 or > 0x100) + { + e.Mobile.SendMessage("Invalid Command Format. PacketID must be between 0 and 0x100."); + return; + } + + e.Mobile.SendMessage($"Packet 0x{packetID:X} throttle is currently {Delays[packetID]}ms."); + } + + [Usage("SetThrottle ")] + [Description("Sets a throttle for the given packet.")] + public static unsafe void SetThrottle(CommandEventArgs e) + { + if (e.Length != 2) + { + e.Mobile.SendMessage("Invalid Command Format. Should be [SetThrottle "); + return; + } + + int packetID = e.GetInt32(0); + int delay = e.GetInt32(1); + + if (packetID is < 0 or > 0x100) + { + e.Mobile.SendMessage("Invalid Command Format. PacketID must be between 0 and 0x100."); + return; + } + + if (delay > 5000) + { + e.Mobile.SendMessage("Invalid Command Format. Delay cannot exceed 5000 milliseconds."); + return; + } + + long oldDelay = Delays[packetID]; + + if (oldDelay == 0 && delay > 0) + { + IncomingPackets.RegisterThrottler(packetID, &Throttle); + e.Mobile.SendMessage($"Set throttle for packet 0x{packetID:X2} to {delay}ms."); + } + else if (oldDelay > 0 && delay == 0) + { + IncomingPackets.RegisterThrottler(packetID, null); + e.Mobile.SendMessage($"Removed throttle for packet 0x{packetID:X2}"); + } + + Delays[packetID] = delay; + SaveDelays(); + } + + private static void SaveDelays() + { + SortedDictionary table = new(); + for (var i = 0; i < Delays.Length; i++) + { + var delay = Delays[i]; + + if (delay != 0) + { + table[$"0x{i:X2}"] = delay; + } + } + + var path = Path.Join(Core.BaseDirectory, ThrottlesConfiguration); + JsonConfig.Serialize(path, table); + } + + public static bool Throttle(int packetId, NetState ns) => + ns.Mobile is { AccessLevel: AccessLevel.Player } && Core.TickCount - ns.GetPacketTime(packetId) < Delays[packetId]; }