diff --git a/Projects/Server/Events/AccountLoginEvent.cs b/Projects/Server/Events/AccountLoginEvent.cs deleted file mode 100644 index 76628e0eb3..0000000000 --- a/Projects/Server/Events/AccountLoginEvent.cs +++ /dev/null @@ -1,48 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: AccountLoginEvent.cs * - * * - * This program is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, either version 3 of the License, or * - * (at your option) any later version. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program. If not, see . * - *************************************************************************/ - -using System; -using System.Runtime.CompilerServices; -using Server.Network; - -namespace Server; - -public class AccountLoginEventArgs -{ - public AccountLoginEventArgs(NetState state, string username, string password) - { - State = state; - Username = username; - Password = password; - } - - public NetState State { get; } - - public string Username { get; } - - public string Password { get; } - - public bool Accepted { get; set; } - - public ALRReason RejectReason { get; set; } -} - -public static partial class EventSink -{ - public static event Action AccountLogin; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InvokeAccountLogin(AccountLoginEventArgs e) => AccountLogin?.Invoke(e); -} diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index 1dc33cf6b1..836f6d5474 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Net; using Server.Accounting; @@ -24,13 +25,28 @@ public static class AccountHandler private static int MaxAccountsPerIP; private static bool AutoAccountCreation; - private static bool RestrictDeletion = !TestCenter.Enabled; - private static TimeSpan DeleteDelay = TimeSpan.FromDays(7.0); + private static readonly bool RestrictDeletion = !TestCenter.Enabled; + private static readonly TimeSpan DeleteDelay = TimeSpan.FromDays(7.0); private static bool PasswordCommandEnabled; + public static bool AsyncAccountLogin { get; private set; } private static Dictionary m_IPTable; - private static char[] m_ForbiddenChars = { '<', '>', ':', '"', '/', '\\', '|', '?', '*' }; + private static readonly SearchValues _forbiddenAccountChars = SearchValues.Create([ + '\0', '\x1', '\x2', '\x3', '\x4', '\x5', '\x6', '\a', '\b', '\t', '\n', '\v', '\f', '\r', '\x7', '\x8', '\x9', + '\xA', '\xB', '\xC', '\xD', '\xE', '\xF', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', + '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', + + '<', '>', ':', '"', '/', '\\', '|', '?', '*', + + '\x7F', '\xFF', + ]); + + private static readonly SearchValues _forbiddenPasswordChars = SearchValues.Create([ + '\0', '\x1', '\x2', '\x3', '\x4', '\x5', '\x6', '\a', '\b', '\t', '\n', '\v', '\f', '\r', '\x7', '\x8', '\x9', + '\xA', '\xB', '\xC', '\xD', '\xE', '\xF', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', + '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', '\x7F', '\xFF' + ]); public static AccessLevel LockdownLevel { get; set; } @@ -47,7 +63,7 @@ public static Dictionary IPTable if (a.LoginIPs.Length > 0) { var ip = a.LoginIPs[0]; - m_IPTable[ip] = (m_IPTable.TryGetValue(ip, out var value) ? value : 0) + 1; + m_IPTable[ip] = (m_IPTable.GetValueOrDefault(ip, 0)) + 1; } } } @@ -58,6 +74,7 @@ public static Dictionary IPTable public static void Configure() { + AsyncAccountLogin = ServerConfiguration.GetSetting("accountHandler.useAsyncAccountLogin", false); MaxAccountsPerIP = ServerConfiguration.GetOrUpdateSetting("accountHandler.maxAccountsPerIP", 1); AutoAccountCreation = ServerConfiguration.GetOrUpdateSetting("accountHandler.enableAutoAccountCreation", true); PasswordCommandEnabled = ServerConfiguration.GetOrUpdateSetting( @@ -73,7 +90,6 @@ public static void Configure() public static void Initialize() { - EventSink.AccountLogin += EventSink_AccountLogin; EventSink.GameLogin += EventSink_GameLogin; } @@ -262,19 +278,6 @@ public static void DeleteRequest(NetState state, int index) public static bool CanCreate(IPAddress ip) => !IPTable.TryGetValue(ip, out var result) || result < MaxAccountsPerIP; - private static bool IsForbiddenChar(char c) - { - for (var i = 0; i < m_ForbiddenChars.Length; ++i) - { - if (c == m_ForbiddenChars[i]) - { - return true; - } - } - - return false; - } - private static Account CreateAccount(NetState state, string un, string pw) { if (un.Length == 0 || pw.Length == 0) @@ -282,21 +285,19 @@ private static Account CreateAccount(NetState state, string un, string pw) return null; } - var isSafe = !(un.StartsWithOrdinal(" ") || - un.EndsWithOrdinal(" ") || - un.EndsWithOrdinal(".")); + var unSpan = un.AsSpan(); - for (var i = 0; isSafe && i < un.Length; ++i) + if (unSpan[0] == ' ' || unSpan[^1] is ' ' or '.') { - isSafe = un[i] >= 0x20 && un[i] < 0x7F && !IsForbiddenChar(un[i]); + return null; } - for (var i = 0; isSafe && i < pw.Length; ++i) + if (unSpan.ContainsAny(_forbiddenAccountChars)) { - isSafe = pw[i] >= 0x20 && pw[i] < 0x7F; + return null; } - if (!isSafe) + if (pw.AsSpan().ContainsAny(_forbiddenPasswordChars)) { return null; } @@ -320,20 +321,18 @@ private static Account CreateAccount(NetState state, string un, string pw) return a; } - public static void EventSink_AccountLogin(AccountLoginEventArgs e) + public static void AccountLogin(AccountLoginEventArgs e) { - var un = e.Username; - var pw = e.Password; - - e.Accepted = false; - - if (Accounts.GetAccount(un) is not Account acct) + var state = e.State; + var username = e.Username; + var password = e.Password; + if (Accounts.GetAccount(username) is not Account acct) { // To prevent someone from making an account of just '' or a bunch of meaningless spaces - if (AutoAccountCreation && !string.IsNullOrWhiteSpace(un)) + if (AutoAccountCreation && !string.IsNullOrWhiteSpace(username)) { - e.State.Account = acct = CreateAccount(e.State, un, pw); - e.Accepted = acct?.CheckAccess(e.State) ?? false; + state.Account = acct = CreateAccount(state, username, password); + e.Accepted = acct?.CheckAccess(state) ?? false; if (!e.Accepted) { @@ -342,37 +341,37 @@ public static void EventSink_AccountLogin(AccountLoginEventArgs e) } else { - logger.Information("Login: {NetState} Invalid username '{Username}'", e.State, un); + logger.Information("Login: {NetState} Invalid username '{Username}'", state, username); e.RejectReason = ALRReason.Invalid; } } - else if (!acct.HasAccess(e.State)) + else if (!acct.HasAccess(state)) { - logger.Information("Login: {NetState} Access denied for '{Username}'", e.State, un); + logger.Information("Login: {NetState} Access denied for '{Username}'", state, username); e.RejectReason = LockdownLevel > AccessLevel.Player ? ALRReason.BadComm : ALRReason.BadPass; } - else if (!acct.CheckPassword(pw)) + else if (!acct.CheckPassword(password)) { - logger.Information("Login: {NetState} Invalid password for '{Username}'", e.State, un); + logger.Information("Login: {NetState} Invalid password for '{Username}'", state, username); e.RejectReason = ALRReason.BadPass; } else if (acct.Banned) { - logger.Information("Login: {NetState} Banned account '{Username}'", e.State, un); + logger.Information("Login: {NetState} Banned account '{Username}'", state, username); e.RejectReason = ALRReason.Blocked; } else { - logger.Information("Login: {NetState} Valid credentials for '{Username}'", e.State, un); - e.State.Account = acct; + logger.Information("Login: {NetState} Valid credentials for '{Username}'", state, username); + state.Account = acct; e.Accepted = true; - acct.LogAccess(e.State); + acct.LogAccess(state); } if (!e.Accepted) { - AccountAttackLimiter.RegisterInvalidAccess(e.State); + AccountAttackLimiter.RegisterInvalidAccess(state); } } diff --git a/Projects/UOContent/Misc/ServerAccess.cs b/Projects/UOContent/Misc/ServerAccess.cs index f6786246c7..1c0bf20069 100644 --- a/Projects/UOContent/Misc/ServerAccess.cs +++ b/Projects/UOContent/Misc/ServerAccess.cs @@ -63,21 +63,21 @@ public static void Configure() } } - public static void Initialize() + public static void ResetProtectedAccount(AccountLoginEventArgs e) { - EventSink.AccountLogin += EventSink_ResetProtectedAccount; - } + var username = e.Username; + if (Accounts.GetAccount(username) is not Account acct) + { + return; + } - public static void EventSink_ResetProtectedAccount(AccountLoginEventArgs e) - { - var username = e.Username.ToLower(); if (!ServerAccessConfiguration.ProtectedAccounts.Contains(username)) { return; } - if (Accounts.GetAccount(username) is not Account acct - || !acct.Banned && acct.AccessLevel >= AccessLevel.Owner || !acct.CheckPassword(e.Password)) + var password = e.Password; + if (!acct.Banned && acct.AccessLevel >= AccessLevel.Owner || !acct.CheckPassword(password)) { return; } @@ -87,7 +87,7 @@ public static void EventSink_ResetProtectedAccount(AccountLoginEventArgs e) logger.Warning("Protected account \"{Username}\" has been reset.", username); - if (e.RejectReason is ALRReason.Blocked or ALRReason.BadPass or ALRReason.BadComm) + if (!e.Accepted && e.RejectReason is ALRReason.Blocked or ALRReason.BadPass or ALRReason.BadComm) { e.Accepted = true; } diff --git a/Projects/UOContent/Network/Packets/AccountLoginEventArgs.cs b/Projects/UOContent/Network/Packets/AccountLoginEventArgs.cs new file mode 100644 index 0000000000..dd556f9f02 --- /dev/null +++ b/Projects/UOContent/Network/Packets/AccountLoginEventArgs.cs @@ -0,0 +1,21 @@ +namespace Server.Network; + +public class AccountLoginEventArgs +{ + public AccountLoginEventArgs(NetState state, string username, string password) + { + State = state; + Username = username; + Password = password; + } + + public NetState State { get; } + + public string Username { get; } + + public string Password { get; } + + public bool Accepted { get; set; } + + public ALRReason RejectReason { get; set; } +} diff --git a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs index a6a87ae809..6e2d658de7 100644 --- a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs @@ -17,6 +17,8 @@ using System.Buffers; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using Server.Accounting; using Server.Assistants; using Server.Commands; @@ -496,11 +498,39 @@ public static void AccountLogin(NetState state, SpanReader reader) var username = reader.ReadAscii(30); var password = reader.ReadAscii(30); - var accountLoginEventArgs = new AccountLoginEventArgs(state, username, password); + var loginEventArgs = new AccountLoginEventArgs(state, username, password); - EventSink.InvokeAccountLogin(accountLoginEventArgs); + // Fire and forget the async login + Task.Run(() => + { + AccountLoginAsync(loginEventArgs); + } + ).ConfigureAwait(false); + } + + private static void AccountLoginAsync(AccountLoginEventArgs e) + { + // Replace with your async/await logic + Core.LoopContext.Send( + o => + { + var loginEventArgs = (AccountLoginEventArgs)o; + AccountHandler.AccountLogin(loginEventArgs); + ServerAccess.ResetProtectedAccount(loginEventArgs); + }, e); + + // Keep this to ensure the response is sent on the correct thread at the end of the async work + Core.LoopContext.Post( + o => + { + SendLoginResponse((AccountLoginEventArgs)o); + }, e); + } - if (accountLoginEventArgs.Accepted) + private static void SendLoginResponse(AccountLoginEventArgs e) + { + var state = e.State; + if (e.Accepted) { var serverListEventArgs = new ServerListEventArgs(state, state.Account); @@ -520,7 +550,7 @@ public static void AccountLogin(NetState state, SpanReader reader) else { state.Account = null; - AccountLogin_ReplyRej(state, accountLoginEventArgs.RejectReason); + AccountLogin_ReplyRej(state, e.RejectReason); } }