fix: Fixes stalled connections and infinite throttle (#1796)
> [!Warning] > **Developer Warning** > The `PacketThrottle` callback return value is now reversed. `true` indicates the connection is _throttled_. ### Summary - Fixes an issue where connections get stalled forever - Fixes an issue where the throttler is not working properly - Removes account attack limiter - Rewrites IP limiter - Removes IP restrictions (they weren't used, and not practical) - Fixes issue where IP limiter was counting before firewall was blocking. View without whitespace: https://github.com/modernuo/ModernUO/pull/1796/files?diff=split&w=1
This commit is contained in:
parent
ccad915464
commit
a4522b9d43
13 changed files with 1538 additions and 1783 deletions
|
|
@ -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<IPAddress, int> _connectionAttempts = new(128);
|
||||
private static readonly HashSet<IPAddress> _throttledAddresses = new();
|
||||
private static readonly SortedSet<IPAccessLog> _connectionAttempts = [];
|
||||
private static readonly SortedSet<IPAccessLog> _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<IPAccessLog>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -823,9 +823,9 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
|||
var throttler = handler.ThrottleCallback;
|
||||
if (throttler != null)
|
||||
{
|
||||
if (!throttler(packetId, this, out bool drop))
|
||||
if (throttler(packetId, this))
|
||||
{
|
||||
return drop ? ParserState.Throttled : ParserState.AwaitingNextPacket;
|
||||
return ParserState.Throttled;
|
||||
}
|
||||
|
||||
SetPacketTime(packetId);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ public unsafe class PacketHandler
|
|||
|
||||
public delegate*<NetState, SpanReader, void> OnReceive { get; }
|
||||
|
||||
public delegate*<int, NetState, out bool, bool> ThrottleCallback { get; set; }
|
||||
public delegate*<int, NetState, bool> ThrottleCallback { get; set; }
|
||||
|
||||
public bool Ingame { get; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public static class IncomingPackets
|
|||
}
|
||||
}
|
||||
|
||||
public static unsafe void RegisterThrottler(int packetID, delegate*<int, NetState, out bool, bool> t)
|
||||
public static unsafe void RegisterThrottler(int packetID, delegate*<int, NetState, bool> t)
|
||||
{
|
||||
var ph = GetHandler(packetID);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PasswordProtectionAlgorithm>();
|
||||
_password = reader.ReadString();
|
||||
_accessLevel = version < 2 ? (AccessLevel)reader.ReadInt() : reader.ReadEnum<AccessLevel>();
|
||||
_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<Mobile>();
|
||||
}
|
||||
|
||||
private void Deserialize(IGenericReader reader, int version)
|
||||
length = reader.ReadInt();
|
||||
_comments = length > 0 ? new List<AccountComment>(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<AccountTag>(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<PasswordProtectionAlgorithm>();
|
||||
_password = reader.ReadString();
|
||||
_accessLevel = version < 2 ? (AccessLevel)reader.ReadInt() : reader.ReadEnum<AccessLevel>();
|
||||
_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<Mobile>();
|
||||
}
|
||||
|
||||
length = reader.ReadInt();
|
||||
_comments = length > 0 ? new List<AccountComment>(length) : null;
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
_comments!.Add(new AccountComment(reader));
|
||||
}
|
||||
|
||||
length = reader.ReadInt();
|
||||
_tags = length > 0 ? new List<AccountTag>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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<InvalidAccountAccessLog> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<object>();
|
||||
|
||||
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;
|
||||
|
|
|
|||
115
Projects/UOContent/Migrations/Server.Accounting.Account.v6.json
generated
Normal file
115
Projects/UOContent/Migrations/Server.Accounting.Account.v6.json
generated
Normal file
|
|
@ -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": [
|
||||
""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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<SortedDictionary<string, int>>(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 <packetID>")]
|
||||
[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 <packetID>");
|
||||
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 <packetID> <timeInMilliseconds>")]
|
||||
[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 <packetID> <timeInMilliseconds>");
|
||||
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<string, int> 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<SortedDictionary<string, int>>(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 <packetID>")]
|
||||
[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 <packetID>");
|
||||
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 <packetID> <timeInMilliseconds>")]
|
||||
[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 <packetID> <timeInMilliseconds>");
|
||||
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<string, int> 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];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue