Replace repo config with generic remote config

This commit is contained in:
Loporrit
2025-08-02 09:34:26 +00:00
parent 72224c46b5
commit 5c9ca801f8
15 changed files with 330 additions and 129 deletions

View File

@@ -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<NoSnapService> _logger;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Dictionary<string, bool> _listOfPlugins = new(StringComparer.Ordinal)
{
["Snapper"] = false,
["Snappy"] = false,
["Meddle.Plugin"] = false
["Meddle.Plugin"] = false,
};
private static readonly HashSet<int> _gposers = new();
private static readonly HashSet<string> _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<NoSnapService> logger, IDalamudPluginInterface pi, MareMediator mediator,
IHostApplicationLifetime hostApplicationLifetime, DalamudUtilService dalamudUtilService, IpcManager ipcManager)
public NoSnapService(ILogger<NoSnapService> 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<PluginChangeMessage>(this, pluginName, (msg) =>
{
_logger.LogInformation("{pluginName} isLoaded = {isLoaded}", pluginName, msg.IsLoaded);
_listOfPlugins[pluginName] = msg.IsLoaded;
Update();
});
}
_remoteConfig = remoteConfig;
Mediator.Subscribe<GposeEndMessage>(this, msg => ClearGposeList());
Mediator.Subscribe<CutsceneEndMessage>(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<NoSnapConfig>("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<PluginChangeMessage>(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));

View File

@@ -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<string, string> 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<RemoteConfigurationService> _logger;
private readonly RemoteConfigCacheService _configService;
private readonly Task _initTask;
public RemoteConfigurationService(ILogger<RemoteConfigurationService> logger, RemoteConfigCacheService configService)
{
_logger = logger;
_configService = configService;
_initTask = Task.Run(DownloadConfig);
}
public async Task<JsonObject> 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<T?> GetConfigAsync<T>(string sectionName)
{
try
{
var json = await GetConfigAsync(sectionName).ConfigureAwait(false);
return JsonSerializer.Deserialize<T>(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<ulong>();
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<string>();
bool verified = signatures.Any(sig =>
ConfigPublicKeys.TryGetValue(sig.Key, out var pubKey) &&
VerifySignature(configString, ts, sig.Value!.GetValue<string>(), 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();
}
}

View File

@@ -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<RepoChangeService> _logger;
private readonly RemoteConfigurationService _remoteConfig;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IFramework _framework;
public RepoChangeService(ILogger<RepoChangeService> logger, IDalamudPluginInterface pluginInterface, IFramework framework)
public RepoChangeService(ILogger<RepoChangeService> 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<RepoChangeConfig>("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<RepoChangeConfig?> 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<RepoChangeConfig>(
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;
}
}
}

View File

@@ -20,6 +20,7 @@ public class ServerConfigurationManager
private HashSet<string>? _cachedWhitelistedUIDs = null;
private HashSet<string>? _cachedBlacklistedUIDs = null;
private string? _realApiUrl = null;
public ServerConfigurationManager(ILogger<ServerConfigurationManager> 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<string> Whitelist => CurrentBlockStorage().Whitelist;
@@ -48,6 +56,7 @@ public class ServerConfigurationManager
_configService.Current.CurrentServer = value;
_cachedWhitelistedUIDs = null;
_cachedBlacklistedUIDs = null;
_realApiUrl = null;
_configService.Save();
}
get