From e2dce0ded6e422d7851cac49467305fa6fa3411a Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:24:39 -0700 Subject: [PATCH] Fixes gateway support --- .../UOContent/Accounting/AccountHandler.cs | 6 + .../Network/Packets/IncomingAccountPackets.cs | 35 +++-- .../Systems/Gateway/GatewayClient.cs | 121 +++++++++++------- .../Systems/Gateway/GatewayConfig.cs | 2 - .../Systems/Gateway/GatewayHeartbeat.cs | 3 +- .../Systems/Gateway/GatewayLoginHandler.cs | 33 ++++- .../Systems/Gateway/GatewaySessionStore.cs | 3 +- 7 files changed, 138 insertions(+), 65 deletions(-) diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index 1e676ed9e..266b2d786 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -330,6 +330,12 @@ public static class AccountHandler [OnEvent(nameof(GameServer.GameServerLoginEvent))] public static void OnGameServerLogin(GameServer.GameLoginEventArgs e) { + // When gateway is enabled, the GatewayLoginHandler handles login validation. + if (Systems.Gateway.GatewayConfig.Enabled) + { + return; + } + var un = e.Username; var pw = e.Password; diff --git a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs index c9b9f71ea..7c63bcf62 100644 --- a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs @@ -360,22 +360,33 @@ public static class IncomingAccountPackets var authId = reader.ReadInt32(); - if (!_authIDWindow.TryGetValue(authId, out var ap)) + if (Systems.Gateway.GatewayConfig.Enabled) { - state.LogInfo("Invalid client detected, disconnecting..."); - state.Disconnect("Unable to find auth id."); + // Gateway mode: AuthId was generated by the gateway, not by this server. + // Skip the local _authIDWindow and seed checks -- the gateway handler validates the AuthId. + state.AuthId = authId; + state.Seeded = true; } - - if (state.AuthId != 0 && authId != state.AuthId || state.AuthId == 0 && authId != state.Seed) + else { - state.LogInfo("Invalid client detected, disconnecting..."); - state.Disconnect("Invalid auth id in game login packet."); - return; - } + if (!_authIDWindow.TryGetValue(authId, out var ap)) + { + state.LogInfo("Invalid client detected, disconnecting..."); + state.Disconnect("Unable to find auth id."); + return; + } - _authIDWindow.Remove(authId); - state.Version = ap.Version; - state.Seeded = true; + if (state.AuthId != 0 && authId != state.AuthId || state.AuthId == 0 && authId != state.Seed) + { + state.LogInfo("Invalid client detected, disconnecting..."); + state.Disconnect("Invalid auth id in game login packet."); + return; + } + + _authIDWindow.Remove(authId); + state.Version = ap.Version; + state.Seeded = true; + } var username = reader.ReadLatin1Safe(30); var password = reader.ReadLatin1Safe(30); diff --git a/Projects/UOContent/Systems/Gateway/GatewayClient.cs b/Projects/UOContent/Systems/Gateway/GatewayClient.cs index 8fc524d10..21159930a 100644 --- a/Projects/UOContent/Systems/Gateway/GatewayClient.cs +++ b/Projects/UOContent/Systems/Gateway/GatewayClient.cs @@ -21,7 +21,7 @@ public static class GatewayClient public static bool IsReady => _httpClient != null; public static bool IsSignalRConnected => _hubConnection?.State == HubConnectionState.Connected; - public static void Configure() + public static void Initialize() { if (!GatewayConfig.Enabled) { @@ -36,53 +36,76 @@ public static class GatewayClient }; _httpClient.DefaultRequestHeaders.Add("Authorization", $"ApiKey {GatewayConfig.ApiKey}"); - // SignalR client (optional) - if (GatewayConfig.SignalREnabled) - { - var hubUrl = $"{GatewayConfig.GatewayUrl.TrimEnd('/')}/hubs/gameserver?apiKey={GatewayConfig.ApiKey}"; + logger.Information("Gateway API key configured: length={Length}, first4='{First4}', last4='{Last4}'", + GatewayConfig.ApiKey.Length, + GatewayConfig.ApiKey.Length >= 4 ? GatewayConfig.ApiKey[..4] : GatewayConfig.ApiKey, + GatewayConfig.ApiKey.Length >= 4 ? GatewayConfig.ApiKey[^4..] : GatewayConfig.ApiKey); - _hubConnection = new HubConnectionBuilder() - .WithUrl(hubUrl) - .WithAutomaticReconnect(new[] + // SignalR client + var encodedApiKey = Uri.EscapeDataString(GatewayConfig.ApiKey); + var hubUrl = $"{GatewayConfig.GatewayUrl.TrimEnd('/')}/hubs/gameserver?apiKey={encodedApiKey}"; + + _hubConnection = new HubConnectionBuilder() + .WithUrl(hubUrl, options => + { + // Prefer IPv4 to avoid ::1 vs 127.0.0.1 mismatch in IP allowlists + options.HttpMessageHandlerFactory = handler => { - TimeSpan.Zero, - TimeSpan.FromSeconds(2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10), - TimeSpan.FromSeconds(30) - }) - .Build(); - - // Register handlers -- all marshal to game thread - _hubConnection.On("PushSession", session => + if (handler is SocketsHttpHandler socketsHandler) + { + socketsHandler.ConnectCallback = async (context, ct) => + { + var socket = new System.Net.Sockets.Socket( + System.Net.Sockets.AddressFamily.InterNetwork, + System.Net.Sockets.SocketType.Stream, + System.Net.Sockets.ProtocolType.Tcp); + socket.NoDelay = true; + await socket.ConnectAsync(context.DnsEndPoint, ct); + return new System.Net.Sockets.NetworkStream(socket, ownsSocket: true); + }; + } + return handler; + }; + }) + .WithAutomaticReconnect(new[] { - Core.LoopContext.Post(() => GatewaySessionStore.Add(session), EventLoopContext.Priority.High); - }); + TimeSpan.Zero, + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30) + }) + .Build(); - _hubConnection.On("AdminCommand", command => - { - Core.LoopContext.Post(() => GatewayAdminHandler.HandleCommand(command)); - }); + // Register handlers -- all marshal to game thread + _hubConnection.On("PushSession", session => + { + Core.LoopContext.Post(() => GatewaySessionStore.Add(session), EventLoopContext.Priority.High); + }); - _hubConnection.On("Ping", () => - { - // No-op, connection keep-alive handled by SignalR internally - }); + _hubConnection.On("AdminCommand", command => + { + Core.LoopContext.Post(() => GatewayAdminHandler.HandleCommand(command)); + }); - _hubConnection.Reconnected += connectionId => - { - logger.Information("SignalR reconnected to gateway (ConnectionId: {Id})", connectionId); - return Task.CompletedTask; - }; + _hubConnection.On("Ping", () => + { + // No-op, connection keep-alive handled by SignalR internally + }); - _hubConnection.Closed += error => - { - logger.Warning("SignalR connection to gateway closed: {Message}", error?.Message ?? "clean disconnect"); - return Task.CompletedTask; - }; + _hubConnection.Reconnected += connectionId => + { + logger.Information("SignalR reconnected to gateway (ConnectionId: {Id})", connectionId); + return Task.CompletedTask; + }; - // Connect after server starts (via EventSink.ServerStarted) - } + _hubConnection.Closed += error => + { + logger.Warning("SignalR connection to gateway closed: {Message}", error?.Message ?? "clean disconnect"); + return Task.CompletedTask; + }; + + // Connect after server starts (via EventSink.ServerStarted) } /// @@ -112,6 +135,16 @@ public static class GatewayClient _hubConnection?.InvokeAsync("Heartbeat", new { data.PlayerCount, data.MaxPlayers, data.IsOnline }) ?? Task.CompletedTask; + public static Task SyncAccountStateAsync(string username, string accessLevel, bool isBanned) + { + if (_hubConnection?.State != HubConnectionState.Connected) + { + return Task.CompletedTask; + } + + return _hubConnection.SendAsync("SyncAccountState", new { username, accessLevel, isBanned }); + } + public static async Task ValidateSessionAsync(int authId) { if (_hubConnection?.State != HubConnectionState.Connected) @@ -127,7 +160,7 @@ public static class GatewayClient return null; } - return new GameLoginResponse(result.Valid, result.Reason, result.GameAccountId, result.AccessLevel); + return new GameLoginResponse(result.Valid, result.Reason, result.GameAccountId, result.AccessLevel, result.ClientVersion); } catch (Exception ex) { @@ -159,7 +192,8 @@ public static class GatewayClient [property: JsonPropertyName("accepted")] bool Accepted, [property: JsonPropertyName("reason")] string? Reason, [property: JsonPropertyName("accountId")] Guid? AccountId, - [property: JsonPropertyName("accessLevel")] string? AccessLevel + [property: JsonPropertyName("accessLevel")] string? AccessLevel, + [property: JsonPropertyName("clientVersion")] string? ClientVersion ); public record AdminCommandData( @@ -171,6 +205,7 @@ public static class GatewayClient [property: JsonPropertyName("valid")] bool Valid, [property: JsonPropertyName("gameAccountId")] Guid? GameAccountId, [property: JsonPropertyName("accessLevel")] string? AccessLevel, - [property: JsonPropertyName("reason")] string? Reason + [property: JsonPropertyName("reason")] string? Reason, + [property: JsonPropertyName("clientVersion")] string? ClientVersion ); } diff --git a/Projects/UOContent/Systems/Gateway/GatewayConfig.cs b/Projects/UOContent/Systems/Gateway/GatewayConfig.cs index b6d2231f8..0b506fca3 100644 --- a/Projects/UOContent/Systems/Gateway/GatewayConfig.cs +++ b/Projects/UOContent/Systems/Gateway/GatewayConfig.cs @@ -18,7 +18,6 @@ public static class GatewayConfig public static string ApiKey { get; private set; } = ""; public static int HeartbeatIntervalSeconds { get; private set; } public static int MaxPlayers { get; private set; } - public static bool SignalREnabled { get; private set; } public static void Configure() { @@ -33,7 +32,6 @@ public static class GatewayConfig ApiKey = ServerConfiguration.GetOrUpdateSetting("gateway.apiKey", ""); HeartbeatIntervalSeconds = ServerConfiguration.GetOrUpdateSetting("gateway.heartbeatIntervalSeconds", 30); MaxPlayers = ServerConfiguration.GetOrUpdateSetting("gateway.maxPlayers", 200); - SignalREnabled = ServerConfiguration.GetOrUpdateSetting("gateway.signalREnabled", false); if (string.IsNullOrWhiteSpace(ApiKey)) { diff --git a/Projects/UOContent/Systems/Gateway/GatewayHeartbeat.cs b/Projects/UOContent/Systems/Gateway/GatewayHeartbeat.cs index 5c0020bcc..9db98c819 100644 --- a/Projects/UOContent/Systems/Gateway/GatewayHeartbeat.cs +++ b/Projects/UOContent/Systems/Gateway/GatewayHeartbeat.cs @@ -37,8 +37,7 @@ public static class GatewayHeartbeat // Send first heartbeat immediately SendHeartbeat(); - // Connect SignalR if enabled - if (GatewayConfig.SignalREnabled) + // Connect SignalR { _ = GatewayClient.ConnectSignalRAsync(); } diff --git a/Projects/UOContent/Systems/Gateway/GatewayLoginHandler.cs b/Projects/UOContent/Systems/Gateway/GatewayLoginHandler.cs index 3884e1a02..b8453538f 100644 --- a/Projects/UOContent/Systems/Gateway/GatewayLoginHandler.cs +++ b/Projects/UOContent/Systems/Gateway/GatewayLoginHandler.cs @@ -41,7 +41,13 @@ public static class GatewayLoginHandler // 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); + 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; } @@ -107,9 +113,9 @@ public static class GatewayLoginHandler 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); + // 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); } /// @@ -133,13 +139,16 @@ public static class GatewayLoginHandler 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) + private static void AcceptLoginDeferred(NetState state, string username, string accessLevel, string clientVersion = null) { var acct = FindOrCreateAccount(state, username, accessLevel); @@ -156,9 +165,23 @@ public static class GatewayLoginHandler 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); } /// diff --git a/Projects/UOContent/Systems/Gateway/GatewaySessionStore.cs b/Projects/UOContent/Systems/Gateway/GatewaySessionStore.cs index 251080cc7..29caf875d 100644 --- a/Projects/UOContent/Systems/Gateway/GatewaySessionStore.cs +++ b/Projects/UOContent/Systems/Gateway/GatewaySessionStore.cs @@ -18,7 +18,7 @@ public static class GatewaySessionStore public static void Configure() { - if (!GatewayConfig.Enabled || !GatewayConfig.SignalREnabled) + if (!GatewayConfig.Enabled) { return; } @@ -87,5 +87,6 @@ public record PushedSession( string Username, string AccessLevel, string ClientIP, + string? ClientVersion, DateTime ExpiresAt );