diff --git a/Projects/Server/Configuration/ServerConfiguration.cs b/Projects/Server/Configuration/ServerConfiguration.cs index 7463aadff..d32519748 100644 --- a/Projects/Server/Configuration/ServerConfiguration.cs +++ b/Projects/Server/Configuration/ServerConfiguration.cs @@ -38,11 +38,11 @@ namespace Server public static List Listeners => m_Settings.Listeners; - public static string GetSetting(string key, string defaultValue) - { - m_Settings.Settings.TryGetValue(key, out var value); - return value ?? defaultValue; - } + public static ClientVersion GetSetting(string key, ClientVersion defaultValue) => + m_Settings.Settings.TryGetValue(key, out var value) ? new ClientVersion(value) : defaultValue; + + public static string GetSetting(string key, string defaultValue) => + m_Settings.Settings.TryGetValue(key, out var value) ? value : defaultValue; public static int GetSetting(string key, int defaultValue) { diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index 5c0f91e60..46a9622aa 100755 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -197,7 +197,9 @@ namespace Server.Network public Pipe SendPipe { get; } - public Socket Connection { get; } + public bool Running => _running; + + public Socket Connection { get; private set; } public bool CompressionEnabled { get; set; } diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index a9b9f1955..b78e67a41 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -39,7 +39,7 @@ namespace Server.Network private static readonly byte[] _socketRejected = { 0x82, 0xFF }; public static IPEndPoint[] ListeningAddresses { get; private set; } - public static TcpListener[] Listeners { get; private set; } + public static Socket[] Listeners { get; private set; } public static HashSet Instances { get; } = new(2048); private static readonly ConcurrentQueue _connectedQueue = new(); @@ -52,7 +52,7 @@ namespace Server.Network public static void Start() { HashSet listeningAddresses = new HashSet(); - List listeners = new List(); + List listeners = new List(); foreach (var ipep in ServerConfiguration.Listeners) { @@ -88,7 +88,7 @@ namespace Server.Network { foreach (var listener in Listeners) { - listener.Server.Close(); + listener.Close(); } } @@ -99,21 +99,19 @@ namespace Server.Network .Select(uip => new IPEndPoint(uip.Address, ipep.Port)) ); - public static TcpListener CreateListener(IPEndPoint ipep) + public static Socket CreateListener(IPEndPoint ipep) { - var listener = new TcpListener(ipep) + var listener = new Socket(ipep.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { - Server = - { - LingerState = new LingerOption(false, 0), - ExclusiveAddressUse = true, - NoDelay = true - } + LingerState = new LingerOption(false, 0), + ExclusiveAddressUse = true, + NoDelay = true }; try { - listener.Start(32); + listener.Bind(ipep); + listener.Listen(32); return listener; } catch (SocketException se) @@ -148,13 +146,14 @@ namespace Server.Network } } - private static async void BeginAcceptingSockets(this TcpListener listener) + private static async void BeginAcceptingSockets(this Socket listener) { while (true) { try { - var socket = await listener.AcceptSocketAsync(); + var socket = await listener.AcceptAsync(); + var rejected = false; if (Instances.Count >= MaxConnections) { diff --git a/Projects/UOContent/Misc/ClientVerification.cs b/Projects/UOContent/Misc/ClientVerification.cs index 23764174a..046455d2e 100644 --- a/Projects/UOContent/Misc/ClientVerification.cs +++ b/Projects/UOContent/Misc/ClientVerification.cs @@ -1,10 +1,12 @@ using System; -using System.Diagnostics; +using System.Buffers.Binary; using System.IO; +using Server.Buffers; using Server.Gumps; using Server.Logging; using Server.Mobiles; using Server.Network; +using Server.Text; namespace Server.Misc { @@ -12,29 +14,36 @@ namespace Server.Misc { private static readonly ILogger logger = LogFactory.GetLogger(typeof(ClientVerification)); - private static bool m_DetectClientRequirement; - private static OldClientResponse m_OldClientResponse; + private static bool _enable; + private static bool _detectClientRequirement; + private static InvalidClientResponse _invalidClientResponse; + private static string _versionExpression; - private static TimeSpan m_AgeLeniency; - private static TimeSpan m_GameTimeLeniency; + private static TimeSpan _ageLeniency; + private static TimeSpan _gameTimeLeniency; - public static ClientVersion Required { get; set; } + public static ClientVersion MinRequired { get; private set; } + public static ClientVersion MaxRequired { get; private set; } - public static bool AllowRegular { get; set; } = true; - - public static bool AllowUOTD { get; set; } = true; - - public static bool AllowGod { get; set; } = true; - - public static TimeSpan KickDelay { get; set; } + public static bool AllowRegular => true; + public static bool AllowUOTD => false; + public static TimeSpan KickDelay { get; private set; } public static void Configure() { - m_DetectClientRequirement = ServerConfiguration.GetOrUpdateSetting("clientVerification.enable", true); - m_OldClientResponse = - ServerConfiguration.GetOrUpdateSetting("clientVerification.oldClientResponse", OldClientResponse.Kick); - m_AgeLeniency = ServerConfiguration.GetOrUpdateSetting("clientVerification.ageLeniency", TimeSpan.FromDays(10)); - m_GameTimeLeniency = ServerConfiguration.GetOrUpdateSetting( + MinRequired = ServerConfiguration.GetSetting("clientVerification.minRequired", (ClientVersion)null); + MaxRequired = ServerConfiguration.GetSetting("clientVerification.maxRequired", (ClientVersion)null); + + if (MinRequired == null && MaxRequired == null) + { + _detectClientRequirement = ServerConfiguration.GetOrUpdateSetting("clientVerification.detectFromClientExe", true); + } + + _enable = ServerConfiguration.GetOrUpdateSetting("clientVerification.enable", true); + _invalidClientResponse = + ServerConfiguration.GetOrUpdateSetting("clientVerification.invalidClientResponse", InvalidClientResponse.Kick); + _ageLeniency = ServerConfiguration.GetOrUpdateSetting("clientVerification.ageLeniency", TimeSpan.FromDays(10)); + _gameTimeLeniency = ServerConfiguration.GetOrUpdateSetting( "clientVerification.gameTimeLeniency", TimeSpan.FromHours(25) ); @@ -45,121 +54,154 @@ namespace Server.Misc { EventSink.ClientVersionReceived += EventSink_ClientVersionReceived; - if (m_DetectClientRequirement) + if (_detectClientRequirement) { var path = Core.FindDataFile("client.exe", false); if (File.Exists(path)) { - var info = FileVersionInfo.GetVersionInfo(path); - - if (info.FileMajorPart != 0 || info.FileMinorPart != 0 || info.FileBuildPart != 0 || - info.FilePrivatePart != 0) + using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + var buffer = GC.AllocateUninitializedArray((int)fs.Length, true); + fs.Read(buffer); + // VS_VERSION_INFO (unicode) + Span vsVersionInfo = stackalloc byte[] { - Required = new ClientVersion( - info.FileMajorPart, - info.FileMinorPart, - info.FileBuildPart, - info.FilePrivatePart - ); + 0x56, 0x00, 0x53, 0x00, 0x5F, 0x00, 0x56, 0x00, + 0x45, 0x00, 0x52, 0x00, 0x53, 0x00, 0x49, 0x00, + 0x4F, 0x00, 0x4E, 0x00, 0x5F, 0x00, 0x49, 0x00, + 0x4E, 0x00, 0x46, 0x00, 0x4F, 0x00 + }; + + for (var i = 0; i < buffer.Length; i++) + { + if (vsVersionInfo.SequenceEqual(buffer.AsSpan(i, 30))) + { + var offset = i + 42; // 30 + 12 + + var minorPart = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(offset)); + var majorPart = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(offset + 2)); + var privatePart = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(offset + 4)); + var buildPart = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(offset + 6)); + + MinRequired = new ClientVersion(majorPart, minorPart, buildPart, privatePart); + break; + } } } } - if (Required != null) + if (MinRequired != null || MaxRequired != null) { logger.Information( - "Restricting client version to {0}. Action to be taken: {1}", - Required, - m_OldClientResponse + $"Restricting client version to {GetVersionExpression()}. Action to be taken: {_invalidClientResponse}" ); } } + private static string GetVersionExpression() + { + if (_versionExpression == null) + { + if (MinRequired != null && MaxRequired != null) + { + _versionExpression = $"{MinRequired}-{MaxRequired}"; + } + else if (MinRequired != null) + { + _versionExpression = $"{MinRequired} or newer"; + } + else + { + _versionExpression = $"{MaxRequired} or older"; + } + } + + return _versionExpression; + } + private static void EventSink_ClientVersionReceived(NetState state, ClientVersion version) { - string kickMessage = null; + using var message = new ValueStringBuilder(); - if (state.Mobile?.AccessLevel != AccessLevel.Player) + if (!_enable || state.Mobile?.AccessLevel != AccessLevel.Player) { return; } - if (Required != null && version < Required && (m_OldClientResponse == OldClientResponse.Kick || - m_OldClientResponse == OldClientResponse.LenientKick && - Core.Now - state.Mobile.Created > m_AgeLeniency && - state.Mobile is PlayerMobile mobile && - mobile.GameTime > m_GameTimeLeniency)) + var strictRequirement = _invalidClientResponse == InvalidClientResponse.Kick || + _invalidClientResponse == InvalidClientResponse.LenientKick && + Core.Now - state.Mobile.Created > _ageLeniency && + state.Mobile is PlayerMobile mobile && + mobile.GameTime > _gameTimeLeniency; + + bool shouldKick = false; + + if (MinRequired != null && version < MinRequired) { - kickMessage = $"This server requires your client version be at least {Required}."; + message.Append($"This server doesn't support clients older than {MinRequired}."); + shouldKick = strictRequirement; } - else if (!AllowGod || !AllowRegular || !AllowUOTD) + else if (MaxRequired != null && version > MaxRequired) { - if (!AllowGod && version.Type == ClientType.God) + message.Append($"This server doesn't support clients newer than {MaxRequired}."); + shouldKick = strictRequirement; + } + else if (!AllowRegular || !AllowUOTD) + { + if (!AllowRegular && version.Type == ClientType.Regular) { - kickMessage = "This server does not allow god clients to connect."; - } - else if (!AllowRegular && version.Type == ClientType.Regular) - { - kickMessage = "This server does not allow regular clients to connect."; + message.Append("This server does not allow regular clients to connect."); + shouldKick = true; } else if (!AllowUOTD && state.IsUOTDClient) { - kickMessage = "This server does not allow UO:TD clients to connect."; + message.Append("This server does not allow UO:TD clients to connect."); + shouldKick = true; } - if (!AllowGod && !AllowRegular && !AllowUOTD) - { - kickMessage = "This server does not allow any clients to connect."; - } - else if (AllowGod && !AllowRegular && !AllowUOTD && version.Type != ClientType.God) - { - kickMessage = "This server requires you to use the god client."; - } - else if (kickMessage != null) + if (message.Length > 0) { if (AllowRegular && AllowUOTD) { - kickMessage += " You can use regular or UO:TD clients."; + message.Append(" You can use regular or UO:TD clients."); } else if (AllowRegular) { - kickMessage += " You can use regular clients."; + message.Append(" You can use regular clients."); } else if (AllowUOTD) { - kickMessage += " You can use UO:TD clients."; + message.Append(" You can use UO:TD clients."); } } } - if (kickMessage != null) + if (message.Length > 0) { - state.Mobile.SendMessage(0x22, kickMessage); - state.Mobile.SendMessage(0x22, "You will be disconnected in {0} seconds.", KickDelay.TotalSeconds); - - Timer.StartTimer(KickDelay, () => OnKick(state)); + state.Mobile.SendMessage(0x22, message.ToString()); } - else if (Required != null && version < Required) + + if (shouldKick) { - switch (m_OldClientResponse) + state.Mobile.SendMessage(0x22, "You will be disconnected in {0} seconds.", KickDelay.TotalSeconds); + Timer.StartTimer(KickDelay, () => OnKick(state)); + return; + } + + if (message.Length > 0) + { + switch (_invalidClientResponse) { - case OldClientResponse.Warn: + case InvalidClientResponse.Warn: { state.Mobile.SendMessage( 0x22, - "Your client is out of date. Please update your client.", - Required - ); - state.Mobile.SendMessage( - 0x22, - "This server recommends that your client version be at least {0}.", - Required + $"This server recommends that your client version is {GetVersionExpression()}." ); break; } - case OldClientResponse.LenientKick: - case OldClientResponse.Annoy: + case InvalidClientResponse.LenientKick: + case InvalidClientResponse.Annoy: { SendAnnoyGump(state.Mobile); break; @@ -170,10 +212,11 @@ namespace Server.Misc private static void OnKick(NetState ns) { - if (ns.Connection != null) + if (ns.Running) { - ns.LogInfo("Disconnecting, bad version"); - ns.Disconnect($"Invalid client version {ns.Version}."); + var version = ns.Version; + ns.LogInfo($"Disconnecting, bad version ({version})"); + ns.Disconnect($"Invalid client version {version}."); } } @@ -181,12 +224,12 @@ namespace Server.Misc { from.SendMessage("You will be reminded of this again."); - if (m_OldClientResponse == OldClientResponse.LenientKick) + if (_invalidClientResponse == InvalidClientResponse.LenientKick) { from.SendMessage( - "Old clients will be kicked after {0} days of character age and {1} hours of play time", - m_AgeLeniency, - m_GameTimeLeniency + "Invalid clients will be kicked after {0} days of character age and {1} hours of play time", + _ageLeniency, + _gameTimeLeniency ); } @@ -195,28 +238,29 @@ namespace Server.Misc private static void SendAnnoyGump(Mobile m) { - if (m.NetState != null && m.NetState.Version < Required) + if (m.NetState != null) { Gump g = new WarningGump( 1060637, 30720, - $"Your client is out of date. Please update your client.
This server recommends that your client version be at least {Required}.

You are currently using version {m.NetState.Version}.

To patch, run UOPatch.exe inside your Ultima Online folder.", + $"Your client is invalid.
This server recommends that your client version is {GetVersionExpression()}.

You are currently using version {m.NetState.Version}.", 0xFFC000, 480, 360, okay => KickMessage(m, okay), false - ); - - g.Draggable = false; - g.Closable = false; - g.Resizable = false; + ) + { + Draggable = false, + Closable = false, + Resizable = false, + }; m.SendGump(g); } } - private enum OldClientResponse + private enum InvalidClientResponse { Ignore, Warn,