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 (fast path)", state, username); // Set client version from the pushed session before the packet handler sends the response if (state.Version == null && session.ClientVersion != null) { state.Version = new ClientVersion(session.ClientVersion); } state.Version ??= ClientVersion.Version70654; 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 deferred accept logic (sends packets directly) logger.Information("Login: {NetState} Account '{Username}' at character list (via Gateway, deferred path)", state, username); AcceptLoginDeferred(state, username, response.AccessLevel, response.ClientVersion); } /// /// 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(); // Sync real account state back to gateway _ = GatewayClient.SyncAccountStateAsync(username, acct.AccessLevel.ToString(), acct.Banned); } /// /// 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, string clientVersion = null) { 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 // The game server connection doesn't have the client version (0xEF goes to the login server, // not the game server). Set it from the gateway session data, or fall back to latest known. if (state.Version == null) { state.Version = clientVersion != null ? new ClientVersion(clientVersion) : ClientVersion.Version70654; } state.CompressionEnabled = true; state.SendSupportedFeature(); state.SendCharacterList(); // Sync the real account state back to the gateway (access level, ban status). // This ensures the gateway DB reflects the game server's actual state, // especially for accounts that existed before the gateway was deployed. _ = GatewayClient.SyncAccountStateAsync(username, acct.AccessLevel.ToString(), acct.Banned); } /// /// 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; } }