210 lines
7.5 KiB
C#
210 lines
7.5 KiB
C#
using System;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json.Serialization;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.SignalR.Client;
|
|
using Server.Logging;
|
|
|
|
namespace Server.Systems.Gateway;
|
|
|
|
public static class GatewayClient
|
|
{
|
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(GatewayClient));
|
|
|
|
// REST client (always available as fallback)
|
|
private static HttpClient _httpClient;
|
|
|
|
// SignalR client (optional, preferred when connected)
|
|
private static HubConnection _hubConnection;
|
|
|
|
public static bool IsReady => _httpClient != null;
|
|
public static bool IsSignalRConnected => _hubConnection?.State == HubConnectionState.Connected;
|
|
|
|
public static void Initialize()
|
|
{
|
|
if (!GatewayConfig.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// REST client (always configured)
|
|
_httpClient = new HttpClient
|
|
{
|
|
BaseAddress = new Uri(GatewayConfig.GatewayUrl.TrimEnd('/')),
|
|
Timeout = TimeSpan.FromSeconds(10)
|
|
};
|
|
_httpClient.DefaultRequestHeaders.Add("Authorization", $"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);
|
|
|
|
// 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 =>
|
|
{
|
|
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[]
|
|
{
|
|
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 =>
|
|
{
|
|
Core.LoopContext.Post(() => GatewaySessionStore.Add(session), EventLoopContext.Priority.High);
|
|
});
|
|
|
|
_hubConnection.On<AdminCommandData>("AdminCommand", command =>
|
|
{
|
|
Core.LoopContext.Post(() => GatewayAdminHandler.HandleCommand(command));
|
|
});
|
|
|
|
_hubConnection.On("Ping", () =>
|
|
{
|
|
// No-op, connection keep-alive handled by SignalR internally
|
|
});
|
|
|
|
_hubConnection.Reconnected += connectionId =>
|
|
{
|
|
logger.Information("SignalR reconnected to gateway (ConnectionId: {Id})", connectionId);
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
_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>
|
|
/// Connects the SignalR hub. Called after server is fully loaded.
|
|
/// </summary>
|
|
public static async Task ConnectSignalRAsync()
|
|
{
|
|
if (_hubConnection == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
await _hubConnection.StartAsync();
|
|
logger.Information("SignalR connected to gateway at {Url}", GatewayConfig.GatewayUrl);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Warning("SignalR connection to gateway failed: {Message}. Will retry automatically.", ex.Message);
|
|
}
|
|
}
|
|
|
|
// --- SignalR methods ---
|
|
|
|
public static Task SendHeartbeatAsync(HeartbeatRequest data) =>
|
|
_hubConnection?.InvokeAsync("Heartbeat", new { data.PlayerCount, 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)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
var result = await _hubConnection.InvokeAsync<SessionValidationResult>("ValidateSession", authId);
|
|
if (result == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new GameLoginResponse(result.Valid, result.Reason, result.GameAccountId, result.AccessLevel, result.ClientVersion);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Warning("SignalR ValidateSession failed: {Message}", ex.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// --- REST methods (fallback) ---
|
|
|
|
public static Task<HttpResponseMessage> PostAsJsonAsync<T>(string path, T content) =>
|
|
_httpClient.PostAsJsonAsync(path, content);
|
|
|
|
// --- Types ---
|
|
|
|
public record HeartbeatRequest(
|
|
[property: JsonPropertyName("playerCount")] int PlayerCount,
|
|
[property: JsonPropertyName("isOnline")] bool IsOnline
|
|
);
|
|
|
|
public record GameLoginRequest(
|
|
[property: JsonPropertyName("authId")] int AuthId,
|
|
[property: JsonPropertyName("username")] string Username,
|
|
[property: JsonPropertyName("password")] string Password
|
|
);
|
|
|
|
public record GameLoginResponse(
|
|
[property: JsonPropertyName("accepted")] bool Accepted,
|
|
[property: JsonPropertyName("reason")] string? Reason,
|
|
[property: JsonPropertyName("accountId")] Guid? AccountId,
|
|
[property: JsonPropertyName("accessLevel")] string? AccessLevel,
|
|
[property: JsonPropertyName("clientVersion")] string? ClientVersion
|
|
);
|
|
|
|
public record AdminCommandData(
|
|
[property: JsonPropertyName("type")] string Type,
|
|
[property: JsonPropertyName("payload")] string Payload
|
|
);
|
|
|
|
public record SessionValidationResult(
|
|
[property: JsonPropertyName("valid")] bool Valid,
|
|
[property: JsonPropertyName("gameAccountId")] Guid? GameAccountId,
|
|
[property: JsonPropertyName("accessLevel")] string? AccessLevel,
|
|
[property: JsonPropertyName("reason")] string? Reason,
|
|
[property: JsonPropertyName("clientVersion")] string? ClientVersion
|
|
);
|
|
}
|