LANCommander/LANCommander.Server.Services/ServerEngines/RemoteServerEngine.cs

273 lines
9.4 KiB
C#

using System.Net.Http.Json;
using System.Text.Json;
using LANCommander.SDK.Models;
using LANCommander.Server.Services.Abstractions;
using LANCommander.Server.Services.Enums;
using LANCommander.Server.Services.Models;
using LANCommander.Server.Settings.Enums;
using LANCommander.Server.Settings.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace LANCommander.Server.Services.ServerEngines;
public class RemoteServerEngine(
ILogger<RemoteServerEngine> logger,
SettingsProvider<Settings.Settings> settingsProvider,
IServiceProvider serviceProvider) : IServerEngine
{
private record RemoteServerInfo(Guid HostId, Guid RemoteServerId);
private readonly Dictionary<Guid, RemoteServerInfo> _tracked = new();
private readonly Dictionary<Guid, HttpClient> _clients = new();
private readonly Dictionary<Guid, ServerProcessStatus> _status = new();
private Timer _pollTimer;
public event EventHandler<ServerStatusUpdateEventArgs>? OnServerStatusUpdate;
public event EventHandler<ServerLogEventArgs>? OnServerLog;
public async Task InitializeAsync()
{
foreach (var config in settingsProvider.CurrentValue.Server.GameServers.ServerEngines
.Where(e => e.Type == ServerEngine.Remote))
{
if (!string.IsNullOrWhiteSpace(config.Address) &&
Uri.TryCreate(config.Address, UriKind.Absolute, out _))
{
_clients[config.Id] = CreateHttpClient(config);
}
}
using var scope = serviceProvider.CreateScope();
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
var servers = await serverService.GetAsync(s => s.Engine == ServerEngine.Remote);
foreach (var server in servers)
{
if (server.RemoteHostId.HasValue && server.RemoteServerId.HasValue &&
_clients.ContainsKey(server.RemoteHostId.Value))
{
_tracked[server.Id] = new RemoteServerInfo(server.RemoteHostId.Value, server.RemoteServerId.Value);
}
}
_pollTimer = new Timer(PollStatus, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
}
public bool IsManaging(Guid serverId) => _tracked.ContainsKey(serverId);
public async Task StartAsync(Guid serverId)
{
if (!_tracked.ContainsKey(serverId))
throw new Exception("Server is not being tracked by this engine.");
var info = _tracked[serverId];
await EnsureTokenValidAsync(info.HostId);
var client = _clients[info.HostId];
await client.PostAsync($"api/Server/{info.RemoteServerId}/Start", null);
}
public async Task StopAsync(Guid serverId)
{
if (!_tracked.ContainsKey(serverId))
throw new Exception("Server is not being tracked by this engine.");
var info = _tracked[serverId];
await EnsureTokenValidAsync(info.HostId);
var client = _clients[info.HostId];
await client.PostAsync($"api/Server/{info.RemoteServerId}/Stop", null);
}
public async Task<ServerProcessStatus> GetStatusAsync(Guid serverId)
{
if (!_tracked.ContainsKey(serverId))
return ServerProcessStatus.Stopped;
var info = _tracked[serverId];
try
{
await EnsureTokenValidAsync(info.HostId);
var client = _clients[info.HostId];
var response = await client.GetAsync($"api/Server/{info.RemoteServerId}/Status");
if (!response.IsSuccessStatusCode)
return ServerProcessStatus.Stopped;
var status = await response.Content.ReadFromJsonAsync<ServerProcessStatus>();
return status;
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Could not retrieve status for remote server {ServerId}", serverId);
return ServerProcessStatus.Stopped;
}
}
public async Task<IEnumerable<SDK.Models.Server>> GetRemoteServersAsync(Guid hostId)
{
if (!_clients.TryGetValue(hostId, out var client))
return [];
try
{
await EnsureTokenValidAsync(hostId);
var servers = await client.GetFromJsonAsync<IEnumerable<SDK.Models.Server>>("api/Server/");
return servers ?? [];
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Could not retrieve servers from remote host {HostId}", hostId);
return [];
}
}
public async Task<string> AuthenticateAsync(Guid hostId, string username, string password)
{
var config = settingsProvider.CurrentValue.Server.GameServers.ServerEngines
.FirstOrDefault(e => e.Id == hostId);
if (config == null)
return "Remote host configuration not found.";
if (!Uri.TryCreate(config.Address, UriKind.Absolute, out _))
return "Invalid address configured for remote host.";
try
{
var tempClient = CreateHttpClient(config, includeAuth: false);
var loginModel = new { Username = username, Password = password };
var response = await tempClient.PostAsJsonAsync("api/Auth/Login", loginModel);
if (!response.IsSuccessStatusCode)
return $"Authentication failed: {response.StatusCode}";
var token = await response.Content.ReadFromJsonAsync<AuthToken>();
if (token == null)
return "Authentication failed: empty response.";
settingsProvider.Update(s =>
{
var c = s.Server.GameServers.ServerEngines.FirstOrDefault(e => e.Id == hostId);
if (c == null)
return;
c.AccessToken = token.AccessToken;
c.RefreshToken = token.RefreshToken;
c.TokenExpiration = token.Expiration;
});
_clients[hostId] = CreateHttpClient(settingsProvider.CurrentValue.Server.GameServers.ServerEngines
.FirstOrDefault(e => e.Id == hostId) ?? config);
return string.Empty;
}
catch (Exception ex)
{
logger?.LogError(ex, "Error authenticating with remote host {HostId}", hostId);
return ex.Message;
}
}
private async Task EnsureTokenValidAsync(Guid hostId)
{
var config = settingsProvider.CurrentValue.Server.GameServers.ServerEngines
.FirstOrDefault(e => e.Id == hostId);
if (config == null)
return;
if (config.TokenExpiration > DateTime.UtcNow.AddMinutes(5))
return;
if (string.IsNullOrWhiteSpace(config.RefreshToken))
return;
try
{
var tempClient = CreateHttpClient(config, includeAuth: false);
var refreshPayload = new AuthToken
{
AccessToken = config.AccessToken,
RefreshToken = config.RefreshToken,
Expiration = config.TokenExpiration
};
var response = await tempClient.PostAsJsonAsync("api/Auth/Refresh", refreshPayload);
if (!response.IsSuccessStatusCode)
return;
var token = await response.Content.ReadFromJsonAsync<AuthToken>();
if (token == null)
return;
settingsProvider.Update(s =>
{
var c = s.Server.GameServers.ServerEngines.FirstOrDefault(e => e.Id == hostId);
if (c != null)
{
c.AccessToken = token.AccessToken;
c.RefreshToken = token.RefreshToken;
c.TokenExpiration = token.Expiration;
}
});
_clients[hostId] = CreateHttpClient(settingsProvider.CurrentValue.Server.GameServers.ServerEngines
.FirstOrDefault(e => e.Id == hostId) ?? config);
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Could not refresh token for remote host {HostId}", hostId);
}
}
private HttpClient CreateHttpClient(ServerEngineConfiguration config, bool includeAuth = true)
{
var client = new HttpClient();
client.BaseAddress = new Uri(config.Address.TrimEnd('/') + "/");
if (includeAuth && !string.IsNullOrWhiteSpace(config.AccessToken))
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", config.AccessToken);
return client;
}
private async void PollStatus(object? state)
{
foreach (var (serverId, info) in _tracked.ToList())
{
try
{
var newStatus = await GetStatusAsync(serverId);
if (!_status.TryGetValue(serverId, out var oldStatus) || oldStatus != newStatus)
{
_status[serverId] = newStatus;
using var scope = serviceProvider.CreateScope();
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
var server = await serverService.GetAsync(serverId);
if (server != null)
OnServerStatusUpdate?.Invoke(this, new ServerStatusUpdateEventArgs(server, newStatus));
}
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Error polling status for remote server {ServerId}", serverId);
}
}
}
}