Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds support for AsyncLogin #1791

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 0 additions & 48 deletions Projects/Server/Events/AccountLoginEvent.cs

This file was deleted.

91 changes: 45 additions & 46 deletions Projects/UOContent/Accounting/AccountHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Net;
using Server.Accounting;
Expand All @@ -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<IPAddress, int> m_IPTable;

private static char[] m_ForbiddenChars = { '<', '>', ':', '"', '/', '\\', '|', '?', '*' };
private static readonly SearchValues<char> _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<char> _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; }

Expand All @@ -47,7 +63,7 @@ public static Dictionary<IPAddress, int> 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;
}
}
}
Expand All @@ -58,6 +74,7 @@ public static Dictionary<IPAddress, int> 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(
Expand All @@ -73,7 +90,6 @@ public static void Configure()

public static void Initialize()
{
EventSink.AccountLogin += EventSink_AccountLogin;
EventSink.GameLogin += EventSink_GameLogin;
}

Expand Down Expand Up @@ -262,41 +278,26 @@ 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)
{
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;
}
Expand All @@ -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)
{
Expand All @@ -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);
}
}

Expand Down
18 changes: 9 additions & 9 deletions Projects/UOContent/Misc/ServerAccess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
21 changes: 21 additions & 0 deletions Projects/UOContent/Network/Packets/AccountLoginEventArgs.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
38 changes: 34 additions & 4 deletions Projects/UOContent/Network/Packets/IncomingAccountPackets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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);
}
}

Expand Down
Loading