From 5c9ca801f8db80b59423fecd685bd537819c1849 Mon Sep 17 00:00:00 2001 From: Loporrit <141286461+loporrit@users.noreply.github.com> Date: Sat, 2 Aug 2025 09:34:26 +0000 Subject: [PATCH] Replace repo config with generic remote config --- .gitmodules | 3 + BunnyWhispers | 1 + .../Configurations/RemoteConfigCache.cs | 13 ++ .../RemoteConfigCacheService.cs | 11 + MareSynchronos/MareSynchronos.csproj | 1 + MareSynchronos/Plugin.cs | 3 + MareSynchronos/Services/NoSnapService.cs | 74 ++++--- .../Services/RemoteConfigurationService.cs | 202 ++++++++++++++++++ MareSynchronos/Services/RepoChangeService.cs | 92 +------- .../ServerConfigurationManager.cs | 9 + MareSynchronos/UI/IntroUI.cs | 2 +- .../WebAPI/Files/FileUploadManager.cs | 2 +- .../WebAPI/SignalR/HubConnectionConfig.cs | 3 + MareSynchronos/WebAPI/SignalR/HubFactory.cs | 25 ++- .../WebAPI/SignalR/TokenProvider.cs | 18 +- 15 files changed, 330 insertions(+), 129 deletions(-) create mode 160000 BunnyWhispers create mode 100644 MareSynchronos/MareConfiguration/Configurations/RemoteConfigCache.cs create mode 100644 MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs create mode 100644 MareSynchronos/Services/RemoteConfigurationService.cs diff --git a/.gitmodules b/.gitmodules index ab3d0db..95e93ac 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "Glamourer.Api"] path = Glamourer.Api url = https://github.com/loporrit/Glamourer.Api.git +[submodule "BunnyWhispers"] + path = BunnyWhispers + url = https://github.com/loporrit/BunnyWhispers.git diff --git a/BunnyWhispers b/BunnyWhispers new file mode 160000 index 0000000..ffb3896 --- /dev/null +++ b/BunnyWhispers @@ -0,0 +1 @@ +Subproject commit ffb3896b136caafab3fadb5cb5903cacce24f033 diff --git a/MareSynchronos/MareConfiguration/Configurations/RemoteConfigCache.cs b/MareSynchronos/MareConfiguration/Configurations/RemoteConfigCache.cs new file mode 100644 index 0000000..5ad3f9b --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/RemoteConfigCache.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Nodes; + +namespace MareSynchronos.MareConfiguration.Configurations; + +public class RemoteConfigCache : IMareConfiguration +{ + public int Version { get; set; } = 0; + public ulong Timestamp { get; set; } = 0; + public string Origin { get; set; } = string.Empty; + public DateTimeOffset? LastModified { get; set; } = null; + public string ETag { get; set; } = string.Empty; + public JsonObject Configuration { get; set; } = new(); +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs b/MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs new file mode 100644 index 0000000..66c7ff4 --- /dev/null +++ b/MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs @@ -0,0 +1,11 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class RemoteConfigCacheService : ConfigurationServiceBase +{ + public const string ConfigName = "remotecache.json"; + + public RemoteConfigCacheService(string configDir) : base(configDir) { } + public override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 5c7bb23..5b790ea 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -53,6 +53,7 @@ + diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 221d511..56cfc7e 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -153,6 +153,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new RemoteConfigCacheService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton>(s => s.GetRequiredService()); @@ -163,8 +164,10 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton>(s => s.GetRequiredService()); + collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); collection.AddSingleton(); diff --git a/MareSynchronos/Services/NoSnapService.cs b/MareSynchronos/Services/NoSnapService.cs index 5e4f6aa..5daa903 100644 --- a/MareSynchronos/Services/NoSnapService.cs +++ b/MareSynchronos/Services/NoSnapService.cs @@ -4,72 +4,71 @@ using MareSynchronos.MareConfiguration.Models; using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using System.Text.Json; +using System.Text.Json.Serialization; namespace MareSynchronos.Services; -public class NoSnapService : IHostedService, IMediatorSubscriber +public sealed class NoSnapService : IHostedService, IMediatorSubscriber { + private record NoSnapConfig + { + [JsonPropertyName("listOfPlugins")] + public string[]? ListOfPlugins { get; set; } + } + private readonly ILogger _logger; + private readonly IDalamudPluginInterface _pluginInterface; private readonly Dictionary _listOfPlugins = new(StringComparer.Ordinal) { ["Snapper"] = false, ["Snappy"] = false, - ["Meddle.Plugin"] = false + ["Meddle.Plugin"] = false, }; private static readonly HashSet _gposers = new(); private static readonly HashSet _gposersNamed = new(StringComparer.Ordinal); private readonly IHostApplicationLifetime _hostApplicationLifetime; private readonly DalamudUtilService _dalamudUtilService; private readonly IpcManager _ipcManager; + private readonly RemoteConfigurationService _remoteConfig; public static bool AnyLoaded { get; private set; } = false; public MareMediator Mediator { get; init; } - public NoSnapService(ILogger logger, IDalamudPluginInterface pi, MareMediator mediator, - IHostApplicationLifetime hostApplicationLifetime, DalamudUtilService dalamudUtilService, IpcManager ipcManager) + public NoSnapService(ILogger logger, IDalamudPluginInterface pluginInterface, MareMediator mediator, + IHostApplicationLifetime hostApplicationLifetime, DalamudUtilService dalamudUtilService, IpcManager ipcManager, + RemoteConfigurationService remoteConfig) { _logger = logger; + _pluginInterface = pluginInterface; Mediator = mediator; _hostApplicationLifetime = hostApplicationLifetime; _dalamudUtilService = dalamudUtilService; _ipcManager = ipcManager; - foreach (var pluginName in _listOfPlugins.Keys) - { - var plugin = pi.InstalledPlugins.FirstOrDefault(p => p.InternalName.Equals(pluginName, StringComparison.Ordinal)); - if (plugin?.IsLoaded ?? false) - _listOfPlugins[pluginName] = true; - Mediator.SubscribeKeyed(this, pluginName, (msg) => - { - _logger.LogInformation("{pluginName} isLoaded = {isLoaded}", pluginName, msg.IsLoaded); - _listOfPlugins[pluginName] = msg.IsLoaded; - Update(); - }); - } + _remoteConfig = remoteConfig; Mediator.Subscribe(this, msg => ClearGposeList()); Mediator.Subscribe(this, msg => ClearGposeList()); - - Update(); } public void AddGposer(int objectIndex) { if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested) { - _logger.LogInformation("Immediately reverting object index {id}", objectIndex); + _logger.LogTrace("Immediately reverting object index {id}", objectIndex); RevertAndRedraw(objectIndex); return; } - _logger.LogInformation("Registering gposer object index {id}", objectIndex); + _logger.LogTrace("Registering gposer object index {id}", objectIndex); lock (_gposers) _gposers.Add(objectIndex); } public void RemoveGposer(int objectIndex) { - _logger.LogInformation("Un-registering gposer object index {id}", objectIndex); + _logger.LogTrace("Un-registering gposer object index {id}", objectIndex); lock (_gposers) _gposers.Remove(objectIndex); } @@ -78,12 +77,12 @@ public class NoSnapService : IHostedService, IMediatorSubscriber { if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested) { - _logger.LogInformation("Immediately reverting {name}", name); + _logger.LogTrace("Immediately reverting {name}", name); RevertAndRedraw(name); return; } - _logger.LogInformation("Registering gposer {name}", name); + _logger.LogTrace("Registering gposer {name}", name); lock (_gposers) _gposersNamed.Add(name); } @@ -91,7 +90,7 @@ public class NoSnapService : IHostedService, IMediatorSubscriber private void ClearGposeList() { if (_gposers.Count > 0 || _gposersNamed.Count > 0) - _logger.LogInformation("Clearing gposer list"); + _logger.LogTrace("Clearing gposer list"); lock (_gposers) _gposers.Clear(); lock (_gposersNamed) @@ -170,9 +169,31 @@ public class NoSnapService : IHostedService, IMediatorSubscriber }).GetAwaiter().GetResult(); } - public Task StartAsync(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken cancellationToken) { - return Task.CompletedTask; + var config = await _remoteConfig.GetConfigAsync("noSnap").ConfigureAwait(false) ?? new(); + + if (config.ListOfPlugins != null) + { + _listOfPlugins.Clear(); + foreach (var pluginName in config.ListOfPlugins) + _listOfPlugins.TryAdd(pluginName, value: false); + } + + foreach (var pluginName in _listOfPlugins.Keys) + { + var plugin = _pluginInterface.InstalledPlugins.FirstOrDefault(p => p.InternalName.Equals(pluginName, StringComparison.Ordinal)); + if (plugin?.IsLoaded ?? false) + _listOfPlugins[pluginName] = true; + Mediator.SubscribeKeyed(this, pluginName, (msg) => + { + _listOfPlugins[pluginName] = msg.IsLoaded; + _logger.LogDebug("{pluginName} isLoaded = {isLoaded}", pluginName, msg.IsLoaded); + Update(); + }); + } + + Update(); } public Task StopAsync(CancellationToken cancellationToken) @@ -187,7 +208,6 @@ public class NoSnapService : IHostedService, IMediatorSubscriber if (AnyLoaded != anyLoadedNow) { - _logger.LogInformation("AnyLoaded is now {AnyLoaded}", AnyLoaded); AnyLoaded = anyLoadedNow; Mediator.Publish(new RecalculatePerformanceMessage(null)); diff --git a/MareSynchronos/Services/RemoteConfigurationService.cs b/MareSynchronos/Services/RemoteConfigurationService.cs new file mode 100644 index 0000000..cdd62c2 --- /dev/null +++ b/MareSynchronos/Services/RemoteConfigurationService.cs @@ -0,0 +1,202 @@ +using Chaos.NaCl; +using MareSynchronos.MareConfiguration; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace MareSynchronos.Services; + +public sealed class RemoteConfigurationService +{ + private readonly static Dictionary ConfigPublicKeys = new(StringComparer.Ordinal) + { + { "4D6633E0", "GWRoAiXP9lcn9/34wGgziYcqQH8f6zWtZrRyp66Ekso=" }, + }; + + private readonly static string[] ConfigSources = [ + "https://plugin.lop-sync.com/config/config.json", + "https://plugin.lop-sync.net/config/config.json", + ]; + + private readonly ILogger _logger; + private readonly RemoteConfigCacheService _configService; + private readonly Task _initTask; + + public RemoteConfigurationService(ILogger logger, RemoteConfigCacheService configService) + { + _logger = logger; + _configService = configService; + _initTask = Task.Run(DownloadConfig); + } + + public async Task GetConfigAsync(string sectionName) + { + await _initTask.ConfigureAwait(false); + if (!_configService.Current.Configuration.TryGetPropertyValue(sectionName, out var section)) + section = null; + return (section as JsonObject) ?? new(); + } + + public async Task GetConfigAsync(string sectionName) + { + try + { + var json = await GetConfigAsync(sectionName).ConfigureAwait(false); + return JsonSerializer.Deserialize(json); + } + catch (JsonException ex) + { + _logger.LogWarning(ex, "Invalid JSON in remote config: {sectionName}", sectionName); + return default; + } + } + + private async Task DownloadConfig() + { + string? jsonResponse = null; + + foreach (var remoteUrl in ConfigSources) + { + try + { + _logger.LogDebug("Fetching {url}", remoteUrl); + + using var httpClient = new HttpClient( + new HttpClientHandler + { + AllowAutoRedirect = true, + MaxAutomaticRedirections = 5 + } + ); + + httpClient.Timeout = TimeSpan.FromSeconds(6); + + var ver = Assembly.GetExecutingAssembly().GetName().Version; + httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); + + var request = new HttpRequestMessage(HttpMethod.Get, remoteUrl); + + if (remoteUrl.Equals(_configService.Current.Origin, StringComparison.Ordinal)) + { + if (!string.IsNullOrEmpty(_configService.Current.ETag)) + request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(_configService.Current.ETag)); + + if (_configService.Current.LastModified != null) + request.Headers.IfModifiedSince = _configService.Current.LastModified; + } + + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + + if (response.StatusCode == HttpStatusCode.NotModified) + { + _logger.LogDebug("Using cached remote configuration from {url}", remoteUrl); + return; + } + + response.EnsureSuccessStatusCode(); + + var contentType = response.Content.Headers.ContentType?.MediaType; + + if (contentType == null || !contentType.Equals("application/json", StringComparison.Ordinal)) + { + _logger.LogWarning("HTTP request for remote config failed: wrong MIME type"); + continue; + } + + _logger.LogInformation("Downloaded new configuration from {url}", remoteUrl); + + _configService.Current.Origin = remoteUrl; + _configService.Current.ETag = response.Headers.ETag?.ToString() ?? string.Empty; + + try + { + if (response.Content.Headers.Contains("Last-Modified")) + { + var lastModified = response.Content.Headers.GetValues("Last-Modified").First(); + _configService.Current.LastModified = DateTimeOffset.Parse(lastModified, System.Globalization.CultureInfo.InvariantCulture); + } + } + catch + { + _configService.Current.LastModified = null; + } + + jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + break; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "HTTP request for remote config failed"); + + if (remoteUrl.Equals(_configService.Current.Origin, StringComparison.Ordinal)) + { + _configService.Current.ETag = string.Empty; + _configService.Current.LastModified = null; + _configService.Save(); + } + } + } + + if (jsonResponse == null) + { + _logger.LogWarning("Could not download remote config"); + return; + } + + try + { + var jsonDoc = JsonNode.Parse(jsonResponse) as JsonObject; + + if (jsonDoc == null) + { + _logger.LogWarning("Downloaded remote config is not a JSON object"); + return; + } + + LoadConfig(jsonDoc); + } + catch (JsonException ex) + { + _logger.LogWarning(ex, "Invalid JSON in remote config response"); + } + } + + private static bool VerifySignature(string message, ulong ts, string signature, string pubKey) + { + byte[] msg = [.. BitConverter.GetBytes(ts), .. Encoding.UTF8.GetBytes(message)]; + byte[] sig = Convert.FromBase64String(signature); + byte[] pub = Convert.FromBase64String(pubKey); + return Ed25519.Verify(sig, msg, pub); + } + + private void LoadConfig(JsonObject jsonDoc) + { + var ts = jsonDoc["ts"]!.GetValue(); + + if (ts <= _configService.Current.Timestamp) + { + _logger.LogDebug("Remote configuration is not newer than cached config"); + return; + } + + var signatures = jsonDoc["sig"]!.AsObject(); + var configString = jsonDoc["config"]!.GetValue(); + bool verified = signatures.Any(sig => + ConfigPublicKeys.TryGetValue(sig.Key, out var pubKey) && + VerifySignature(configString, ts, sig.Value!.GetValue(), pubKey)); + + if (!verified) + { + _logger.LogWarning("Could not verify signature for downloaded remote config"); + return; + } + + _configService.Current.Configuration = JsonNode.Parse(configString)!.AsObject(); + _configService.Current.Timestamp = ts; + _configService.Save(); + } +} diff --git a/MareSynchronos/Services/RepoChangeService.cs b/MareSynchronos/Services/RepoChangeService.cs index 2749b01..fb17aca 100644 --- a/MareSynchronos/Services/RepoChangeService.cs +++ b/MareSynchronos/Services/RepoChangeService.cs @@ -1,4 +1,3 @@ - using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -6,7 +5,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Net.Http.Headers; using System.Reflection; -using System.Runtime.InteropServices; using System.Text.Json; namespace MareSynchronos.Services; @@ -215,12 +213,14 @@ public sealed class RepoChangeService : IHostedService #endregion private readonly ILogger _logger; + private readonly RemoteConfigurationService _remoteConfig; private readonly IDalamudPluginInterface _pluginInterface; private readonly IFramework _framework; - public RepoChangeService(ILogger logger, IDalamudPluginInterface pluginInterface, IFramework framework) + public RepoChangeService(ILogger logger, RemoteConfigurationService remoteConfig, IDalamudPluginInterface pluginInterface, IFramework framework) { _logger = logger; + _remoteConfig = remoteConfig; _pluginInterface = pluginInterface; _framework = framework; } @@ -228,10 +228,7 @@ public sealed class RepoChangeService : IHostedService public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogDebug("Starting RepoChange Service"); - var repoChangeConfig = await DownloadRepoChangeConfig().ConfigureAwait(false); - - if (repoChangeConfig == null) - return; + var repoChangeConfig = await _remoteConfig.GetConfigAsync("repoChange").ConfigureAwait(false) ?? new(); var currentRepo = repoChangeConfig.CurrentRepo; var validRepos = (repoChangeConfig.ValidRepos ?? []).ToList(); @@ -403,85 +400,4 @@ public sealed class RepoChangeService : IHostedService _logger.LogDebug("Stopping RepoChange Service"); return Task.CompletedTask; } - - private async Task DownloadRepoChangeConfig() - { - string[] repoChangeSources = [ - "https://plugin.lop-sync.com/repochange/config.json", - "https://plugin.lop-sync.net/repochange/config.json", - ]; - - string? jsonResponse = null; - - foreach (var repoChangeUrl in repoChangeSources) - { - try - { - _logger.LogTrace("Fetching {url}", repoChangeUrl); - - using var httpClient = new HttpClient( - new HttpClientHandler - { - AllowAutoRedirect = true, - MaxAutomaticRedirections = 5 - } - ); - - var ver = Assembly.GetExecutingAssembly().GetName().Version; - httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); - - var response = await httpClient.GetAsync(repoChangeUrl).ConfigureAwait(false); - - response.EnsureSuccessStatusCode(); - - var contentType = response.Content.Headers.ContentType?.MediaType; - - if (contentType == null || !contentType.Equals("application/json", StringComparison.Ordinal)) - { - _logger.LogWarning("HTTP request for RepoChange config failed: wrong MIME type"); - continue; - } - - jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - break; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "HTTP request for RepoChange config failed"); - } - } - - if (jsonResponse == null) - { - _logger.LogWarning("Could not download RepoChange config"); - return null; - } - - try - { - var config = JsonSerializer.Deserialize( - jsonResponse, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - AllowTrailingCommas = true - } - ); - - if (config == null) - { - _logger.LogWarning("Deserialization of RepoChange config returned null"); - return null; - } - - config.ValidRepos ??= []; - - return config; - } - catch (JsonException ex) - { - _logger.LogWarning(ex, "Invalid JSON in RepoChange config response"); - return null; - } - } } diff --git a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs index 7efc289..e63f3c3 100644 --- a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs +++ b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs @@ -20,6 +20,7 @@ public class ServerConfigurationManager private HashSet? _cachedWhitelistedUIDs = null; private HashSet? _cachedBlacklistedUIDs = null; + private string? _realApiUrl = null; public ServerConfigurationManager(ILogger logger, ServerConfigService configService, ServerTagConfigService serverTagConfig, SyncshellConfigService syncshellConfig, NotesConfigService notesConfig, @@ -36,6 +37,13 @@ public class ServerConfigurationManager } public string CurrentApiUrl => CurrentServer.ServerUri; + public string CurrentRealApiUrl + { + get + { + return _realApiUrl ?? CurrentApiUrl; + } + } public ServerStorage CurrentServer => _configService.Current.ServerStorage[CurrentServerIndex]; public IReadOnlyList Whitelist => CurrentBlockStorage().Whitelist; @@ -48,6 +56,7 @@ public class ServerConfigurationManager _configService.Current.CurrentServer = value; _cachedWhitelistedUIDs = null; _cachedBlacklistedUIDs = null; + _realApiUrl = null; _configService.Save(); } get diff --git a/MareSynchronos/UI/IntroUI.cs b/MareSynchronos/UI/IntroUI.cs index 8caa35a..ca8055b 100644 --- a/MareSynchronos/UI/IntroUI.cs +++ b/MareSynchronos/UI/IntroUI.cs @@ -264,7 +264,7 @@ This service is provided as-is. using HttpClient httpClient = new(); var ver = Assembly.GetExecutingAssembly().GetName().Version; httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); - var postUri = MareAuth.AuthRegisterFullPath(new Uri(_serverConfigurationManager.CurrentApiUrl + var postUri = MareAuth.AuthRegisterFullPath(new Uri(_serverConfigurationManager.CurrentRealApiUrl .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) .Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase))); _logger.LogInformation("Registering new account: {uri}", postUri.ToString()); diff --git a/MareSynchronos/WebAPI/Files/FileUploadManager.cs b/MareSynchronos/WebAPI/Files/FileUploadManager.cs index ced2615..10b7f1e 100644 --- a/MareSynchronos/WebAPI/Files/FileUploadManager.cs +++ b/MareSynchronos/WebAPI/Files/FileUploadManager.cs @@ -108,7 +108,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase _uploadCancellationTokenSource = new CancellationTokenSource(); var uploadToken = _uploadCancellationTokenSource.Token; - Logger.LogDebug("Sending Character data {hash} to service {url}", data.DataHash.Value, _serverManager.CurrentApiUrl); + Logger.LogDebug("Sending Character data {hash} to service {url}", data.DataHash.Value, _serverManager.CurrentRealApiUrl); HashSet unverifiedUploads = GetUnverifiedFiles(data); if (unverifiedUploads.Any()) diff --git a/MareSynchronos/WebAPI/SignalR/HubConnectionConfig.cs b/MareSynchronos/WebAPI/SignalR/HubConnectionConfig.cs index ced9bbd..f7eb825 100644 --- a/MareSynchronos/WebAPI/SignalR/HubConnectionConfig.cs +++ b/MareSynchronos/WebAPI/SignalR/HubConnectionConfig.cs @@ -5,6 +5,9 @@ namespace MareSynchronos.WebAPI.SignalR; public record HubConnectionConfig { + [JsonPropertyName("api_url")] + public string ApiUrl { get; set; } = string.Empty; + [JsonPropertyName("hub_url")] public string HubUrl { get; set; } = string.Empty; diff --git a/MareSynchronos/WebAPI/SignalR/HubFactory.cs b/MareSynchronos/WebAPI/SignalR/HubFactory.cs index a48ddd8..25e1086 100644 --- a/MareSynchronos/WebAPI/SignalR/HubFactory.cs +++ b/MareSynchronos/WebAPI/SignalR/HubFactory.cs @@ -1,4 +1,5 @@ using MareSynchronos.API.SignalR; +using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.WebAPI.SignalR.Utils; @@ -18,6 +19,7 @@ public class HubFactory : MediatorSubscriberBase { private readonly ILoggerProvider _loggingProvider; private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly RemoteConfigurationService _remoteConfig; private readonly TokenProvider _tokenProvider; private HubConnection? _instance; private string _cachedConfigFor = string.Empty; @@ -25,10 +27,11 @@ public class HubFactory : MediatorSubscriberBase private bool _isDisposed = false; public HubFactory(ILogger logger, MareMediator mediator, - ServerConfigurationManager serverConfigurationManager, + ServerConfigurationManager serverConfigurationManager, RemoteConfigurationService remoteConfig, TokenProvider tokenProvider, ILoggerProvider pluginLog) : base(logger, mediator) { _serverConfigurationManager = serverConfigurationManager; + _remoteConfig = remoteConfig; _tokenProvider = tokenProvider; _loggingProvider = pluginLog; } @@ -85,7 +88,12 @@ public class HubFactory : MediatorSubscriberBase } if (_serverConfigurationManager.CurrentApiUrl.Equals(ApiController.LoporritServiceUri, StringComparison.Ordinal)) - defaultConfig.HubUrl = ApiController.LoporritServiceHubUri; + { + var mainServerConfig = await _remoteConfig.GetConfigAsync("mainServer").ConfigureAwait(false) ?? new(); + defaultConfig = mainServerConfig; + if (string.IsNullOrEmpty(mainServerConfig.HubUrl)) + defaultConfig.HubUrl = ApiController.LoporritServiceHubUri; + } string jsonResponse; @@ -140,21 +148,18 @@ public class HubFactory : MediatorSubscriberBase try { - var config = JsonSerializer.Deserialize( - jsonResponse, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - AllowTrailingCommas = true - }); + var config = JsonSerializer.Deserialize(jsonResponse); if (config == null) return defaultConfig; + if (string.IsNullOrEmpty(config.ApiUrl)) + config.ApiUrl = defaultConfig.ApiUrl; + if (string.IsNullOrEmpty(config.HubUrl)) config.HubUrl = defaultConfig.HubUrl; - config.Transports ??= []; + config.Transports ??= defaultConfig.Transports ?? []; return config; } diff --git a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs index 633450a..1ec7fd3 100644 --- a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs +++ b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs @@ -11,6 +11,7 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Reflection; +using System.Text.Json; namespace MareSynchronos.WebAPI.SignalR; @@ -20,13 +21,16 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly ServerConfigurationManager _serverManager; + private readonly RemoteConfigurationService _remoteConfig; private readonly ConcurrentDictionary _tokenCache = new(); private readonly ConcurrentDictionary _wellKnownCache = new(StringComparer.Ordinal); - public TokenProvider(ILogger logger, ServerConfigurationManager serverManager, DalamudUtilService dalamudUtil, MareMediator mareMediator) + public TokenProvider(ILogger logger, ServerConfigurationManager serverManager, RemoteConfigurationService remoteConfig, + DalamudUtilService dalamudUtil, MareMediator mareMediator) { _logger = logger; _serverManager = serverManager; + _remoteConfig = remoteConfig; _dalamudUtil = dalamudUtil; _httpClient = new( new HttpClientHandler @@ -67,11 +71,21 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber Uri tokenUri; HttpResponseMessage result; + var authApiUrl = _serverManager.CurrentApiUrl; + + // Override the API URL used for auth from remote config, if one is available + if (authApiUrl.Equals(ApiController.LoporritServiceUri, StringComparison.Ordinal)) + { + var config = await _remoteConfig.GetConfigAsync("mainServer").ConfigureAwait(false) ?? new(); + if (!string.IsNullOrEmpty(config.ApiUrl)) + authApiUrl = config.ApiUrl; + } + try { _logger.LogDebug("GetNewToken: Requesting"); - tokenUri = MareAuth.AuthV2FullPath(new Uri(_serverManager.CurrentApiUrl + tokenUri = MareAuth.AuthV2FullPath(new Uri(authApiUrl .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) .Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase))); var secretKey = _serverManager.GetSecretKey(out _)!;