Replace repo config with generic remote config
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,3 +7,6 @@
|
|||||||
[submodule "Glamourer.Api"]
|
[submodule "Glamourer.Api"]
|
||||||
path = Glamourer.Api
|
path = Glamourer.Api
|
||||||
url = https://github.com/loporrit/Glamourer.Api.git
|
url = https://github.com/loporrit/Glamourer.Api.git
|
||||||
|
[submodule "BunnyWhispers"]
|
||||||
|
path = BunnyWhispers
|
||||||
|
url = https://github.com/loporrit/BunnyWhispers.git
|
||||||
|
|||||||
1
BunnyWhispers
Submodule
1
BunnyWhispers
Submodule
Submodule BunnyWhispers added at ffb3896b13
@@ -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();
|
||||||
|
}
|
||||||
11
MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs
Normal file
11
MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class RemoteConfigCacheService : ConfigurationServiceBase<RemoteConfigCache>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "remotecache.json";
|
||||||
|
|
||||||
|
public RemoteConfigCacheService(string configDir) : base(configDir) { }
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
<ProjectReference Include="..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||||
|
<ProjectReference Include="..\BunnyWhispers\Chaos.NaCl\Chaos.NaCl.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
|
collection.AddSingleton((s) => new RemoteConfigCacheService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<MareConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<MareConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
||||||
@@ -163,8 +164,10 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<PlayerPerformanceConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<PlayerPerformanceConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerBlockConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerBlockConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
||||||
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<RemoteConfigCacheService>());
|
||||||
collection.AddSingleton<ConfigurationMigrator>();
|
collection.AddSingleton<ConfigurationMigrator>();
|
||||||
collection.AddSingleton<ConfigurationSaveService>();
|
collection.AddSingleton<ConfigurationSaveService>();
|
||||||
|
collection.AddSingleton<RemoteConfigurationService>();
|
||||||
|
|
||||||
collection.AddSingleton<HubFactory>();
|
collection.AddSingleton<HubFactory>();
|
||||||
|
|
||||||
|
|||||||
@@ -4,72 +4,71 @@ using MareSynchronos.MareConfiguration.Models;
|
|||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
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 ILogger<NoSnapService> _logger;
|
||||||
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
private readonly Dictionary<string, bool> _listOfPlugins = new(StringComparer.Ordinal)
|
private readonly Dictionary<string, bool> _listOfPlugins = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
["Snapper"] = false,
|
["Snapper"] = false,
|
||||||
["Snappy"] = false,
|
["Snappy"] = false,
|
||||||
["Meddle.Plugin"] = false
|
["Meddle.Plugin"] = false,
|
||||||
};
|
};
|
||||||
private static readonly HashSet<int> _gposers = new();
|
private static readonly HashSet<int> _gposers = new();
|
||||||
private static readonly HashSet<string> _gposersNamed = new(StringComparer.Ordinal);
|
private static readonly HashSet<string> _gposersNamed = new(StringComparer.Ordinal);
|
||||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly RemoteConfigurationService _remoteConfig;
|
||||||
|
|
||||||
public static bool AnyLoaded { get; private set; } = false;
|
public static bool AnyLoaded { get; private set; } = false;
|
||||||
|
|
||||||
public MareMediator Mediator { get; init; }
|
public MareMediator Mediator { get; init; }
|
||||||
|
|
||||||
public NoSnapService(ILogger<NoSnapService> logger, IDalamudPluginInterface pi, MareMediator mediator,
|
public NoSnapService(ILogger<NoSnapService> logger, IDalamudPluginInterface pluginInterface, MareMediator mediator,
|
||||||
IHostApplicationLifetime hostApplicationLifetime, DalamudUtilService dalamudUtilService, IpcManager ipcManager)
|
IHostApplicationLifetime hostApplicationLifetime, DalamudUtilService dalamudUtilService, IpcManager ipcManager,
|
||||||
|
RemoteConfigurationService remoteConfig)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_pluginInterface = pluginInterface;
|
||||||
Mediator = mediator;
|
Mediator = mediator;
|
||||||
_hostApplicationLifetime = hostApplicationLifetime;
|
_hostApplicationLifetime = hostApplicationLifetime;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_ipcManager = ipcManager;
|
_ipcManager = ipcManager;
|
||||||
foreach (var pluginName in _listOfPlugins.Keys)
|
_remoteConfig = remoteConfig;
|
||||||
{
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Mediator.Subscribe<GposeEndMessage>(this, msg => ClearGposeList());
|
Mediator.Subscribe<GposeEndMessage>(this, msg => ClearGposeList());
|
||||||
Mediator.Subscribe<CutsceneEndMessage>(this, msg => ClearGposeList());
|
Mediator.Subscribe<CutsceneEndMessage>(this, msg => ClearGposeList());
|
||||||
|
|
||||||
Update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddGposer(int objectIndex)
|
public void AddGposer(int objectIndex)
|
||||||
{
|
{
|
||||||
if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
|
if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Immediately reverting object index {id}", objectIndex);
|
_logger.LogTrace("Immediately reverting object index {id}", objectIndex);
|
||||||
RevertAndRedraw(objectIndex);
|
RevertAndRedraw(objectIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Registering gposer object index {id}", objectIndex);
|
_logger.LogTrace("Registering gposer object index {id}", objectIndex);
|
||||||
lock (_gposers)
|
lock (_gposers)
|
||||||
_gposers.Add(objectIndex);
|
_gposers.Add(objectIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveGposer(int 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)
|
lock (_gposers)
|
||||||
_gposers.Remove(objectIndex);
|
_gposers.Remove(objectIndex);
|
||||||
}
|
}
|
||||||
@@ -78,12 +77,12 @@ public class NoSnapService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
|
if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Immediately reverting {name}", name);
|
_logger.LogTrace("Immediately reverting {name}", name);
|
||||||
RevertAndRedraw(name);
|
RevertAndRedraw(name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Registering gposer {name}", name);
|
_logger.LogTrace("Registering gposer {name}", name);
|
||||||
lock (_gposers)
|
lock (_gposers)
|
||||||
_gposersNamed.Add(name);
|
_gposersNamed.Add(name);
|
||||||
}
|
}
|
||||||
@@ -91,7 +90,7 @@ public class NoSnapService : IHostedService, IMediatorSubscriber
|
|||||||
private void ClearGposeList()
|
private void ClearGposeList()
|
||||||
{
|
{
|
||||||
if (_gposers.Count > 0 || _gposersNamed.Count > 0)
|
if (_gposers.Count > 0 || _gposersNamed.Count > 0)
|
||||||
_logger.LogInformation("Clearing gposer list");
|
_logger.LogTrace("Clearing gposer list");
|
||||||
lock (_gposers)
|
lock (_gposers)
|
||||||
_gposers.Clear();
|
_gposers.Clear();
|
||||||
lock (_gposersNamed)
|
lock (_gposersNamed)
|
||||||
@@ -170,9 +169,31 @@ public class NoSnapService : IHostedService, IMediatorSubscriber
|
|||||||
}).GetAwaiter().GetResult();
|
}).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)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
@@ -187,7 +208,6 @@ public class NoSnapService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
if (AnyLoaded != anyLoadedNow)
|
if (AnyLoaded != anyLoadedNow)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("AnyLoaded is now {AnyLoaded}", AnyLoaded);
|
|
||||||
AnyLoaded = anyLoadedNow;
|
AnyLoaded = anyLoadedNow;
|
||||||
Mediator.Publish(new RecalculatePerformanceMessage(null));
|
Mediator.Publish(new RecalculatePerformanceMessage(null));
|
||||||
|
|
||||||
|
|||||||
202
MareSynchronos/Services/RemoteConfigurationService.cs
Normal file
202
MareSynchronos/Services/RemoteConfigurationService.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
@@ -6,7 +5,6 @@ using Microsoft.Extensions.Hosting;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
@@ -215,12 +213,14 @@ public sealed class RepoChangeService : IHostedService
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private readonly ILogger<RepoChangeService> _logger;
|
private readonly ILogger<RepoChangeService> _logger;
|
||||||
|
private readonly RemoteConfigurationService _remoteConfig;
|
||||||
private readonly IDalamudPluginInterface _pluginInterface;
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
private readonly IFramework _framework;
|
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;
|
_logger = logger;
|
||||||
|
_remoteConfig = remoteConfig;
|
||||||
_pluginInterface = pluginInterface;
|
_pluginInterface = pluginInterface;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
}
|
}
|
||||||
@@ -228,10 +228,7 @@ public sealed class RepoChangeService : IHostedService
|
|||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Starting RepoChange Service");
|
_logger.LogDebug("Starting RepoChange Service");
|
||||||
var repoChangeConfig = await DownloadRepoChangeConfig().ConfigureAwait(false);
|
var repoChangeConfig = await _remoteConfig.GetConfigAsync<RepoChangeConfig>("repoChange").ConfigureAwait(false) ?? new();
|
||||||
|
|
||||||
if (repoChangeConfig == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var currentRepo = repoChangeConfig.CurrentRepo;
|
var currentRepo = repoChangeConfig.CurrentRepo;
|
||||||
var validRepos = (repoChangeConfig.ValidRepos ?? []).ToList();
|
var validRepos = (repoChangeConfig.ValidRepos ?? []).ToList();
|
||||||
@@ -403,85 +400,4 @@ public sealed class RepoChangeService : IHostedService
|
|||||||
_logger.LogDebug("Stopping RepoChange Service");
|
_logger.LogDebug("Stopping RepoChange Service");
|
||||||
return Task.CompletedTask;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class ServerConfigurationManager
|
|||||||
|
|
||||||
private HashSet<string>? _cachedWhitelistedUIDs = null;
|
private HashSet<string>? _cachedWhitelistedUIDs = null;
|
||||||
private HashSet<string>? _cachedBlacklistedUIDs = null;
|
private HashSet<string>? _cachedBlacklistedUIDs = null;
|
||||||
|
private string? _realApiUrl = null;
|
||||||
|
|
||||||
public ServerConfigurationManager(ILogger<ServerConfigurationManager> logger, ServerConfigService configService,
|
public ServerConfigurationManager(ILogger<ServerConfigurationManager> logger, ServerConfigService configService,
|
||||||
ServerTagConfigService serverTagConfig, SyncshellConfigService syncshellConfig, NotesConfigService notesConfig,
|
ServerTagConfigService serverTagConfig, SyncshellConfigService syncshellConfig, NotesConfigService notesConfig,
|
||||||
@@ -36,6 +37,13 @@ public class ServerConfigurationManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string CurrentApiUrl => CurrentServer.ServerUri;
|
public string CurrentApiUrl => CurrentServer.ServerUri;
|
||||||
|
public string CurrentRealApiUrl
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _realApiUrl ?? CurrentApiUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
public ServerStorage CurrentServer => _configService.Current.ServerStorage[CurrentServerIndex];
|
public ServerStorage CurrentServer => _configService.Current.ServerStorage[CurrentServerIndex];
|
||||||
|
|
||||||
public IReadOnlyList<string> Whitelist => CurrentBlockStorage().Whitelist;
|
public IReadOnlyList<string> Whitelist => CurrentBlockStorage().Whitelist;
|
||||||
@@ -48,6 +56,7 @@ public class ServerConfigurationManager
|
|||||||
_configService.Current.CurrentServer = value;
|
_configService.Current.CurrentServer = value;
|
||||||
_cachedWhitelistedUIDs = null;
|
_cachedWhitelistedUIDs = null;
|
||||||
_cachedBlacklistedUIDs = null;
|
_cachedBlacklistedUIDs = null;
|
||||||
|
_realApiUrl = null;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ This service is provided as-is.
|
|||||||
using HttpClient httpClient = new();
|
using HttpClient httpClient = new();
|
||||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
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("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||||
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
||||||
_logger.LogInformation("Registering new account: {uri}", postUri.ToString());
|
_logger.LogInformation("Registering new account: {uri}", postUri.ToString());
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
_uploadCancellationTokenSource = new CancellationTokenSource();
|
_uploadCancellationTokenSource = new CancellationTokenSource();
|
||||||
var uploadToken = _uploadCancellationTokenSource.Token;
|
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<string> unverifiedUploads = GetUnverifiedFiles(data);
|
HashSet<string> unverifiedUploads = GetUnverifiedFiles(data);
|
||||||
if (unverifiedUploads.Any())
|
if (unverifiedUploads.Any())
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ namespace MareSynchronos.WebAPI.SignalR;
|
|||||||
|
|
||||||
public record HubConnectionConfig
|
public record HubConnectionConfig
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("api_url")]
|
||||||
|
public string ApiUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonPropertyName("hub_url")]
|
[JsonPropertyName("hub_url")]
|
||||||
public string HubUrl { get; set; } = string.Empty;
|
public string HubUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MareSynchronos.API.SignalR;
|
using MareSynchronos.API.SignalR;
|
||||||
|
using MareSynchronos.Services;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
using MareSynchronos.WebAPI.SignalR.Utils;
|
using MareSynchronos.WebAPI.SignalR.Utils;
|
||||||
@@ -18,6 +19,7 @@ public class HubFactory : MediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
private readonly ILoggerProvider _loggingProvider;
|
private readonly ILoggerProvider _loggingProvider;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly RemoteConfigurationService _remoteConfig;
|
||||||
private readonly TokenProvider _tokenProvider;
|
private readonly TokenProvider _tokenProvider;
|
||||||
private HubConnection? _instance;
|
private HubConnection? _instance;
|
||||||
private string _cachedConfigFor = string.Empty;
|
private string _cachedConfigFor = string.Empty;
|
||||||
@@ -25,10 +27,11 @@ public class HubFactory : MediatorSubscriberBase
|
|||||||
private bool _isDisposed = false;
|
private bool _isDisposed = false;
|
||||||
|
|
||||||
public HubFactory(ILogger<HubFactory> logger, MareMediator mediator,
|
public HubFactory(ILogger<HubFactory> logger, MareMediator mediator,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
ServerConfigurationManager serverConfigurationManager, RemoteConfigurationService remoteConfig,
|
||||||
TokenProvider tokenProvider, ILoggerProvider pluginLog) : base(logger, mediator)
|
TokenProvider tokenProvider, ILoggerProvider pluginLog) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
_remoteConfig = remoteConfig;
|
||||||
_tokenProvider = tokenProvider;
|
_tokenProvider = tokenProvider;
|
||||||
_loggingProvider = pluginLog;
|
_loggingProvider = pluginLog;
|
||||||
}
|
}
|
||||||
@@ -85,7 +88,12 @@ public class HubFactory : MediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_serverConfigurationManager.CurrentApiUrl.Equals(ApiController.LoporritServiceUri, StringComparison.Ordinal))
|
if (_serverConfigurationManager.CurrentApiUrl.Equals(ApiController.LoporritServiceUri, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var mainServerConfig = await _remoteConfig.GetConfigAsync<HubConnectionConfig>("mainServer").ConfigureAwait(false) ?? new();
|
||||||
|
defaultConfig = mainServerConfig;
|
||||||
|
if (string.IsNullOrEmpty(mainServerConfig.HubUrl))
|
||||||
defaultConfig.HubUrl = ApiController.LoporritServiceHubUri;
|
defaultConfig.HubUrl = ApiController.LoporritServiceHubUri;
|
||||||
|
}
|
||||||
|
|
||||||
string jsonResponse;
|
string jsonResponse;
|
||||||
|
|
||||||
@@ -140,21 +148,18 @@ public class HubFactory : MediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var config = JsonSerializer.Deserialize<HubConnectionConfig>(
|
var config = JsonSerializer.Deserialize<HubConnectionConfig>(jsonResponse);
|
||||||
jsonResponse,
|
|
||||||
new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
AllowTrailingCommas = true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (config == null)
|
if (config == null)
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(config.ApiUrl))
|
||||||
|
config.ApiUrl = defaultConfig.ApiUrl;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(config.HubUrl))
|
if (string.IsNullOrEmpty(config.HubUrl))
|
||||||
config.HubUrl = defaultConfig.HubUrl;
|
config.HubUrl = defaultConfig.HubUrl;
|
||||||
|
|
||||||
config.Transports ??= [];
|
config.Transports ??= defaultConfig.Transports ?? [];
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using System.Net;
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace MareSynchronos.WebAPI.SignalR;
|
namespace MareSynchronos.WebAPI.SignalR;
|
||||||
|
|
||||||
@@ -20,13 +21,16 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
|
|||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger<TokenProvider> _logger;
|
private readonly ILogger<TokenProvider> _logger;
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
|
private readonly RemoteConfigurationService _remoteConfig;
|
||||||
private readonly ConcurrentDictionary<JwtIdentifier, string> _tokenCache = new();
|
private readonly ConcurrentDictionary<JwtIdentifier, string> _tokenCache = new();
|
||||||
private readonly ConcurrentDictionary<string, string?> _wellKnownCache = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, string?> _wellKnownCache = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
public TokenProvider(ILogger<TokenProvider> logger, ServerConfigurationManager serverManager, DalamudUtilService dalamudUtil, MareMediator mareMediator)
|
public TokenProvider(ILogger<TokenProvider> logger, ServerConfigurationManager serverManager, RemoteConfigurationService remoteConfig,
|
||||||
|
DalamudUtilService dalamudUtil, MareMediator mareMediator)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_serverManager = serverManager;
|
_serverManager = serverManager;
|
||||||
|
_remoteConfig = remoteConfig;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_httpClient = new(
|
_httpClient = new(
|
||||||
new HttpClientHandler
|
new HttpClientHandler
|
||||||
@@ -67,11 +71,21 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
|
|||||||
Uri tokenUri;
|
Uri tokenUri;
|
||||||
HttpResponseMessage result;
|
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<HubConnectionConfig>("mainServer").ConfigureAwait(false) ?? new();
|
||||||
|
if (!string.IsNullOrEmpty(config.ApiUrl))
|
||||||
|
authApiUrl = config.ApiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogDebug("GetNewToken: Requesting");
|
_logger.LogDebug("GetNewToken: Requesting");
|
||||||
|
|
||||||
tokenUri = MareAuth.AuthV2FullPath(new Uri(_serverManager.CurrentApiUrl
|
tokenUri = MareAuth.AuthV2FullPath(new Uri(authApiUrl
|
||||||
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||||
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
|
||||||
var secretKey = _serverManager.GetSecretKey(out _)!;
|
var secretKey = _serverManager.GetSecretKey(out _)!;
|
||||||
|
|||||||
Reference in New Issue
Block a user