diff --git a/SS14.Launcher/Api/AuthApi.cs b/SS14.Launcher/Api/AuthApi.cs index 5a2a4be..c4e45dd 100644 --- a/SS14.Launcher/Api/AuthApi.cs +++ b/SS14.Launcher/Api/AuthApi.cs @@ -18,17 +18,19 @@ namespace SS14.Launcher.Api; public sealed class AuthApi { private readonly HttpClient _httpClient; + private readonly LoginProviderManager _loginProviderManager; - public AuthApi(HttpClient http) + public AuthApi(HttpClient http, LoginProviderManager loginProviderManager) { _httpClient = http; + _loginProviderManager = loginProviderManager; } public async Task AuthenticateAsync(AuthenticateRequest request) { try { - var authUrl = LoginManager.GetAuthServerById(request.Server ?? ConfigConstants.FallbackAuthServer, request.ServerUrl); + var authUrl = _loginProviderManager.GetAuthServerById(request.Server ?? ConfigConstants.FallbackAuthServer, request.ServerUrl); using var resp = await _httpClient.PostAsJsonAsync(authUrl.AuthAuthUrl, request); @@ -83,7 +85,7 @@ public async Task RegisterAsync(string server, string? serverUrl { var request = new RegisterRequest(username, email, password); - var authUrl = LoginManager.GetAuthServerById(server, serverUrl).AuthRegUrl; + var authUrl = _loginProviderManager.GetAuthServerById(server, serverUrl).AuthRegUrl; using var resp = await _httpClient.PostAsJsonAsync(authUrl, request); @@ -125,7 +127,7 @@ public async Task RegisterAsync(string server, string? serverUrl { var request = new ResetPasswordRequest(email); - var authUrl = LoginManager.GetAuthServerById(server, serverUrl).AuthPwResetUrl; + var authUrl = _loginProviderManager.GetAuthServerById(server, serverUrl).AuthPwResetUrl; using var resp = await _httpClient.PostAsJsonAsync(authUrl, request); @@ -152,7 +154,7 @@ public async Task RegisterAsync(string server, string? serverUrl { var request = new ResendConfirmationRequest(email); - var authUrl = LoginManager.GetAuthServerById(server, serverUrl).AuthResendUrl; + var authUrl = _loginProviderManager.GetAuthServerById(server, serverUrl).AuthResendUrl; using var resp = await _httpClient.PostAsJsonAsync(authUrl, request); @@ -184,7 +186,7 @@ public async Task RegisterAsync(string server, string? serverUrl { var request = new RefreshRequest(token); - var authUrl = LoginManager.GetAuthServerById(server, serverUrl).AuthRefreshUrl; + var authUrl = _loginProviderManager.GetAuthServerById(server, serverUrl).AuthRefreshUrl; using var resp = await _httpClient.PostAsJsonAsync(authUrl, request); @@ -227,7 +229,7 @@ public async Task LogoutTokenAsync(string server, string? serverUrl, string toke { var request = new LogoutRequest(token); - var authUrl = LoginManager.GetAuthServerById(server, serverUrl).AuthLogoutUrl; + var authUrl = _loginProviderManager.GetAuthServerById(server, serverUrl).AuthLogoutUrl; using var resp = await _httpClient.PostAsJsonAsync(authUrl, request); @@ -259,7 +261,7 @@ public async Task CheckTokenAsync(string server, string? serverUrl, string { try { - var authUrl = LoginManager.GetAuthServerById(server, serverUrl).AuthPingUrl; + var authUrl = _loginProviderManager.GetAuthServerById(server, serverUrl).AuthPingUrl; using var requestMessage = new HttpRequestMessage(HttpMethod.Get, authUrl); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("SS14Auth", token); diff --git a/SS14.Launcher/App.xaml.cs b/SS14.Launcher/App.xaml.cs index 0014c13..b10c981 100644 --- a/SS14.Launcher/App.xaml.cs +++ b/SS14.Launcher/App.xaml.cs @@ -17,7 +17,9 @@ using Splat; using SS14.Launcher.Localization; using SS14.Launcher.Models; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Models.ContentManagement; +using SS14.Launcher.Models.Data; using SS14.Launcher.Models.OverrideAssets; using SS14.Launcher.Utility; using SS14.Launcher.ViewModels; @@ -200,14 +202,19 @@ public override void OnFrameworkInitializationCompleted() } } - private void OnStartup(object? s, ControlledApplicationLifetimeStartupEventArgs e) + private async void OnStartup(object? s, ControlledApplicationLifetimeStartupEventArgs e) { var loc = Locator.Current.GetRequiredService(); var msgr = Locator.Current.GetRequiredService(); var contentManager = Locator.Current.GetRequiredService(); var overrideAssets = Locator.Current.GetRequiredService(); var launcherInfo = Locator.Current.GetRequiredService(); + var cdnManager = Locator.Current.GetRequiredService(); + var cfg = Locator.Current.GetRequiredService(); + cdnManager.ShowPingWindow(); + await Task.Run(cdnManager.SortFastestAndMap); + cfg.LoadHubs(cdnManager); loc.Initialize(); launcherInfo.Initialize(); contentManager.Initialize(); diff --git a/SS14.Launcher/ConfigConstants.cs b/SS14.Launcher/ConfigConstants.cs index 16c5ed1..5f854c4 100644 --- a/SS14.Launcher/ConfigConstants.cs +++ b/SS14.Launcher/ConfigConstants.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Utility; -using TerraFX.Interop.Windows; namespace SS14.Launcher; @@ -32,32 +32,33 @@ public static class ConfigConstants // Amount of time to wait to let a redialling client properly die public const int LauncherCommandsRedialWaitTimeout = 1000; - public const string FallbackAuthServer = "Space-Wizards"; - public const string GuestAuthServer = "guest"; - public const string CustomAuthServer = "Custom"; + public static readonly UriCdnDefinition FallbackAuthServer = "Space-Wizards"; + public static readonly UriCdnDefinition GuestAuthServer = "guest"; + public static readonly UriCdnDefinition CustomAuthServer = "Custom"; public static readonly AuthServer TemplateAuthServer = new(new("https://example.com/"), new("https://example.com/")); - public static readonly Dictionary AuthUrls = new() + + public static readonly Dictionary AuthUrls = new() { { "SimpleStation", - new(new("https://auth.simplestation.org/"), new("https://account.simplestation.org/"), true) + new("SimpleStationAuth", "SimpleStationAccount", true) }, { FallbackAuthServer, - new(new("https://auth.spacestation14.com/"), new("https://account.spacestation14.com/"), false) + new("FallbackAuthServerAuth", "FallbackAuthServerAccount", false) }, { CustomAuthServer, - new (new("https://example.com/"), new("https://example.com/")) + new ("CustomAuthServerAuth", "CustomAuthServerAccount") }, }; - public static readonly Uri[] DefaultHubUrls = + public static readonly UriCdnDefinition[] DefaultHubUrls = [ - new("https://hub.simplestation.org/"), - new("https://hub.singularity14.co.uk/"), - new("https://cdn.spacestationmultiverse.com/hub/"), - new("https://hub.spacestation14.com/"), + "SimpleStationHub", + "SingularityHub", + "MultiverseHub", + "SpaceStationHub", ]; public const string DiscordUrl = "https://discord.gg/49KeKwXc8g"; @@ -66,61 +67,42 @@ public static class ConfigConstants public const string NewsFeedUrl = "https://spacestation14.com/post/index.xml"; //TODO public const string TranslateUrl = "https://docs.spacestation14.com/en/general-development/contributing-translations.html"; //TODO - public static readonly Dictionary EngineBuildsUrl = new() + public static readonly Dictionary EngineBuildsUrl = new() { { - "Robust", - new UrlFallbackSet([ - "https://robust-builds.cdn.spacestation14.com/manifest.json", - "https://robust-builds.fallback.cdn.spacestation14.com/manifest.json", - ]) + "Robust", "RobustEngine" }, { - "Multiverse", - new UrlFallbackSet([ - "https://cdn.spacestationmultiverse.com/ssmv-engine-manifest", - ]) + "Multiverse", "MultiverseEngine" }, { - "Supermatter", - new UrlFallbackSet([ - "https://cdn.simplestation.org/supermatter/manifest.json", - ]) + "Supermatter", "SupermatterEngine" }, }; - public static readonly Dictionary EngineModulesUrl = new() + public static readonly Dictionary EngineModulesUrl = new() { { - "Robust", - new UrlFallbackSet([ - "https://robust-builds.cdn.spacestation14.com/modules.json", - "https://robust-builds.fallback.cdn.spacestation14.com/modules.json", - ]) + "Robust", "RobustModules" }, { - "Multiverse", - new UrlFallbackSet([ - // Same as Robust for now - "https://robust-builds.cdn.spacestation14.com/modules.json", - "https://robust-builds.fallback.cdn.spacestation14.com/modules.json", - ]) + "Multiverse", "MultiverseModules" }, }; - private static readonly UrlFallbackSet LauncherDataBaseUrl = new([ - "http://assets.simplestation.org/launcher/", - ]); - // How long to keep cached copies of Robust manifests. // TODO: Take this from Cache-Control header responses instead. public static readonly TimeSpan RobustManifestCacheTime = TimeSpan.FromMinutes(15); - public static readonly UrlFallbackSet UrlLauncherInfo = LauncherDataBaseUrl + "info.json"; - public static readonly UrlFallbackSet UrlAssetsBase = LauncherDataBaseUrl + "assets/"; + public static readonly UriCdnDefinition UrlLauncherInfo = "LauncherInfo"; + public static readonly UriCdnDefinition UrlAssetsBase = "LauncherAssetsBase"; public const string FallbackUsername = "JoeGenero"; + public record struct AuthServerDefinition( + UriCdnDefinition AuthUrl, + UriCdnDefinition AccountSite, + bool? Recommended = null); public class AuthServer( Uri authUrl, diff --git a/SS14.Launcher/Controls/CDN/CdnPingEntry.axaml b/SS14.Launcher/Controls/CDN/CdnPingEntry.axaml new file mode 100644 index 0000000..bbf9fbb --- /dev/null +++ b/SS14.Launcher/Controls/CDN/CdnPingEntry.axaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/SS14.Launcher/Controls/CDN/CdnPingEntry.axaml.cs b/SS14.Launcher/Controls/CDN/CdnPingEntry.axaml.cs new file mode 100644 index 0000000..2acbf62 --- /dev/null +++ b/SS14.Launcher/Controls/CDN/CdnPingEntry.axaml.cs @@ -0,0 +1,37 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using SS14.Launcher.Models.CDN; + +namespace SS14.Launcher.Controls.CDN; + +public partial class CdnPingEntry : UserControl +{ + public CdnPingEntry() + { + InitializeComponent(); + } + + public void SetData(CdnDataCompound cdnData) + { + UrlName.Text = cdnData.CdnData.Uri.ToString(); + var result = cdnData.Ping; + + if (result.TimeoutMs is not null) + { + Status.Text = $"{result.TimeoutMs}ms"; + } + else + { + Status.Text = result.Reason; + } + + if (result.Error) + { + Status.Foreground = Brushes.White; + Status.Background = Brushes.DarkRed; + } + } +} + diff --git a/SS14.Launcher/Controls/CDN/CdnPingGroupControl.axaml b/SS14.Launcher/Controls/CDN/CdnPingGroupControl.axaml new file mode 100644 index 0000000..e6f1ff8 --- /dev/null +++ b/SS14.Launcher/Controls/CDN/CdnPingGroupControl.axaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/SS14.Launcher/Controls/CDN/CdnPingGroupControl.axaml.cs b/SS14.Launcher/Controls/CDN/CdnPingGroupControl.axaml.cs new file mode 100644 index 0000000..a2317c5 --- /dev/null +++ b/SS14.Launcher/Controls/CDN/CdnPingGroupControl.axaml.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace SS14.Launcher.Controls.CDN; + +public partial class CdnPingGroupControl : Border +{ + private Dictionary _entries = new(); + + public CdnPingGroupControl() + { + InitializeComponent(); + } + + public CdnPingEntry EnsureEntry(string key) + { + if (_entries.TryGetValue(key, out var entry)) + return entry; + + entry = new CdnPingEntry(); + _entries.Add(key, entry); + PingInfoContainer.Children.Add(entry); + return entry; + } +} + diff --git a/SS14.Launcher/Controls/CDN/CdnPingWindow.axaml b/SS14.Launcher/Controls/CDN/CdnPingWindow.axaml new file mode 100644 index 0000000..863ebb9 --- /dev/null +++ b/SS14.Launcher/Controls/CDN/CdnPingWindow.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/SS14.Launcher/Controls/CDN/CdnPingWindow.axaml.cs b/SS14.Launcher/Controls/CDN/CdnPingWindow.axaml.cs new file mode 100644 index 0000000..1810ec4 --- /dev/null +++ b/SS14.Launcher/Controls/CDN/CdnPingWindow.axaml.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Threading; +using DynamicData; +using SS14.Launcher.Models.CDN; + +namespace SS14.Launcher.Controls.CDN; + +public partial class CdnPingWindow : Window +{ + public Dictionary ActiveCdnGroups = []; + + public CdnPingWindow() + { + InitializeComponent(); + } + + public void ResolveItem(CdnDataCompound cdnDataCompound) + { + Dispatcher.UIThread.Post(() => + { + if(!ActiveCdnGroups.TryGetValue(cdnDataCompound.CdnData.Id, out var group)) + { + group = new CdnPingGroupControl(); + group.GroupLabel.Content = cdnDataCompound.CdnData.Id.ToString(); + ActiveCdnGroups.Add(cdnDataCompound.CdnData.Id, group); + CdnPanel.Children.Add(group); + } + + var entry = group.EnsureEntry(cdnDataCompound.CdnData.Uri.AbsoluteUri); + entry.SetData(cdnDataCompound); + }); + } +} diff --git a/SS14.Launcher/Models/CDN/CdnDataListSerializer.cs b/SS14.Launcher/Models/CDN/CdnDataListSerializer.cs new file mode 100644 index 0000000..2e8f9f7 --- /dev/null +++ b/SS14.Launcher/Models/CDN/CdnDataListSerializer.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SS14.Launcher.Models.CDN; + +public static class CdnDataListSerializer +{ + public static IEnumerable DeserializeCdnList(string raw) + { + foreach (var newLine in raw.Split("\n")) + { + foreach (var splited in newLine.Trim().Split(';')) + { + var nameValue = splited.Split('='); + if(nameValue.Length != 2) continue; + yield return new UriCdnData(nameValue[0], new Uri(nameValue[1])); + } + } + } + + public static string SerializeCdnList(IEnumerable cdnList) + { + var str = ""; + foreach (var data in cdnList) + { + str += data.ToString() + "\n"; + } + return str; + } +} diff --git a/SS14.Launcher/Models/CDN/CdnHelper.cs b/SS14.Launcher/Models/CDN/CdnHelper.cs new file mode 100644 index 0000000..f996567 --- /dev/null +++ b/SS14.Launcher/Models/CDN/CdnHelper.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using SS14.Launcher.Utility; + +namespace SS14.Launcher.Models.CDN; + +public static class CdnHelper +{ + public static readonly List DefaultCdnList = new() + { + //AUTH + new UriCdnData("SimpleStationAuth", new Uri("https://auth.simplestation.org/")), + new UriCdnData("SimpleStationAccount", new Uri("https://account.simplestation.org/")), + + new UriCdnData("FallbackAuthServerAuth", new Uri("https://auth.spacestation14.com/")), + new UriCdnData("FallbackAuthServerAccount", new Uri("https://account.spacestation14.com/")), + + new UriCdnData("CustomAuthServerAuth", new Uri("https://auth.example.com/")), + new UriCdnData("CustomAuthServerAccount", new Uri("https://account.example.com/")), + + //HUB + new UriCdnData("SimpleStationHub",new Uri("https://hub.simplestation.org/")), + new UriCdnData("SingularityHub",new Uri("https://hub.singularity14.co.uk/")), + new UriCdnData("MultiverseHub",new Uri("https://cdn.spacestationmultiverse.com/hub/")), + new UriCdnData("SpaceStationHub",new Uri("https://hub.spacestation14.com/")), + + //ENGINE + new UriCdnData("RobustEngine", new Uri("https://robust-builds.cdn.spacestation14.com/manifest.json")), + new UriCdnData("RobustEngine", new Uri("https://robust-builds.fallback.cdn.spacestation14.com/manifest.json")), + + new UriCdnData("MultiverseEngine", new Uri("https://cdn.spacestationmultiverse.com/ssmv-engine-manifest")), + new UriCdnData("SupermatterEngine", new Uri("https://cdn.simplestation.org/supermatter/manifest.json")), + + //MODULES + new UriCdnData("RobustModules", new Uri("https://robust-builds.cdn.spacestation14.com/modules.json")), + new UriCdnData("RobustModules", new Uri("https://robust-builds.fallback.cdn.spacestation14.com/modules.json")), + // Same as Robust for now + new UriCdnData("MultiverseModules", new Uri("https://robust-builds.cdn.spacestation14.com/modules.json")), + new UriCdnData("MultiverseModules", new Uri("https://robust-builds.fallback.cdn.spacestation14.com/modules.json")), + + //Launcher assets + new UriCdnData("LauncherInfo", new Uri("http://assets.simplestation.org/launcher/info.json")), + new UriCdnData("LauncherAssetsBase", new Uri("http://assets.simplestation.org/launcher/assets/")), + }; + + public static ConfigConstants.AuthServer ResolveDefinition(this CdnManager cdnManager, ConfigConstants.AuthServerDefinition definition) + { + return new ConfigConstants.AuthServer( + cdnManager.ResolveDefinition(definition.AuthUrl), + cdnManager.ResolveDefinition(definition.AccountSite), + definition.Recommended + ); + } + + public static UrlFallbackSet ResolveUrlSet(this CdnManager cdnManager, UriCdnDefinition definition) + { + return new UrlFallbackSet([cdnManager.ResolveDefinition(definition).AbsoluteUri]); + } + + public static IEnumerable ResolveDefinition(this CdnManager cdnManager, IEnumerable definitions) + { + foreach (var definition in definitions) + { + yield return cdnManager.ResolveDefinition(definition); + } + } +} diff --git a/SS14.Launcher/Models/CDN/CdnManager.cs b/SS14.Launcher/Models/CDN/CdnManager.cs new file mode 100644 index 0000000..315f7fe --- /dev/null +++ b/SS14.Launcher/Models/CDN/CdnManager.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Threading; +using Serilog; +using SS14.Launcher.Controls.CDN; +using SS14.Launcher.Models.Data; + +namespace SS14.Launcher.Models.CDN; + +public class CdnManager +{ + private readonly List _cdnList; + private readonly Dictionary _cdnMap = []; + + private static readonly PingCache Cache = new(); + + private static readonly string DataPath = Path.Combine(LauncherPaths.DirUserData, "mirrors.txt"); + + public CdnManager() + { + using var fileStream = File.Open(DataPath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite); + using var streamReader = new StreamReader(fileStream); + + _cdnList = CdnDataListSerializer.DeserializeCdnList(streamReader.ReadToEnd()).ToList(); + + if (_cdnList.Count == 0) + { + _cdnList = CdnHelper.DefaultCdnList; + Dirty(); + } + } + + public Uri ResolveDefinition(UriCdnDefinition definition) + { + if (_cdnMap.TryGetValue(definition, out var cdnData)) return cdnData.Uri; + + if (Uri.TryCreate(definition.Name, UriKind.Absolute, out var uri)) + { + Log.Warning("Trying to resolve url, not definition: {definition}", definition); + return uri; + } + + throw new KeyNotFoundException($"Cdn definition {definition} not found"); + } + + private CdnPingWindow _pingWindow = default!; + + public void ShowPingWindow() + { + _pingWindow = new CdnPingWindow(); + _pingWindow.Show(); + } + + public async Task SortFastestAndMap() + { + Log.Information("Resolving all CDN data with length: {0}", _cdnList.Count); + + _cdnMap.Clear(); + + var compoundMap = new Dictionary>(); + + foreach (var cdnData in _cdnList) + { + var dataCompound = new CdnDataCompound(cdnData); + + if (!compoundMap.TryGetValue(cdnData.Id, out var compoundDataList)) + { + compoundDataList = new List(); + compoundMap.Add(cdnData.Id, compoundDataList); + } + + compoundDataList.Add(dataCompound); + } + + foreach (var (definition, list) in compoundMap.ToDictionary()) + { + switch (list.Count) + { + case 0: + compoundMap.Remove(definition); + continue; + case 1: + _cdnMap[definition] = list[0].CdnData; + Log.Information($"Skip {definition} because is have only one cdn data: {list[0].CdnData}"); + compoundMap.Remove(definition); + continue; + } + + Dispatcher.UIThread.Post(() => + { + foreach (var compound in list) + { + _pingWindow.ResolveItem(compound); + } + }); + } + + var compoundList = compoundMap.SelectMany(a => a.Value).ToList(); + Log.Information("Resolving fastest CDN. Count: {0}", compoundList.Count()); + + var compoundTask = compoundList.Select(Ping); + + await Task.WhenAll(compoundTask); + + foreach (var (key, value) in compoundMap) + { + value.Sort((a,b) => a.CompareTo(b)); + var fastest = value.First(); + + Log.Information("Resolved CDN data " + fastest.CdnData); + _cdnMap[key] = fastest.CdnData; + } + + Log.Information("Resolved all CDN data"); + } + + private async Task Ping(CdnDataCompound compound) + { + compound.Ping = await Cache.GetPingAsync(compound.CdnData.Uri.Host); + _pingWindow.ResolveItem(compound); + } + + private void Dirty() + { + File.WriteAllText(DataPath, CdnDataListSerializer.SerializeCdnList(_cdnList)); + } +} + +public sealed class CdnDataCompound: IComparable +{ + + public UriCdnData CdnData { get; set; } + public CdnPingResponse Ping { get; set; } = new CdnPingResponse(null, "Resolving"); + + public CdnDataCompound(UriCdnData cdnData) + { + CdnData = cdnData; + } + + public int CompareTo(CdnDataCompound? other) + { + if (ReferenceEquals(other, null)) + return -1; + + if (Ping.TimeoutMs.HasValue && other.Ping.TimeoutMs.HasValue) + { + int cmp = Ping.TimeoutMs.Value.CompareTo(other.Ping.TimeoutMs.Value); + if (cmp != 0) + return cmp; + } + else if (Ping.TimeoutMs.HasValue) + { + return -1; + } + else if (other.Ping.TimeoutMs.HasValue) + { + return 1; + } + + return string.Compare( + CdnData.Uri.ToString(), + other.CdnData.Uri.ToString(), + StringComparison.OrdinalIgnoreCase); + } +} + +public class PingCache +{ + private readonly ConcurrentDictionary _cache = new(); + private readonly ConcurrentDictionary> _resolvingCache = new(); + + public Task GetPingAsync(string host) + { + if (_resolvingCache.TryGetValue(host, out var cacheTask)) + { + return cacheTask; + } + + if (_cache.TryGetValue(host, out var entry)) + { + return Task.FromResult(entry); + } + + var task = InternalGetPingAsync(host); + + _resolvingCache.TryAdd(host, task); + + return task; + } + + private async Task InternalGetPingAsync(string host) + { + using var ping = new Ping(); + try + { + var reply = await ping.SendPingAsync(host, TimeSpan.FromMilliseconds(700)); + + var resp = reply.Status == IPStatus.Success + ? new CdnPingResponse((int)reply.RoundtripTime, "Successful") + : new CdnPingResponse(null, reply.Status.ToString(), true); + + _cache[host] = resp; + return resp; + } + catch (Exception ex) + { + var resp = new CdnPingResponse(null, ex.Message, true); + _cache[host] = resp; + return resp; + } + finally + { + _resolvingCache.TryRemove(host, out _); + } + } +} + +public record struct CdnPingResponse(int? TimeoutMs, string Reason = "", bool Error = false); + diff --git a/SS14.Launcher/Models/CDN/UriCdnData.cs b/SS14.Launcher/Models/CDN/UriCdnData.cs new file mode 100644 index 0000000..c7863f4 --- /dev/null +++ b/SS14.Launcher/Models/CDN/UriCdnData.cs @@ -0,0 +1,11 @@ +using System; + +namespace SS14.Launcher.Models.CDN; + +public readonly record struct UriCdnData(UriCdnDefinition Id, Uri Uri) +{ + public override string ToString() + { + return $"{Id.Name}={Uri.ToString()};"; + } +}; diff --git a/SS14.Launcher/Models/CDN/UriCdnDefinition.cs b/SS14.Launcher/Models/CDN/UriCdnDefinition.cs new file mode 100644 index 0000000..ed7c6e8 --- /dev/null +++ b/SS14.Launcher/Models/CDN/UriCdnDefinition.cs @@ -0,0 +1,12 @@ +namespace SS14.Launcher.Models.CDN; + +public record struct UriCdnDefinition(string Name) +{ + public static implicit operator UriCdnDefinition(string name) => new(name); + public static implicit operator string(UriCdnDefinition definition) => definition.Name; + + public override string ToString() + { + return Name; + } +} diff --git a/SS14.Launcher/Models/Connector.cs b/SS14.Launcher/Models/Connector.cs index 7f48414..882ab6a 100644 --- a/SS14.Launcher/Models/Connector.cs +++ b/SS14.Launcher/Models/Connector.cs @@ -33,6 +33,7 @@ public class Connector : ReactiveObject private readonly DataManager _cfg; private readonly LoginManager _loginManager; private readonly IEngineManager _engineManager; + private readonly LoginProviderManager _loginProviderManager; private ConnectionStatus _status = ConnectionStatus.None; private bool _clientExitedBadly; @@ -47,6 +48,7 @@ public Connector() _updater = Locator.Current.GetRequiredService(); _cfg = Locator.Current.GetRequiredService(); _loginManager = Locator.Current.GetRequiredService(); + _loginProviderManager = Locator.Current.GetRequiredService(); _engineManager = Locator.Current.GetRequiredService(); _http = Locator.Current.GetRequiredService(); } @@ -328,7 +330,7 @@ private async Task LaunchClientWrap( var account = _loginManager.Logins.Items.FirstOrDefault(l => info.AuthInformation.LoginUrls?.Contains(l.ServerUrl) ?? l.Server == ConfigConstants.FallbackAuthServer); var authServers = info.AuthInformation.LoginUrls?.ToString() ?? - "(Fallback) " + LoginManager.GetAuthServerById(ConfigConstants.FallbackAuthServer).AuthUrl; + "(Fallback) " + _loginProviderManager.GetAuthServerById(ConfigConstants.FallbackAuthServer).AuthUrl; if (account == null) { Log.Error("No logged in account found for any of the server's allowed auth providers: {AuthServers}", string.Join(", ", authServers)); @@ -341,8 +343,8 @@ private async Task LaunchClientWrap( _loginManager.ActiveAccount = account; } - var authServer = LoginManager.GetAuthServerById(account.Server, account.ServerUrl, - LoginManager.TryGetAccountUrl(account.Server, account.ServerUrl)).AuthUrl.ToString(); + var authServer = _loginProviderManager.GetAuthServerById(account.Server, account.ServerUrl, + _loginProviderManager.TryGetAccountUrl(account.Server, account.ServerUrl)).AuthUrl.ToString(); cVars.Add(("ROBUST_AUTH_TOKEN", account.LoginInfo.Token.Token)); cVars.Add(("ROBUST_AUTH_USERID", account.LoginInfo.UserId.ToString())); diff --git a/SS14.Launcher/Models/Data/CVars.cs b/SS14.Launcher/Models/Data/CVars.cs index e0d1030..ce9b102 100644 --- a/SS14.Launcher/Models/Data/CVars.cs +++ b/SS14.Launcher/Models/Data/CVars.cs @@ -1,5 +1,6 @@ using System; using JetBrains.Annotations; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Utility; namespace SS14.Launcher.Models.Data; @@ -114,6 +115,12 @@ public static class CVars /// Language the user selected. Null means it should be automatically selected based on system language. /// public static readonly CVarDef Language = CVarDef.Create("Language", null); + + /// + /// Cdn data for mirrors. + /// + public static readonly CVarDef CdnVars = CVarDef.Create("CdnVars", + CdnDataListSerializer.SerializeCdnList(CdnHelper.DefaultCdnList)); } /// diff --git a/SS14.Launcher/Models/Data/DataManager.cs b/SS14.Launcher/Models/Data/DataManager.cs index afdd643..26db2b3 100644 --- a/SS14.Launcher/Models/Data/DataManager.cs +++ b/SS14.Launcher/Models/Data/DataManager.cs @@ -16,6 +16,8 @@ using Microsoft.Toolkit.Mvvm.Messaging; using ReactiveUI; using Serilog; +using Splat; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Utility; namespace SS14.Launcher.Models.Data; @@ -334,9 +336,14 @@ public void Load() SetCVar(CVars.Fingerprint, Guid.NewGuid().ToString()); } + CommitConfig(); + } + + public void LoadHubs(CdnManager cdnManager) + { // If we don't have any hubs, add the defaults if (Hubs.Count == 0) - foreach (var hub in ConfigConstants.DefaultHubUrls) + foreach (var hub in cdnManager.ResolveDefinition(ConfigConstants.DefaultHubUrls)) Hubs.Add(new Hub(hub, Hubs.Count)); CommitConfig(); diff --git a/SS14.Launcher/Models/EngineManager/EngineManagerDynamic.Manifest.cs b/SS14.Launcher/Models/EngineManager/EngineManagerDynamic.Manifest.cs index a0c57a1..8b56803 100644 --- a/SS14.Launcher/Models/EngineManager/EngineManagerDynamic.Manifest.cs +++ b/SS14.Launcher/Models/EngineManager/EngineManagerDynamic.Manifest.cs @@ -68,20 +68,22 @@ private async Task UpdateBuildManifest(CancellationToken cancel, string name) { // TODO: If-Modified-Since and If-None-Match request conditions. - if (ConfigConstants.EngineBuildsUrl.TryGetValue(name, out var urlSet)) - foreach (var url in urlSet.Urls) + if (ConfigConstants.EngineBuildsUrl.TryGetValue(name, out var urlDefinition)) + { + var url = _cdnManager.ResolveDefinition(urlDefinition).ToString(); + + try { - try - { - _cachedEngineVersionInfo.Remove(name); - _cachedEngineVersionInfo.Add(name, await new UrlFallbackSet([url]).GetFromJsonAsync>(_http, cancel)); - break; - } - catch (Exception e) - { - Log.Error(e, "Failed to download manifest from {url}", url); - } + _cachedEngineVersionInfo.Remove(name); + _cachedEngineVersionInfo.Add(name, + await new UrlFallbackSet([url]) + .GetFromJsonAsync>(_http, cancel)); } + catch (Exception e) + { + Log.Error(e, "Failed to download manifest from {url}", url); + } + } _robustCacheValidUntil = _manifestStopwatch.Elapsed + ConfigConstants.RobustManifestCacheTime; } diff --git a/SS14.Launcher/Models/EngineManager/EngineManagerDynamic.cs b/SS14.Launcher/Models/EngineManager/EngineManagerDynamic.cs index 7a001b1..e5ed40a 100644 --- a/SS14.Launcher/Models/EngineManager/EngineManagerDynamic.cs +++ b/SS14.Launcher/Models/EngineManager/EngineManagerDynamic.cs @@ -13,6 +13,7 @@ using NSec.Cryptography; using Serilog; using Splat; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Models.Data; using SS14.Launcher.Utility; @@ -27,6 +28,7 @@ public sealed partial class EngineManagerDynamic : IEngineManager private readonly DataManager _cfg = Locator.Current.GetRequiredService(); private readonly HttpClient _http = Locator.Current.GetRequiredService(); + private readonly CdnManager _cdnManager = Locator.Current.GetRequiredService(); public string GetEnginePath(string engineVersion) { @@ -323,9 +325,10 @@ private static unsafe bool VerifyModuleSignature(FileStream stream, string modul public async Task GetEngineModuleManifest(string engine, CancellationToken cancel = default) { - if (!ConfigConstants.EngineBuildsUrl.TryGetValue(engine, out var urls)) + if (!ConfigConstants.EngineBuildsUrl.TryGetValue(engine, out var rawurls)) throw new InvalidOperationException("No manifest URL for engine module"); + var urls = new UrlFallbackSet([_cdnManager.ResolveDefinition(rawurls).ToString()]); if (await urls.GetFromJsonAsync(_http, cancel) is { } manifest) return manifest; diff --git a/SS14.Launcher/Models/LauncherInfoManager.cs b/SS14.Launcher/Models/LauncherInfoManager.cs index 00e6862..bbfbeca 100644 --- a/SS14.Launcher/Models/LauncherInfoManager.cs +++ b/SS14.Launcher/Models/LauncherInfoManager.cs @@ -3,13 +3,15 @@ using System.Net.Http; using System.Threading.Tasks; using Serilog; +using SS14.Launcher.Models.CDN; +using SS14.Launcher.Utility; namespace SS14.Launcher.Models; /// /// Fetches and caches information from . /// -public sealed class LauncherInfoManager(HttpClient httpClient) +public sealed class LauncherInfoManager(HttpClient httpClient, CdnManager cdnManager) { private readonly Random _messageRandom = new(); private string[]? _messages; @@ -39,8 +41,9 @@ private async Task LoadData() LauncherInfoModel? info; try { - Log.Debug("Loading launcher info... {Url}", ConfigConstants.UrlLauncherInfo); - info = await ConfigConstants.UrlLauncherInfo.GetFromJsonAsync(httpClient); + var url = cdnManager.ResolveUrlSet(ConfigConstants.UrlLauncherInfo); + Log.Debug("Loading launcher info... {Url}", url); + info = await url.GetFromJsonAsync(httpClient); if (info == null) { Log.Warning("Launcher info response was null."); diff --git a/SS14.Launcher/Models/Logins/LoginManager.cs b/SS14.Launcher/Models/Logins/LoginManager.cs index b2319ca..816c859 100644 --- a/SS14.Launcher/Models/Logins/LoginManager.cs +++ b/SS14.Launcher/Models/Logins/LoginManager.cs @@ -9,6 +9,7 @@ using ReactiveUI; using Serilog; using SS14.Launcher.Api; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Models.Data; namespace SS14.Launcher.Models.Logins; @@ -26,6 +27,7 @@ public sealed class LoginManager : ReactiveObject private readonly DataManager _cfg; private readonly AuthApi _authApi; + private readonly CdnManager _cdnManager; private IDisposable? _timer; @@ -33,6 +35,9 @@ public sealed class LoginManager : ReactiveObject private readonly IObservableCache _logins; + public ConfigConstants.AuthServer DefaultAuthServer => + _cdnManager.ResolveDefinition(ConfigConstants.AuthUrls[ConfigConstants.FallbackAuthServer]); + public Guid? ActiveAccountId { get => _activeLoginId; @@ -62,10 +67,11 @@ public LoggedInAccount? ActiveAccount public IObservableCache Logins { get; } - public LoginManager(DataManager cfg, AuthApi authApi) + public LoginManager(DataManager cfg, AuthApi authApi, CdnManager cdnManager) { _cfg = cfg; _authApi = authApi; + _cdnManager = cdnManager; _logins = _cfg.Logins .Connect() @@ -193,42 +199,6 @@ private async Task UpdateSingleAccountStatus(ActiveLoginData data) } } - public static ConfigConstants.AuthServer GetAuthServerById(string serverId, string? customAuthUrl = null, string? customAccountSite = null) - { - if (serverId != ConfigConstants.CustomAuthServer) - return ConfigConstants.AuthUrls[serverId]; - - if (customAuthUrl == null) - throw new ArgumentException("Custom server selected but no custom URLs provided."); - - customAccountSite ??= TryGetAccountUrl(serverId, customAuthUrl); - if (customAccountSite == null) - throw new ArgumentException("Failed to get account URL for custom server."); - - return new ConfigConstants.AuthServer(new(customAuthUrl), new(customAccountSite)); - } - - public static string? TryGetAccountUrl(string serverId, string? customAuthUrl = null) - { - if (serverId != ConfigConstants.CustomAuthServer) - return GetAuthServerById(serverId).AccountSite.ToString(); - - if (customAuthUrl == null) - throw new ArgumentException("Custom server selected but no custom URLs provided."); - - // Make an http request to the custom URL to get the account URL - var http = HappyEyeballsHttp.CreateHttpClient(); - var response = http.GetAsync(new Uri(customAuthUrl) + ConfigConstants.TemplateAuthServer.AuthAccountSitePath).Result; - http.Dispose(); - if (!response.IsSuccessStatusCode) - { - Log.Error("Failed to get account URL from custom auth server with status {status}", response.StatusCode); - return null; - } - - return response.Content.AsJson().Result.WebBaseUrl; - } - private sealed class ActiveLoginData : LoggedInAccount { private AccountLoginStatus _status; @@ -245,6 +215,4 @@ public void SetStatus(AccountLoginStatus status) Log.Debug("Setting status for login {account} to {status}", LoginInfo, status); } } - - private sealed record AccountSiteResponse(string WebBaseUrl); } diff --git a/SS14.Launcher/Models/Logins/LoginProviderManager.cs b/SS14.Launcher/Models/Logins/LoginProviderManager.cs new file mode 100644 index 0000000..32ec836 --- /dev/null +++ b/SS14.Launcher/Models/Logins/LoginProviderManager.cs @@ -0,0 +1,55 @@ +using System; +using Serilog; +using SS14.Launcher.Models.CDN; + +namespace SS14.Launcher.Models.Logins; + +public sealed class LoginProviderManager +{ + private readonly CdnManager _cdnManager; + + public LoginProviderManager(CdnManager cdnManager) + { + _cdnManager = cdnManager; + } + + public ConfigConstants.AuthServer GetAuthServerById(string serverId, string? customAuthUrl = null, string? customAccountSite = null) + { + if (serverId != ConfigConstants.CustomAuthServer) + { + return _cdnManager.ResolveDefinition(ConfigConstants.AuthUrls[serverId]); + } + + if (customAuthUrl == null) + throw new ArgumentException("Custom server selected but no custom URLs provided."); + + customAccountSite ??= TryGetAccountUrl(serverId, customAuthUrl); + if (customAccountSite == null) + throw new ArgumentException("Failed to get account URL for custom server."); + + return new ConfigConstants.AuthServer(new(customAuthUrl), new(customAccountSite)); + } + + public string? TryGetAccountUrl(string serverId, string? customAuthUrl = null) + { + if (serverId != ConfigConstants.CustomAuthServer) + return GetAuthServerById(serverId).AccountSite.ToString(); + + if (customAuthUrl == null) + throw new ArgumentException("Custom server selected but no custom URLs provided."); + + // Make an http request to the custom URL to get the account URL + var http = HappyEyeballsHttp.CreateHttpClient(); + var response = http.GetAsync(new Uri(customAuthUrl) + ConfigConstants.TemplateAuthServer.AuthAccountSitePath).Result; + http.Dispose(); + if (!response.IsSuccessStatusCode) + { + Log.Error("Failed to get account URL from custom auth server with status {status}", response.StatusCode); + return null; + } + + return response.Content.AsJson().Result.WebBaseUrl; + } + + private sealed record AccountSiteResponse(string WebBaseUrl); +} diff --git a/SS14.Launcher/Models/OverrideAssets/OverrideAssetsManager.cs b/SS14.Launcher/Models/OverrideAssets/OverrideAssetsManager.cs index 46be9a2..6798ceb 100644 --- a/SS14.Launcher/Models/OverrideAssets/OverrideAssetsManager.cs +++ b/SS14.Launcher/Models/OverrideAssets/OverrideAssetsManager.cs @@ -10,6 +10,7 @@ using Dapper; using Microsoft.Data.Sqlite; using Serilog; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Models.Data; namespace SS14.Launcher.Models.OverrideAssets; @@ -19,6 +20,7 @@ public sealed class OverrideAssetsManager private readonly DataManager _dataManager; private readonly HttpClient _httpClient; private readonly LauncherInfoManager _infoManager; + private readonly CdnManager _cdnManager; private bool _overridesUpdated; @@ -26,11 +28,12 @@ public sealed class OverrideAssetsManager public event Action? AssetsChanged; - public OverrideAssetsManager(DataManager dataManager, HttpClient httpClient, LauncherInfoManager infoManager) + public OverrideAssetsManager(DataManager dataManager, HttpClient httpClient, LauncherInfoManager infoManager, CdnManager cdnManager) { _dataManager = dataManager; _httpClient = httpClient; _infoManager = infoManager; + _cdnManager = cdnManager; } public void Initialize() @@ -143,7 +146,7 @@ private async Task UpdateAssetsBody(CancellationToken cancel) Log.Debug("Downloading override asset for {AssetName}: {OverrideName}", name, overrideName); - var url = ConfigConstants.UrlAssetsBase + overrideName; + var url = _cdnManager.ResolveUrlSet(ConfigConstants.UrlAssetsBase) + overrideName; var data = await url.GetByteArrayAsync(_httpClient, cancel); db.Execute("INSERT OR REPLACE INTO OverrideAsset(Name, OverrideName, Data) VALUES (@Name, @OverrideName, @Data)", diff --git a/SS14.Launcher/Models/ServerStatus/ServerListCache.cs b/SS14.Launcher/Models/ServerStatus/ServerListCache.cs index fd380b1..18d0d8e 100644 --- a/SS14.Launcher/Models/ServerStatus/ServerListCache.cs +++ b/SS14.Launcher/Models/ServerStatus/ServerListCache.cs @@ -126,7 +126,8 @@ public async void RefreshServerList(CancellationToken cancel) _allServers.AddItems(entries.Select(entry => { var statusData = new ServerStatusData(entry.Address, entry.HubAddress); - ServerStatusCache.ApplyStatus(statusData, entry.StatusData); + var cache = new ServerStatusCache(); + cache.ApplyStatus(statusData, entry.StatusData); return statusData; })); diff --git a/SS14.Launcher/Models/ServerStatus/ServerStatusCache.cs b/SS14.Launcher/Models/ServerStatus/ServerStatusCache.cs index c7f3064..bb9c454 100644 --- a/SS14.Launcher/Models/ServerStatus/ServerStatusCache.cs +++ b/SS14.Launcher/Models/ServerStatus/ServerStatusCache.cs @@ -11,6 +11,7 @@ using Serilog; using Splat; using SS14.Launcher.Api; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Utility; namespace SS14.Launcher.Models.ServerStatus; @@ -24,10 +25,12 @@ public sealed class ServerStatusCache : IServerSource // Oh well! private readonly Dictionary _cachedData = new(); private readonly HttpClient _http; + private readonly CdnManager _cdnManager; public ServerStatusCache() { _http = Locator.Current.GetRequiredService(); + _cdnManager = Locator.Current.GetRequiredService(); } /// @@ -75,7 +78,7 @@ private async Task UpdateStatusFor(CacheReg reg) } } - public static async Task UpdateStatusFor(ServerStatusData data, HttpClient http, CancellationToken cancel) + public async Task UpdateStatusFor(ServerStatusData data, HttpClient http, CancellationToken cancel) { try { @@ -119,7 +122,7 @@ public static async Task UpdateStatusFor(ServerStatusData data, HttpClient http, } } - public static void ApplyStatus(ServerStatusData data, ServerApi.ServerStatus status) + public void ApplyStatus(ServerStatusData data, ServerApi.ServerStatus status) { data.Status = ServerStatusCode.Online; data.Name = status.Name; @@ -149,7 +152,7 @@ public static void ApplyStatus(ServerStatusData data, ServerApi.ServerStatus sta var inferredTags = ServerTagInfer.InferTags(status); data.Tags = baseTags.Concat(inferredTags).ToArray(); - data.Auths = status.AuthMethods ?? [ConfigConstants.AuthUrls[ConfigConstants.FallbackAuthServer].AuthUrl.AbsoluteUri]; + data.Auths = status.AuthMethods ?? [_cdnManager.ResolveDefinition(ConfigConstants.AuthUrls[ConfigConstants.FallbackAuthServer]).AuthUrl.AbsoluteUri]; } public static async void UpdateInfoForCore(ServerStatusData data, Func> fetch) diff --git a/SS14.Launcher/Program.cs b/SS14.Launcher/Program.cs index 81787f0..2a84f67 100644 --- a/SS14.Launcher/Program.cs +++ b/SS14.Launcher/Program.cs @@ -15,6 +15,7 @@ using SS14.Launcher.Api; using SS14.Launcher.Localization; using SS14.Launcher.Models; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Models.ContentManagement; using SS14.Launcher.Models.Data; using SS14.Launcher.Models.ServerStatus; @@ -91,7 +92,10 @@ public static void Main(string[] args) var cfg = new DataManager(); cfg.Load(); + var cdnManager = new CdnManager(); + Locator.CurrentMutable.RegisterConstant(cfg); + Locator.CurrentMutable.RegisterConstant(cdnManager); CheckWindowsVersion(); // Bad antivirus check disabled: I assume Avast/AVG fixed their shit. @@ -119,7 +123,7 @@ public static void Main(string[] args) try { - BuildAvaloniaApp(cfg).StartWithClassicDesktopLifetime(args); + BuildAvaloniaApp(cfg, cdnManager).StartWithClassicDesktopLifetime(args); } finally { @@ -209,7 +213,7 @@ private static void CheckWine(DataManager dataManager) } // Avalonia configuration, don't remove; also used by visual designer. - private static AppBuilder BuildAvaloniaApp(DataManager cfg) + private static AppBuilder BuildAvaloniaApp(DataManager cfg, CdnManager cdnManager) { var locator = Locator.CurrentMutable; @@ -221,13 +225,15 @@ private static AppBuilder BuildAvaloniaApp(DataManager cfg) Locator.CurrentMutable.RegisterConstant(http); var loc = new LocalizationManager(cfg); - var authApi = new AuthApi(http); + var loginProviderManager = new LoginProviderManager(cdnManager); + var authApi = new AuthApi(http, loginProviderManager); var hubApi = new HubApi(http); - var launcherInfo = new LauncherInfoManager(http); - var overrideAssets = new OverrideAssetsManager(cfg, http, launcherInfo); - var loginManager = new LoginManager(cfg, authApi); + var launcherInfo = new LauncherInfoManager(http, cdnManager); + var overrideAssets = new OverrideAssetsManager(cfg, http, launcherInfo, cdnManager); + var loginManager = new LoginManager(cfg, authApi, cdnManager); locator.RegisterConstant(loc); + locator.RegisterConstant(loginProviderManager); locator.RegisterConstant(new ContentManager()); locator.RegisterConstant(new EngineManagerDynamic()); locator.RegisterConstant(new Updater()); diff --git a/SS14.Launcher/ViewModels/HubSettingsViewModel.cs b/SS14.Launcher/ViewModels/HubSettingsViewModel.cs index cb040b2..861ccf3 100644 --- a/SS14.Launcher/ViewModels/HubSettingsViewModel.cs +++ b/SS14.Launcher/ViewModels/HubSettingsViewModel.cs @@ -5,6 +5,7 @@ using System.Linq; using DynamicData; using Splat; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Models.Data; using SS14.Launcher.Utility; @@ -12,10 +13,11 @@ namespace SS14.Launcher.ViewModels; public class HubSettingsViewModel : ViewModelBase { - public Uri[] DefaultHubs => ConfigConstants.DefaultHubUrls; + public Uri[] DefaultHubs => _cdnManager.ResolveDefinition(ConfigConstants.DefaultHubUrls).ToArray(); public ObservableCollection HubList { get; set; } = new(); private readonly DataManager _dataManager = Locator.Current.GetRequiredService(); + private readonly CdnManager _cdnManager = Locator.Current.GetRequiredService(); public void Save() { diff --git a/SS14.Launcher/ViewModels/Login/AuthTfaViewModel.cs b/SS14.Launcher/ViewModels/Login/AuthTfaViewModel.cs index 71cd006..4682057 100644 --- a/SS14.Launcher/ViewModels/Login/AuthTfaViewModel.cs +++ b/SS14.Launcher/ViewModels/Login/AuthTfaViewModel.cs @@ -11,6 +11,7 @@ public sealed class AuthTfaViewModel : BaseLoginViewModel { private readonly AuthApi.AuthenticateRequest _request; private readonly LoginManager _loginMgr; + private readonly LoginProviderManager _loginProviderManager; private readonly AuthApi _authApi; private readonly DataManager _cfg; @@ -22,11 +23,13 @@ public AuthTfaViewModel( MainWindowLoginViewModel parentVm, AuthApi.AuthenticateRequest request, LoginManager loginMgr, + LoginProviderManager loginProviderManager, AuthApi authApi, DataManager cfg) : base(parentVm) { _request = request; _loginMgr = loginMgr; + _loginProviderManager = loginProviderManager; _authApi = authApi; _cfg = cfg; @@ -75,7 +78,7 @@ public void RecoveryCode() { // I don't want to implement recovery code stuff, so if you need them, // bloody use them to disable your authenticator app online. - Helpers.OpenUri(LoginManager.GetAuthServerById(_request.Server ?? ConfigConstants.FallbackAuthServer, _request.ServerUrl).AccountManUrl); + Helpers.OpenUri(_loginProviderManager.GetAuthServerById(_request.Server ?? ConfigConstants.FallbackAuthServer, _request.ServerUrl).AccountManUrl); } public void Cancel() diff --git a/SS14.Launcher/ViewModels/Login/ForgotPasswordViewModel.cs b/SS14.Launcher/ViewModels/Login/ForgotPasswordViewModel.cs index 7669c83..4fb8cc7 100644 --- a/SS14.Launcher/ViewModels/Login/ForgotPasswordViewModel.cs +++ b/SS14.Launcher/ViewModels/Login/ForgotPasswordViewModel.cs @@ -27,7 +27,7 @@ public sealed class ForgotPasswordViewModel : BaseLoginViewModel public ForgotPasswordViewModel( MainWindowLoginViewModel parentVM, - AuthApi authApi) + AuthApi authApi, LoginProviderManager loginProviderManager) : base(parentVM) { _authApi = authApi; @@ -36,7 +36,7 @@ public ForgotPasswordViewModel( .Subscribe(s => { IsCustom = Server == ConfigConstants.CustomAuthServer; - ServerUrlPlaceholder = LoginManager.GetAuthServerById(IsCustom ? ConfigConstants.AuthUrls.First().Key : Server).AuthUrl.ToString(); + ServerUrlPlaceholder = loginProviderManager.GetAuthServerById(IsCustom ? ConfigConstants.AuthUrls.First().Key : Server).AuthUrl.ToString(); IsServerPotentiallyValid = !IsCustom || !Busy && !string.IsNullOrEmpty(EditingEmail) && Uri.TryCreate(ServerUrl, UriKind.Absolute, out _); }); } diff --git a/SS14.Launcher/ViewModels/Login/LoginViewModel.cs b/SS14.Launcher/ViewModels/Login/LoginViewModel.cs index c5551f3..5c8404c 100644 --- a/SS14.Launcher/ViewModels/Login/LoginViewModel.cs +++ b/SS14.Launcher/ViewModels/Login/LoginViewModel.cs @@ -16,6 +16,7 @@ public class LoginViewModel : BaseLoginViewModel { private readonly AuthApi _authApi; private readonly LoginManager _loginMgr; + private readonly LoginProviderManager _loginProviderManager; private readonly DataManager _dataManager; private readonly LocalizationManager _loc = LocalizationManager.Instance; @@ -33,11 +34,12 @@ public class LoginViewModel : BaseLoginViewModel [Reactive] public bool IsPasswordVisible { get; set; } public LoginViewModel(MainWindowLoginViewModel parentVm, AuthApi authApi, - LoginManager loginMgr, DataManager dataManager) : base(parentVm) + LoginManager loginMgr, LoginProviderManager loginProviderManager, DataManager dataManager) : base(parentVm) { BusyText = _loc.GetString("login-login-busy-logging-in"); _authApi = authApi; _loginMgr = loginMgr; + _loginProviderManager = loginProviderManager; _dataManager = dataManager; this.WhenAnyValue(x => x.Server, x => x.ServerUrl, x => x.EditingUsername, x => x.EditingPassword) @@ -47,7 +49,7 @@ public LoginViewModel(MainWindowLoginViewModel parentVm, AuthApi authApi, ? !string.IsNullOrEmpty(s.Item2) && !string.IsNullOrEmpty(s.Item2) && !string.IsNullOrEmpty(s.Item3) : !string.IsNullOrEmpty(s.Item1) && !string.IsNullOrEmpty(s.Item3); IsCustom = Server == ConfigConstants.CustomAuthServer; - ServerUrlPlaceholder = LoginManager.GetAuthServerById(IsCustom ? ConfigConstants.AuthUrls.First().Key : Server).AuthUrl.ToString(); + ServerUrlPlaceholder = loginProviderManager.GetAuthServerById(IsCustom ? ConfigConstants.AuthUrls.First().Key : Server).AuthUrl.ToString(); IsServerPotentiallyValid = !IsCustom || !Busy && Uri.TryCreate(ServerUrl, UriKind.Absolute, out _); }); } @@ -87,7 +89,7 @@ public static async Task DoLogin( if (resp.IsSuccess) { var loginInfo = resp.LoginInfo; - loginInfo.ServerUrl ??= ConfigConstants.AuthUrls[ConfigConstants.FallbackAuthServer].AuthUrl.AbsoluteUri; + loginInfo.ServerUrl ??= loginMgr.DefaultAuthServer.AuthUrl.AbsoluteUri; Log.Information($"Login successful for user {loginInfo.UserId} on server {loginInfo.Server} at {loginInfo.ServerUrl}"); var oldLogin = loginMgr.Logins.KeyValues.FirstOrDefault(a => a.Key == loginInfo.UserId && a.Value.Server == loginInfo.Server @@ -125,7 +127,7 @@ public static async Task DoLogin( // Registration is purely via website for now public void RegisterPressed() => - Helpers.OpenUri(LoginManager.GetAuthServerById(Server, ServerUrl).AccountRegUrl); + Helpers.OpenUri(_loginProviderManager.GetAuthServerById(Server, ServerUrl).AccountRegUrl); public void ResendConfirmationPressed() => - Helpers.OpenUri(LoginManager.GetAuthServerById(Server, ServerUrl).AccountResendUrl); + Helpers.OpenUri(_loginProviderManager.GetAuthServerById(Server, ServerUrl).AccountResendUrl); } diff --git a/SS14.Launcher/ViewModels/Login/RegisterViewModel.cs b/SS14.Launcher/ViewModels/Login/RegisterViewModel.cs index c9615a1..8853843 100644 --- a/SS14.Launcher/ViewModels/Login/RegisterViewModel.cs +++ b/SS14.Launcher/ViewModels/Login/RegisterViewModel.cs @@ -36,7 +36,12 @@ public class RegisterViewModel : BaseLoginViewModel [Reactive] public bool Is13OrOlder { get; set; } - public RegisterViewModel(MainWindowLoginViewModel parentVm, DataManager cfg, AuthApi authApi, LoginManager loginMgr) + public RegisterViewModel( + MainWindowLoginViewModel parentVm, + DataManager cfg, + AuthApi authApi, + LoginManager loginMgr, + LoginProviderManager loginProviderManager) : base(parentVm) { _cfg = cfg; @@ -51,7 +56,7 @@ public RegisterViewModel(MainWindowLoginViewModel parentVm, DataManager cfg, Aut .Subscribe(s => { IsCustom = Server == ConfigConstants.CustomAuthServer; - ServerUrlPlaceholder = LoginManager.GetAuthServerById(IsCustom ? ConfigConstants.AuthUrls.First().Key : Server).AuthUrl.ToString(); + ServerUrlPlaceholder = loginProviderManager.GetAuthServerById(IsCustom ? ConfigConstants.AuthUrls.First().Key : Server).AuthUrl.ToString(); IsServerPotentiallyValid = !IsCustom || !Busy && !string.IsNullOrEmpty(EditingEmail) && Uri.TryCreate(ServerUrl, UriKind.Absolute, out _); }); } diff --git a/SS14.Launcher/ViewModels/Login/ResendConfirmationViewModel.cs b/SS14.Launcher/ViewModels/Login/ResendConfirmationViewModel.cs index 567b063..99a0eca 100644 --- a/SS14.Launcher/ViewModels/Login/ResendConfirmationViewModel.cs +++ b/SS14.Launcher/ViewModels/Login/ResendConfirmationViewModel.cs @@ -23,7 +23,7 @@ public class ResendConfirmationViewModel : BaseLoginViewModel private bool _errored; - public ResendConfirmationViewModel(MainWindowLoginViewModel parentVM, AuthApi authApi) : base(parentVM) + public ResendConfirmationViewModel(MainWindowLoginViewModel parentVM, AuthApi authApi, LoginProviderManager loginProviderManager) : base(parentVM) { _authApi = authApi; @@ -31,7 +31,7 @@ public ResendConfirmationViewModel(MainWindowLoginViewModel parentVM, AuthApi au .Subscribe(s => { IsCustom = Server == ConfigConstants.CustomAuthServer; - ServerUrlPlaceholder = LoginManager.GetAuthServerById(IsCustom ? ConfigConstants.AuthUrls.First().Key : Server).AuthUrl.ToString(); + ServerUrlPlaceholder = loginProviderManager.GetAuthServerById(IsCustom ? ConfigConstants.AuthUrls.First().Key : Server).AuthUrl.ToString(); IsServerPotentiallyValid = !IsCustom || !Busy && !string.IsNullOrEmpty(EditingEmail) && Uri.TryCreate(ServerUrl, UriKind.Absolute, out _); }); } diff --git a/SS14.Launcher/ViewModels/MainWindowLoginViewModel.cs b/SS14.Launcher/ViewModels/MainWindowLoginViewModel.cs index 8430e70..1e6964a 100644 --- a/SS14.Launcher/ViewModels/MainWindowLoginViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowLoginViewModel.cs @@ -14,6 +14,7 @@ public class MainWindowLoginViewModel : ViewModelBase private readonly AuthApi _authApi; private readonly LoginManager _loginMgr; private BaseLoginViewModel _screen; + private readonly LoginProviderManager _loginProviderMgr; public LanguageSelectorViewModel LanguageSelector { get; } = new(); @@ -32,6 +33,7 @@ public MainWindowLoginViewModel() _cfg = Locator.Current.GetRequiredService(); _authApi = Locator.Current.GetRequiredService(); _loginMgr = Locator.Current.GetRequiredService(); + _loginProviderMgr = Locator.Current.GetRequiredService(); _screen = default!; SwitchToLogin(); @@ -39,7 +41,7 @@ public MainWindowLoginViewModel() public void SwitchToLogin() { - Screen = new LoginViewModel(this, _authApi, _loginMgr, _cfg); + Screen = new LoginViewModel(this, _authApi, _loginMgr, _loginProviderMgr, _cfg); } public void SwitchToExpiredLogin(LoggedInAccount account) @@ -49,22 +51,22 @@ public void SwitchToExpiredLogin(LoggedInAccount account) public void SwitchToRegister() { - Screen = new RegisterViewModel(this, _cfg, _authApi, _loginMgr); + Screen = new RegisterViewModel(this, _cfg, _authApi, _loginMgr, _loginProviderMgr); } public void SwitchToForgotPassword() { - Screen = new ForgotPasswordViewModel(this, _authApi); + Screen = new ForgotPasswordViewModel(this, _authApi, _loginProviderMgr); } public void SwitchToAuthTfa(AuthApi.AuthenticateRequest request) { - Screen = new AuthTfaViewModel(this, request, _loginMgr, _authApi, _cfg); + Screen = new AuthTfaViewModel(this, request, _loginMgr, _loginProviderMgr, _authApi, _cfg); } public void SwitchToResendConfirmation() { - Screen = new ResendConfirmationViewModel(this, _authApi); + Screen = new ResendConfirmationViewModel(this, _authApi, _loginProviderMgr); } public void SwitchToRegisterNeedsConfirmation(string server, string? serverUrl, string username, string password) diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs index ab5a312..ae9e04a 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs @@ -9,6 +9,7 @@ using ReactiveUI.Fody.Helpers; using Splat; using SS14.Launcher.Localization; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Models.Data; using SS14.Launcher.Models.Logins; using SS14.Launcher.Models.ServerStatus; @@ -33,7 +34,7 @@ public HomePageViewModel(MainWindowViewModel mainWindowViewModel) _cfg.FavoriteServers .Connect() .Select(x => - new ServerEntryViewModel(MainWindowViewModel, _statusCache.GetStatusFor(x.Address), x, _statusCache, _cfg, Locator.Current.GetRequiredService()) + new ServerEntryViewModel(MainWindowViewModel, _statusCache.GetStatusFor(x.Address), x, _statusCache, _cfg, Locator.Current.GetRequiredService(), Locator.Current.GetRequiredService()) { ViewedInFavoritesPane = true, IsExpanded = _cfg.ExpandedServers.Contains(x.Address) }) .OnItemAdded(a => { diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/OptionsTabViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/OptionsTabViewModel.cs index c4d9e24..cb22033 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/OptionsTabViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/OptionsTabViewModel.cs @@ -16,6 +16,7 @@ public class OptionsTabViewModel : MainWindowTabViewModel private readonly IEngineManager _engineManager; private readonly ContentManager _contentManager; private readonly LoginManager _loginMgr; + private readonly LoginProviderManager _loginProviderMgr; public LanguageSelectorViewModel Language { get; } = new(); @@ -23,6 +24,7 @@ public OptionsTabViewModel() { Cfg = Locator.Current.GetRequiredService(); _loginMgr = Locator.Current.GetRequiredService(); + _loginProviderMgr = Locator.Current.GetRequiredService(); _engineManager = Locator.Current.GetRequiredService(); _contentManager = Locator.Current.GetRequiredService(); @@ -98,6 +100,7 @@ public double UiScalingX } private double _uiScalingY; + public double UiScalingY { get => _uiScalingY; @@ -147,8 +150,8 @@ public void OpenLogDirectory() public void OpenAccountSettings() { if (_loginMgr.ActiveAccount is not { } account - || LoginManager.TryGetAccountUrl(account.Server, account.ServerUrl) is not { } url) + || _loginProviderMgr.TryGetAccountUrl(account.Server, account.ServerUrl) is not { } url) return; - Helpers.OpenUri(LoginManager.GetAuthServerById(account.Server, account.ServerUrl, url).AccountManUrl); + Helpers.OpenUri(_loginProviderMgr.GetAuthServerById(account.Server, account.ServerUrl, url).AccountManUrl); } } diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/ServerEntryViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/ServerEntryViewModel.cs index 450b547..80b7537 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/ServerEntryViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/ServerEntryViewModel.cs @@ -7,6 +7,7 @@ using Microsoft.Toolkit.Mvvm.Messaging; using SS14.Launcher.Api; using SS14.Launcher.Localization; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Models.Data; using SS14.Launcher.Models.Logins; using SS14.Launcher.Models.ServerStatus; @@ -22,19 +23,21 @@ public sealed class ServerEntryViewModel : ObservableRecipient, IRecipient _cacheData.Address; private string _fallbackName = string.Empty; private bool _isExpanded; public ServerEntryViewModel(MainWindowViewModel windowVm, ServerStatusData cacheData, IServerSource serverSource, - DataManager cfg, LoginManager loginManager) + DataManager cfg, LoginManager loginManager, CdnManager cdnManager) { _windowVm = windowVm; _cacheData = cacheData; _serverSource = serverSource; _cfg = cfg; _loginManager = loginManager; + _cdnManager = cdnManager; } public ServerEntryViewModel( @@ -43,8 +46,8 @@ public ServerEntryViewModel( FavoriteServer favorite, IServerSource serverSource, DataManager cfg, - LoginManager loginManager) - : this(windowVm, cacheData, serverSource, cfg, loginManager) + LoginManager loginManager, CdnManager cdnManager) + : this(windowVm, cacheData, serverSource, cfg, loginManager, cdnManager) { Favorite = favorite; } @@ -54,8 +57,8 @@ public ServerEntryViewModel( ServerStatusDataWithFallbackName ssdfb, IServerSource serverSource, DataManager cfg, - LoginManager loginManager) - : this(windowVm, ssdfb.Data, serverSource, cfg, loginManager) + LoginManager loginManager, CdnManager cdnManager) + : this(windowVm, ssdfb.Data, serverSource, cfg, loginManager, cdnManager) { FallbackName = ssdfb.FallbackName ?? ""; } @@ -75,7 +78,7 @@ public void ConnectPressed() } // If there are multiple supported auth methods and we haven't picked a matching one, show a dialog to select an account that fits - var dialog = new SelectAccountDialog(_cacheData.Auths, _loginManager); + var dialog = new SelectAccountDialog(_cacheData.Auths, _loginManager, _cdnManager); dialog.Closed += (_, _) => { if (dialog.SelectedAccount != null) diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/ServerListTabViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/ServerListTabViewModel.cs index 2c14530..e003eeb 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/ServerListTabViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/ServerListTabViewModel.cs @@ -7,6 +7,7 @@ using ReactiveUI.Fody.Helpers; using Splat; using SS14.Launcher.Localization; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Models.Logins; using SS14.Launcher.Models.ServerStatus; using SS14.Launcher.Utility; @@ -137,7 +138,7 @@ private void UpdateSearchedList() SearchedServers.Clear(); foreach (var server in sortList) { - var vm = new ServerEntryViewModel(_windowVm, server, _serverListCache, _windowVm.Cfg, Locator.Current.GetRequiredService()); + var vm = new ServerEntryViewModel(_windowVm, server, _serverListCache, _windowVm.Cfg, Locator.Current.GetRequiredService(), Locator.Current.GetRequiredService()); SearchedServers.Add(vm); } diff --git a/SS14.Launcher/Views/SelectAccountDialog.xaml.cs b/SS14.Launcher/Views/SelectAccountDialog.xaml.cs index c8c6ba3..c82d9a4 100644 --- a/SS14.Launcher/Views/SelectAccountDialog.xaml.cs +++ b/SS14.Launcher/Views/SelectAccountDialog.xaml.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using SS14.Launcher.Models.CDN; using SS14.Launcher.Models.Logins; using SS14.Launcher.Utility; @@ -12,12 +13,12 @@ public partial class SelectAccountDialog : Window public IEnumerable Accounts { get; set; } public bool Error { get; set; } - public SelectAccountDialog(string[] authMethods, LoginManager loginManager) + public SelectAccountDialog(string[] authMethods, LoginManager loginManager, CdnManager cdnManager) { InitializeComponent(); Accounts = loginManager.Logins.KeyValues - .Where(x => authMethods.FirstOrDefault(m => m == ConfigConstants.AuthUrls[x.Value.Server].AuthUrl.AbsoluteUri) != null) + .Where(x => authMethods.FirstOrDefault(m => m == cdnManager.ResolveDefinition(ConfigConstants.AuthUrls[x.Value.Server]).AuthUrl.AbsoluteUri) != null) .Select(x => x.Value); Error = !Accounts.Any(); }