using System; using System.Net.Http.Json; using System.Threading.Tasks; using ModernUO.CodeGeneratedEvents; using Server.Accounting; using Server.Engines.CharacterCreation; using Server.Logging; using Server.Network; using Server.Tasks; namespace Server.Systems.Gateway; /// /// Intercepts the game server login event (0x91 packet) and validates against the Gateway API /// instead of using local account storage. /// /// Uses a three-tier approach: /// 1. Fast path: check local pushed session store (O(1), no network) -- sessions pushed via SignalR /// 2. SignalR path: validate session over SignalR hub connection /// 3. REST path: fallback HTTP validation /// /// When gateway.enabled is false, this handler returns immediately and the default /// AccountHandler processes the login as normal. /// public static class GatewayLoginHandler { private static readonly ILogger logger = LogFactory.GetLogger(typeof(GatewayLoginHandler)); [OnEvent(nameof(GameServer.GameServerLoginEvent))] public static void OnGameServerLogin(GameServer.GameLoginEventArgs e) { if (!GatewayConfig.Enabled || !GatewayClient.IsReady) { return; } var state = e.State; var username = e.Username; var authId = state.AuthId; // Fast path: check local pushed session store (O(1), no network) if (GatewaySessionStore.TryConsume(authId, out var session)) { logger.Information("Login: {NetState} Account '{Username}' accepted via pushed session", state, username); AcceptLogin(e, state, username, session.AccessLevel); return; } // Slow path: network validation (SignalR or REST) var password = e.Password; // Defer the login -- tells the packet handler to skip both accept and reject paths. e.Deferred = true; // HTTP call on background thread, result posted back to game thread. Task.Run(() => ValidateViaNetworkAsync(authId, username, password)) .ContinueWithOnGameThread(t => OnValidationComplete(t, state, username)); } private static async Task ValidateViaNetworkAsync( int authId, string username, string password) { // Try SignalR first if (GatewayClient.IsSignalRConnected) { var signalRResult = await GatewayClient.ValidateSessionAsync(authId); if (signalRResult != null) { return signalRResult; } } // Fall back to REST var request = new GatewayClient.GameLoginRequest(authId, username, password); var httpResponse = await GatewayClient.PostAsJsonAsync("/api/validate-game-login", request); httpResponse.EnsureSuccessStatusCode(); return await httpResponse.Content.ReadFromJsonAsync(); } /// /// Runs on the game thread after the background network call completes. /// private static void OnValidationComplete(Task task, NetState state, string username) { if (!state.Running) { return; // Client disconnected while we were validating } if (task.IsFaulted) { logger.Error(task.Exception, "Gateway login validation failed for '{Username}'", username); state.Disconnect($"Gateway validation error."); return; } var response = task.Result; if (response is not { Accepted: true }) { logger.Information( "Login: {NetState} Gateway rejected '{Username}': {Reason}", state, username, response?.Reason ?? "unknown"); state.Disconnect($"Gateway rejected login: {response?.Reason ?? "unknown"}"); return; } // Gateway accepted -- use common accept logic (deferred path) logger.Information("Login: {NetState} Account '{Username}' at character list (via Gateway)", state, username); AcceptLoginDeferred(state, username, response.AccessLevel); } /// /// Handles login acceptance for the fast path (non-deferred, from pushed session store). /// Sets event args so the packet handler sends the response. /// private static void AcceptLogin(GameServer.GameLoginEventArgs e, NetState state, string username, string accessLevel) { var acct = FindOrCreateAccount(state, username, accessLevel); if (acct.Banned) { logger.Information("Login: {NetState} Locally banned account '{Username}'", state, username); e.Accepted = false; return; } acct.LogAccess(state); state.Account = acct; state.CityInfo = CharacterCreation.GetStartingCities(); e.Accepted = true; e.CityInfo = CharacterCreation.GetStartingCities(); } /// /// Handles login acceptance for the deferred path (after async network validation). /// Sends packets directly since the original packet handler has already returned. /// private static void AcceptLoginDeferred(NetState state, string username, string accessLevel) { var acct = FindOrCreateAccount(state, username, accessLevel); if (acct.Banned) { logger.Information("Login: {NetState} Locally banned account '{Username}'", state, username); state.Disconnect("Account is locally banned."); return; } acct.LogAccess(state); state.Account = acct; state.CityInfo = CharacterCreation.GetStartingCities(); // Send the same packets that IncomingAccountPackets.GameLogin sends on e.Accepted = true state.CompressionEnabled = true; state.SendSupportedFeature(); state.SendCharacterList(); } /// /// Finds an existing local account or creates a new shell account. /// Gateway owns credentials; local account is for character storage only. /// private static Account FindOrCreateAccount(NetState state, string username, string accessLevel) { var acct = Accounts.GetAccount(username) as Account; if (acct == null) { acct = new Account(username, Guid.NewGuid().ToString()); if (Enum.TryParse(accessLevel, true, out var level)) { acct.AccessLevel = level; } logger.Information( "Login: {NetState} Created local account for '{Username}' (AccessLevel: {Level})", state, username, acct.AccessLevel); } return acct; } }