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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace Server.Misc;
|
namespace Server.Misc;
|
||||||
|
|
||||||
public static class IPLimiter
|
public static class IPLimiter
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<IPAddress, int> _connectionAttempts = new(128);
|
private static readonly SortedSet<IPAccessLog> _connectionAttempts = [];
|
||||||
private static readonly HashSet<IPAddress> _throttledAddresses = new();
|
private static readonly SortedSet<IPAccessLog> _throttledAddresses = [];
|
||||||
|
|
||||||
private static long _lastClearedThrottles;
|
private static readonly IPAddress _localHost = IPAddress.Parse("127.0.0.1");
|
||||||
private static long _lastClearedAttempts;
|
|
||||||
|
|
||||||
public static readonly IPAddress[] Exemptions =
|
public static TimeSpan ConnectionAttemptsDuration { get; private set; }
|
||||||
{
|
public static TimeSpan ConnectionThrottleDuration { get; private set; }
|
||||||
IPAddress.Parse( "127.0.0.1" )
|
|
||||||
};
|
|
||||||
|
|
||||||
public static TimeSpan ClearConnectionAttemptsDuration { get; private set; }
|
|
||||||
|
|
||||||
public static TimeSpan ClearThrottledDuration { get; private set; }
|
|
||||||
|
|
||||||
public static bool Enabled { 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()
|
public static void Configure()
|
||||||
{
|
{
|
||||||
Enabled = ServerConfiguration.GetOrUpdateSetting("ipLimiter.enable", true);
|
Enabled = ServerConfiguration.GetOrUpdateSetting("ipLimiter.enable", true);
|
||||||
MaxAddresses = ServerConfiguration.GetOrUpdateSetting("ipLimiter.maxConnectionsPerIP", 10);
|
MaxConnections = ServerConfiguration.GetOrUpdateSetting("ipLimiter.maxConnectionsPerIP", 5);
|
||||||
ClearConnectionAttemptsDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearConnectionAttemptsDuration", TimeSpan.FromSeconds(10));
|
ConnectionAttemptsDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearConnectionAttemptsDuration", TimeSpan.FromSeconds(10));
|
||||||
ClearThrottledDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.clearThrottledDuration", TimeSpan.FromMinutes(2));
|
ConnectionThrottleDuration = ServerConfiguration.GetOrUpdateSetting("ipLimiter.connectionThrottleDuration", TimeSpan.FromMinutes(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsExempt(IPAddress ip)
|
private static readonly IPAccessLog _accessCheck = new(IPAddress.None, DateTime.MinValue);
|
||||||
{
|
|
||||||
for (int i = 0; i < Exemptions.Length; i++)
|
|
||||||
{
|
|
||||||
if (ip.Equals(Exemptions[i]))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool Verify(IPAddress ourAddress)
|
public static bool Verify(IPAddress ourAddress)
|
||||||
{
|
{
|
||||||
if (!Enabled || IsExempt(ourAddress))
|
if (!Enabled || ourAddress.Equals(_localHost))
|
||||||
{
|
{
|
||||||
return true;
|
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)
|
_connectionAttempts.Remove(accessLog);
|
||||||
{
|
accessLog.Count++;
|
||||||
_lastClearedThrottles = now;
|
|
||||||
ClearThrottledAddresses();
|
if (now <= accessLog.Expiration && accessLog.Count >= MaxConnections)
|
||||||
}
|
|
||||||
else if (_throttledAddresses.Contains(ourAddress))
|
|
||||||
{
|
{
|
||||||
|
BlockConnection(now, accessLog);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (_connectionAttempts.Count > 0 && now - _lastClearedAttempts > ClearConnectionAttemptsDuration.TotalMilliseconds)
|
accessLog.Expiration = now + ConnectionAttemptsDuration;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
_lastClearedAttempts = now;
|
accessLog = new IPAccessLog(ourAddress, now + ConnectionAttemptsDuration);
|
||||||
ClearConnectionAttempts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_connectionAttempts, ourAddress, out _);
|
// Add it back so it is sorted properly
|
||||||
count++;
|
_connectionAttempts.Add(accessLog);
|
||||||
|
|
||||||
if (count > MaxAddresses)
|
|
||||||
{
|
|
||||||
_connectionAttempts.Remove(ourAddress);
|
|
||||||
_throttledAddresses.Add(ourAddress);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
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();
|
while (_throttledAddresses.Count > 0)
|
||||||
_connectionAttempts.TrimExcess(128);
|
{
|
||||||
|
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);
|
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;
|
var from = ns.Mobile;
|
||||||
|
|
||||||
if (from?.Deleted != false || from.AccessLevel > AccessLevel.Player)
|
if (from?.Deleted != false || from.AccessLevel > AccessLevel.Player)
|
||||||
{
|
{
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
long now = Core.TickCount;
|
long now = Core.TickCount;
|
||||||
|
|
@ -53,7 +51,7 @@ public static class MovementThrottle
|
||||||
{
|
{
|
||||||
ns._movementCredit = 0;
|
ns._movementCredit = 0;
|
||||||
ns._nextMovementTime = now;
|
ns._nextMovementTime = now;
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
long cost = nextMove - now;
|
long cost = nextMove - now;
|
||||||
|
|
@ -61,11 +59,11 @@ public static class MovementThrottle
|
||||||
if (credit < cost)
|
if (credit < cost)
|
||||||
{
|
{
|
||||||
// Not enough credit, therefore throttled
|
// Not enough credit, therefore throttled
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// On the next event loop, the player receives up to 400ms in grace latency
|
// On the next event loop, the player receives up to 400ms in grace latency
|
||||||
ns._movementCredit = Math.Min(_throttleThreshold, credit - cost);
|
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;
|
var throttler = handler.ThrottleCallback;
|
||||||
if (throttler != null)
|
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);
|
SetPacketTime(packetId);
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ public unsafe class PacketHandler
|
||||||
|
|
||||||
public delegate*<NetState, SpanReader, void> OnReceive { get; }
|
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; }
|
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);
|
var ph = GetHandler(packetID);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,6 @@ public static class TcpServer
|
||||||
|
|
||||||
private static void ProcessConnection(Socket socket)
|
private static void ProcessConnection(Socket socket)
|
||||||
{
|
{
|
||||||
var ipLimiter = IPLimiter.Enabled;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address;
|
var remoteIP = ((IPEndPoint)socket.RemoteEndPoint)!.Address;
|
||||||
|
|
@ -247,15 +246,6 @@ public static class TcpServer
|
||||||
return;
|
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);
|
var firewalled = Firewall.IsBlocked(remoteIP);
|
||||||
if (!firewalled)
|
if (!firewalled)
|
||||||
{
|
{
|
||||||
|
|
@ -273,6 +263,15 @@ public static class TcpServer
|
||||||
return;
|
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);
|
var ns = new NetState(socket);
|
||||||
ConnectedQueue.Enqueue(ns);
|
ConnectedQueue.Enqueue(ns);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,121 +3,136 @@ using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Server.Accounting.Security;
|
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
|
_username = content.Username;
|
||||||
private void MigrateFrom(V4Content content)
|
_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;
|
// Due to a bug where we were not versioning at all, reset so we don't have an issue deserializing
|
||||||
_passwordAlgorithm = content.PasswordAlgorithm;
|
reader.Seek(0, SeekOrigin.Begin);
|
||||||
_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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
_mobiles[i] = reader.ReadEntity<Mobile>();
|
||||||
_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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
if (IPAddress.TryParse(reader.ReadString(), out var address))
|
||||||
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))
|
_loginIPs[i] = Utility.Intern(address);
|
||||||
{
|
|
||||||
_loginIPs[i] = Utility.Intern(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_loginIPs[i] = reader.ReadIPAddress();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
length = reader.ReadInt();
|
|
||||||
_ipRestrictions = new string[length];
|
|
||||||
for (int i = 0; i < length; i++)
|
|
||||||
{
|
{
|
||||||
_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)
|
_totalGameTime = reader.ReadTimeSpan();
|
||||||
{
|
|
||||||
_email = reader.ReadString();
|
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);
|
acct.LogAccess(e.State);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!e.Accepted)
|
|
||||||
{
|
|
||||||
AccountAttackLimiter.RegisterInvalidAccess(e.State);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void EventSink_GameLogin(GameLoginEventArgs e)
|
public static void EventSink_GameLogin(GameLoginEventArgs e)
|
||||||
|
|
@ -409,11 +404,6 @@ public static class AccountHandler
|
||||||
e.Accepted = true;
|
e.Accepted = true;
|
||||||
e.CityInfo = CharacterCreation.GetStartingCities(acct.Young);
|
e.CityInfo = CharacterCreation.GetStartingCities(acct.Young);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!e.Accepted)
|
|
||||||
{
|
|
||||||
AccountAttackLimiter.RegisterInvalidAccess(e.State);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool CheckAccount(Mobile mobCheck, Mobile accCheck)
|
public static bool CheckAccount(Mobile mobCheck, Mobile accCheck)
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ namespace Server.Gumps
|
||||||
AccountDetails_Characters,
|
AccountDetails_Characters,
|
||||||
AccountDetails_Access,
|
AccountDetails_Access,
|
||||||
AccountDetails_Access_ClientIPs,
|
AccountDetails_Access_ClientIPs,
|
||||||
AccountDetails_Access_Restrictions,
|
|
||||||
AccountDetails_Comments,
|
AccountDetails_Comments,
|
||||||
AccountDetails_Tags,
|
AccountDetails_Tags,
|
||||||
AccountDetails_ChangePassword,
|
AccountDetails_ChangePassword,
|
||||||
|
|
@ -132,7 +131,6 @@ namespace Server.Gumps
|
||||||
AdminGumpPage.AccountDetails_Characters,
|
AdminGumpPage.AccountDetails_Characters,
|
||||||
AdminGumpPage.AccountDetails_Access,
|
AdminGumpPage.AccountDetails_Access,
|
||||||
AdminGumpPage.AccountDetails_Access_ClientIPs,
|
AdminGumpPage.AccountDetails_Access_ClientIPs,
|
||||||
AdminGumpPage.AccountDetails_Access_Restrictions,
|
|
||||||
AdminGumpPage.AccountDetails_Comments,
|
AdminGumpPage.AccountDetails_Comments,
|
||||||
AdminGumpPage.AccountDetails_Tags,
|
AdminGumpPage.AccountDetails_Tags,
|
||||||
AdminGumpPage.AccountDetails_ChangeAccess,
|
AdminGumpPage.AccountDetails_ChangeAccess,
|
||||||
|
|
@ -801,8 +799,7 @@ namespace Server.Gumps
|
||||||
GetButtonID(5, 13),
|
GetButtonID(5, 13),
|
||||||
"Access",
|
"Access",
|
||||||
AdminGumpPage.AccountDetails_Access,
|
AdminGumpPage.AccountDetails_Access,
|
||||||
AdminGumpPage.AccountDetails_Access_ClientIPs,
|
AdminGumpPage.AccountDetails_Access_ClientIPs
|
||||||
AdminGumpPage.AccountDetails_Access_Restrictions
|
|
||||||
);
|
);
|
||||||
AddPageButton(190, 70, GetButtonID(5, 2), "Comments", AdminGumpPage.AccountDetails_Comments);
|
AddPageButton(190, 70, GetButtonID(5, 2), "Comments", AdminGumpPage.AccountDetails_Comments);
|
||||||
AddPageButton(190, 90, GetButtonID(5, 3), "Tags", AdminGumpPage.AccountDetails_Tags);
|
AddPageButton(190, 90, GetButtonID(5, 3), "Tags", AdminGumpPage.AccountDetails_Tags);
|
||||||
|
|
@ -968,13 +965,6 @@ namespace Server.Gumps
|
||||||
"View client addresses",
|
"View client addresses",
|
||||||
AdminGumpPage.AccountDetails_Access_ClientIPs
|
AdminGumpPage.AccountDetails_Access_ClientIPs
|
||||||
);
|
);
|
||||||
AddPageButton(
|
|
||||||
20,
|
|
||||||
170,
|
|
||||||
GetButtonID(5, 15),
|
|
||||||
"Manage restrictions",
|
|
||||||
AdminGumpPage.AccountDetails_Access_Restrictions
|
|
||||||
);
|
|
||||||
|
|
||||||
goto case AdminGumpPage.AccountDetails;
|
goto case AdminGumpPage.AccountDetails;
|
||||||
}
|
}
|
||||||
|
|
@ -1038,67 +1028,6 @@ namespace Server.Gumps
|
||||||
AddButton(190, 242 + i * 22, 0xFB1, 0xFB3, GetButtonID(10, index));
|
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;
|
goto case AdminGumpPage.AccountDetails_Access;
|
||||||
}
|
}
|
||||||
case AdminGumpPage.AccountDetails_Characters:
|
case AdminGumpPage.AccountDetails_Characters:
|
||||||
|
|
@ -2733,20 +2662,6 @@ namespace Server.Gumps
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 15:
|
|
||||||
{
|
|
||||||
from.SendGump(
|
|
||||||
new AdminGump(
|
|
||||||
from,
|
|
||||||
AdminGumpPage.AccountDetails_Access_Restrictions,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
m_State
|
|
||||||
)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 4:
|
case 4:
|
||||||
{
|
{
|
||||||
from.Prompt = new AddCommentPrompt(m_State as Account);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case 20: // Change access level
|
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;
|
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.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Server.Json;
|
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
|
CommandSystem.Register("GetThrottle", AccessLevel.Administrator, GetThrottle);
|
||||||
private static readonly int[] Delays = new int[0x100];
|
CommandSystem.Register("SetThrottle", AccessLevel.Administrator, SetThrottle);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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