Fixes gateway support
This commit is contained in:
parent
fde85a5f7c
commit
e2dce0ded6
7 changed files with 138 additions and 65 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<PushedSession>("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<AdminCommandData>("AdminCommand", command =>
|
||||
{
|
||||
Core.LoopContext.Post(() => GatewayAdminHandler.HandleCommand(command));
|
||||
});
|
||||
// Register handlers -- all marshal to game thread
|
||||
_hubConnection.On<PushedSession>("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<AdminCommandData>("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)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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<GameLoginResponse?> 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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -37,8 +37,7 @@ public static class GatewayHeartbeat
|
|||
// Send first heartbeat immediately
|
||||
SendHeartbeat();
|
||||
|
||||
// Connect SignalR if enabled
|
||||
if (GatewayConfig.SignalREnabled)
|
||||
// Connect SignalR
|
||||
{
|
||||
_ = GatewayClient.ConnectSignalRAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles login acceptance for the deferred path (after async network validation).
|
||||
/// Sends packets directly since the original packet handler has already returned.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue