diff --git a/Bloxstrap/Models/ClientFlagSettings.cs b/Bloxstrap/Models/ClientFlagSettings.cs new file mode 100644 index 0000000..b50932b --- /dev/null +++ b/Bloxstrap/Models/ClientFlagSettings.cs @@ -0,0 +1,8 @@ +namespace Bloxstrap.Models +{ + public class ClientFlagSettings + { + [JsonPropertyName("applicationSettings")] + public Dictionary? ApplicationSettings { get; set; } + } +} diff --git a/Bloxstrap/RobloxDeployment.cs b/Bloxstrap/RobloxDeployment.cs index d064e8c..7f00336 100644 --- a/Bloxstrap/RobloxDeployment.cs +++ b/Bloxstrap/RobloxDeployment.cs @@ -15,12 +15,12 @@ { "https://setup.rbxcdn.com", 0 }, { "https://setup-ak.rbxcdn.com", 2 }, { "https://roblox-setup.cachefly.net", 2 }, - { "https://s3.amazonaws.com/setup.roblox.com", 4 } + { "https://s3.amazonaws.com/setup.roblox.com", 4 } }; private static async Task TestConnection(string url, int priority) { - string LOG_IDENT = $"DeployManager::TestConnection.{url}"; + string LOG_IDENT = $"RobloxDeployment::TestConnection.{url}"; await Task.Delay(priority * 1000); @@ -47,7 +47,7 @@ public static async Task InitializeConnectivity() { - const string LOG_IDENT = "DeployManager::InitializeConnectivity"; + const string LOG_IDENT = "RobloxDeployment::InitializeConnectivity"; // this function serves double duty as the setup mirror enumerator, and as our connectivity check // since we're basically asking four different urls for the exact same thing, if all four fail, then it has to be a user-side problem @@ -95,7 +95,16 @@ string location = BaseUrl; if (channel.ToLowerInvariant() != DefaultChannel.ToLowerInvariant()) - location += $"/channel/{channel.ToLowerInvariant()}"; + { + string channelName; + + if (RobloxFastFlags.GetSettings(nameof(RobloxFastFlags.PCClientBootstrapper), channel).Get("FFlagReplaceChannelNameForDownload")) + channelName = "common"; + else + channelName = channel.ToLowerInvariant(); + + location += $"/channel/{channelName}"; + } location += resource; @@ -123,7 +132,6 @@ try { - // TODO - this needs to try both clientsettings and clientsettingscdn deployInfoResponse = await App.HttpClient.GetAsync("https://clientsettingscdn.roblox.com" + path); } catch (Exception ex) diff --git a/Bloxstrap/RobloxFastFlags.cs b/Bloxstrap/RobloxFastFlags.cs new file mode 100644 index 0000000..6121861 --- /dev/null +++ b/Bloxstrap/RobloxFastFlags.cs @@ -0,0 +1,144 @@ +using System.ComponentModel; + +namespace Bloxstrap +{ + public class RobloxFastFlags + { + private string _applicationName; + private string _channelName; + + private bool _initialised = false; + private Dictionary? _flags; + + private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); + + private RobloxFastFlags(string applicationName, string channelName) + { + _applicationName = applicationName; + _channelName = channelName; + } + + private async Task Fetch() + { + if (_initialised) + return; + + await semaphoreSlim.WaitAsync(); + try + { + if (_initialised) + return; + + string logIndent = $"RobloxFastFlags::Fetch.{_applicationName}.{_channelName}"; + App.Logger.WriteLine(logIndent, "Fetching fast flags"); + + string path = $"/v2/settings/application/{_applicationName}"; + if (_channelName != RobloxDeployment.DefaultChannel.ToLowerInvariant()) + path += $"/bucket/{_channelName}"; + + HttpResponseMessage response; + + try + { + response = await App.HttpClient.GetAsync("https://clientsettingscdn.roblox.com" + path); + } + catch (Exception ex) + { + App.Logger.WriteLine(logIndent, "Failed to contact clientsettingscdn! Falling back to clientsettings..."); + App.Logger.WriteException(logIndent, ex); + + response = await App.HttpClient.GetAsync("https://clientsettings.roblox.com" + path); + } + + string rawResponse = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + App.Logger.WriteLine(logIndent, + "Failed to fetch client settings!\r\n" + + $"\tStatus code: {response.StatusCode}\r\n" + + $"\tResponse: {rawResponse}" + ); + + throw new HttpResponseException(response); + } + + var clientSettings = JsonSerializer.Deserialize(rawResponse); + + if (clientSettings == null) + throw new Exception("Deserialised client settings is null!"); + + if (clientSettings.ApplicationSettings == null) + throw new Exception("Deserialised application settings is null!"); + + _flags = clientSettings.ApplicationSettings; + _initialised = true; + } + finally + { + semaphoreSlim.Release(); + } + } + + public async Task GetAsync(string name) + { + await Fetch(); + + if (!_flags!.ContainsKey(name)) + return default; + + string value = _flags[name]; + + try + { + var converter = TypeDescriptor.GetConverter(typeof(T)); + if (converter == null) + return default; + + return (T?)converter.ConvertFromString(value); + } + catch (NotSupportedException) // boohoo + { + return default; + } + } + + public T? Get(string name) + { + return GetAsync(name).Result; + } + + // _cache[applicationName][channelName] + private static Dictionary> _cache = new(); + + public static RobloxFastFlags PCDesktopClient { get; } = GetSettings("PCDesktopClient"); + public static RobloxFastFlags PCClientBootstrapper { get; } = GetSettings("PCClientBootstrapper"); + + public static RobloxFastFlags GetSettings(string applicationName, string? channelName = null, bool shouldCache = true) + { + string channelNameLower; + if (!string.IsNullOrEmpty(channelName)) + channelNameLower = channelName.ToLowerInvariant(); + else + channelNameLower = App.Settings.Prop.Channel.ToLowerInvariant(); + + lock (_cache) + { + if (_cache.ContainsKey(applicationName) && _cache[applicationName].ContainsKey(channelNameLower)) + return _cache[applicationName][channelNameLower]; + + var flags = new RobloxFastFlags(applicationName, channelNameLower); + + if (shouldCache) + { + if (!_cache.ContainsKey(applicationName)) + _cache[applicationName] = new(); + + _cache[applicationName][channelNameLower] = flags; + } + + return flags; + } + } + } +}