From 97225afe87575980d92048e246e0997fe06e4331 Mon Sep 17 00:00:00 2001 From: Yelo420 Date: Sat, 13 Dec 2025 03:01:11 +0100 Subject: [PATCH] - Bug fix: The required request headers for download resume and database backup were not sent. - Bug fix: You couldn't enter spaces for the password in the login window. - Bug fix: The session tokens were not properly persistent --- CHANGELOG.md | 7 ++++ gamevault/AssemblyInfo.cs | 2 +- .../Helper/Integrations/SaveGameHelper.cs | 4 +- .../Helper/PasswordBoxAttachedProperties.cs | 7 +--- .../Web/HttpClientDownloadWithProgress.cs | 34 +++++---------- gamevault/Helper/Web/OAuthHttpClient.cs | 15 ++++--- gamevault/Helper/Web/WebHelper.cs | 41 +++++++++++-------- .../GameDownloadUserControl.xaml.cs | 8 ++-- .../BackupRestoreUserControl.xaml.cs | 8 +++- gamevault/Windows/LoginWindow.xaml.cs | 4 +- 10 files changed, 66 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3384772..c2457ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # GameVault App Changelog +## 1.17.5 +Recommended Gamevault Server Version: `v16.1.2` +### Changes +- Bug fix: The required request headers for download resume and database backup were not sent. +- Bug fix: You couldn't enter spaces for the password in the login window. +- Bug fix: The session tokens were not properly persistent + ## 1.17.4 Recommended Gamevault Server Version: `v16.1.1` ### Changes diff --git a/gamevault/AssemblyInfo.cs b/gamevault/AssemblyInfo.cs index a94e827..24e1a37 100644 --- a/gamevault/AssemblyInfo.cs +++ b/gamevault/AssemblyInfo.cs @@ -11,7 +11,7 @@ //(used if a resource is not found in the page, // app, or any theme specific resource dictionaries) )] -[assembly: AssemblyVersion("1.17.4.0")] +[assembly: AssemblyVersion("1.17.5.0")] [assembly: AssemblyCopyright("© Phalcode™. All Rights Reserved.")] #if DEBUG [assembly: XmlnsDefinition("debug-mode", "Namespace")] diff --git a/gamevault/Helper/Integrations/SaveGameHelper.cs b/gamevault/Helper/Integrations/SaveGameHelper.cs index d802b81..ff8de11 100644 --- a/gamevault/Helper/Integrations/SaveGameHelper.cs +++ b/gamevault/Helper/Integrations/SaveGameHelper.cs @@ -70,7 +70,7 @@ internal async Task RestoreBackup(int gameId, string installationDir) string[] auth = WebHelper.GetCredentials(); string url = @$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}"; - using (HttpResponseMessage response = await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}", HttpCompletionOption.ResponseHeadersRead)) + using (HttpResponseMessage response = await WebHelper.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}", null, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); string fileName = response.Content.Headers.ContentDisposition.FileName.Split('_')[1].Split('.')[0]; @@ -356,7 +356,7 @@ private async Task UploadSavegame(string saveFilePath, int gameId, string string installationId = GetGameInstallationId(installationDir); using (MemoryStream memoryStream = await FileToMemoryStreamAsync(saveFilePath)) { - await WebHelper.UploadFileAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}", memoryStream, "x.zip", new RequestHeader[] { new RequestHeader() { Name = "X-Installation-Id", Value = installationId } }); + await WebHelper.UploadFileAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}", memoryStream, "x.zip", new List { new RequestHeader() { Name = "X-Installation-Id", Value = installationId } }); } } catch diff --git a/gamevault/Helper/PasswordBoxAttachedProperties.cs b/gamevault/Helper/PasswordBoxAttachedProperties.cs index c0dc03f..8cc485f 100644 --- a/gamevault/Helper/PasswordBoxAttachedProperties.cs +++ b/gamevault/Helper/PasswordBoxAttachedProperties.cs @@ -210,13 +210,8 @@ private static void OnPreviewKeyDown(object sender, KeyEventArgs e) if (sender is not TextBox textBox || !GetIsPassword(textBox)) return; - // Prevent spaces in password - if (e.Key == Key.Space) - { - e.Handled = true; - } // Handle Ctrl+A select all - else if (e.Key == Key.A && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) + if (e.Key == Key.A && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) { textBox.SelectAll(); e.Handled = true; diff --git a/gamevault/Helper/Web/HttpClientDownloadWithProgress.cs b/gamevault/Helper/Web/HttpClientDownloadWithProgress.cs index 35fd316..8ef4919 100644 --- a/gamevault/Helper/Web/HttpClientDownloadWithProgress.cs +++ b/gamevault/Helper/Web/HttpClientDownloadWithProgress.cs @@ -14,25 +14,25 @@ namespace gamevault.Helper { - public class HttpClientDownloadWithProgress : IDisposable + public class HttpClientDownloadWithProgress { private readonly string DownloadUrl; private readonly string DestinationFolderPath; private string FileName; private string FallbackFileName; - private KeyValuePair? AdditionalHeader; + private Dictionary? AdditionalHeader; private bool Cancelled = false; private bool Paused = false; private long ResumePosition = -1; private long PreResumeSize = -1; private DateTime LastTime; - private HttpClient HttpClient; + public delegate void ProgressChangedHandler(long totalFileSize, long currentBytesDownloaded, long totalBytesDownloaded, double? progressPercentage, long resumePosition); public event ProgressChangedHandler ProgressChanged; - public HttpClientDownloadWithProgress(string downloadUrl, string destinationFolderPath, string fallbackFileName, KeyValuePair? additionalHeader = null) + public HttpClientDownloadWithProgress(string downloadUrl, string destinationFolderPath, string fallbackFileName, Dictionary? additionalHeader = null) { DownloadUrl = downloadUrl; DestinationFolderPath = destinationFolderPath; @@ -42,8 +42,7 @@ public HttpClientDownloadWithProgress(string downloadUrl, string destinationFold public async Task StartDownload(bool tryResume = false) { - HttpClient = new HttpClient { Timeout = TimeSpan.FromDays(7) }; - CreateHeader(); + if (tryResume) { InitResume(); @@ -55,18 +54,9 @@ public async Task StartDownload(bool tryResume = false) File.Delete($"{DestinationFolderPath}\\gamevault-metadata"); } - using (HttpResponseMessage response = await WebHelper.GetAsync(DownloadUrl, HttpCompletionOption.ResponseHeadersRead)) + using (HttpResponseMessage response = await WebHelper.GetAsync(DownloadUrl, AdditionalHeader, HttpCompletionOption.ResponseHeadersRead)) await DownloadFileFromHttpResponseMessage(response); } - private void CreateHeader() - { - string[] auth = WebHelper.GetCredentials(); - HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{auth[0]}:{auth[1]}"))); - HttpClient.DefaultRequestHeaders.Add("User-Agent", $"GameVault/{SettingsViewModel.Instance.Version}"); - - if (AdditionalHeader != null) - HttpClient.DefaultRequestHeaders.Add(AdditionalHeader.Value.Key, AdditionalHeader.Value.Value); - } private void InitResume() { string resumeData = Preferences.Get(AppConfigKey.DownloadProgress, $"{DestinationFolderPath}\\gamevault-metadata"); @@ -77,7 +67,11 @@ private void InitResume() string[] resumeDataToProcess = resumeData.Split(";"); ResumePosition = long.Parse(resumeDataToProcess[0]); PreResumeSize = long.Parse(resumeDataToProcess[1]); - HttpClient.DefaultRequestHeaders.Range = new RangeHeaderValue(long.Parse(resumeDataToProcess[0]), null); + if (AdditionalHeader == null) + { + AdditionalHeader = new Dictionary(); + } + AdditionalHeader?.Add("Range", $"bytes={ResumePosition}-"); } catch { } } @@ -206,17 +200,11 @@ public void Cancel() return; } Cancelled = true; - Dispose(); } public void Pause() { Paused = true; Cancelled = true; - Dispose(); - } - public void Dispose() - { - HttpClient?.Dispose(); } } } diff --git a/gamevault/Helper/Web/OAuthHttpClient.cs b/gamevault/Helper/Web/OAuthHttpClient.cs index f4f8da9..d17a574 100644 --- a/gamevault/Helper/Web/OAuthHttpClient.cs +++ b/gamevault/Helper/Web/OAuthHttpClient.cs @@ -33,6 +33,11 @@ public void InjectTokens(string accessToken, string refreshToken) { _accessToken = accessToken; _refreshToken = refreshToken; + try + { + nextTokenRefresh = GetNextTokenRefresh(_accessToken); + } + catch { } } public string GetRefreshToken() { @@ -60,7 +65,7 @@ public async Task LoginBasicAuthAsync(string username, string password) return true; } - private async Task SendAsync(HttpRequestMessage request, RequestHeader[]? additionalHeaders = null, HttpCompletionOption option = HttpCompletionOption.ResponseContentRead) + private async Task SendAsync(HttpRequestMessage request, List? additionalHeaders = null, HttpCompletionOption option = HttpCompletionOption.ResponseContentRead) { if (string.IsNullOrEmpty(_accessToken)) { @@ -82,16 +87,16 @@ private async Task SendAsync(HttpRequestMessage request, Re return await _httpClient.SendAsync(request, option); } - public Task GetAsync(string url, RequestHeader[]? additionalHeaders = null, HttpCompletionOption option = HttpCompletionOption.ResponseContentRead) => + public Task GetAsync(string url, List? additionalHeaders = null, HttpCompletionOption option = HttpCompletionOption.ResponseContentRead) => SendAsync(new HttpRequestMessage(HttpMethod.Get, url), additionalHeaders, option); - public Task PostAsync(string url, HttpContent content, RequestHeader[]? additionalHeaders = null) => + public Task PostAsync(string url, HttpContent content, List? additionalHeaders = null) => SendAsync(new HttpRequestMessage(HttpMethod.Post, url) { Content = content }, additionalHeaders); - public Task PutAsync(string url, HttpContent content, RequestHeader[]? additionalHeaders = null) => + public Task PutAsync(string url, HttpContent content, List? additionalHeaders = null) => SendAsync(new HttpRequestMessage(HttpMethod.Put, url) { Content = content }, additionalHeaders); - public Task DeleteAsync(string url, RequestHeader[]? additionalHeaders = null) => + public Task DeleteAsync(string url, List? additionalHeaders = null) => SendAsync(new HttpRequestMessage(HttpMethod.Delete, url), additionalHeaders); private async Task RefreshTokenAsync() diff --git a/gamevault/Helper/Web/WebHelper.cs b/gamevault/Helper/Web/WebHelper.cs index 1141e8a..ecc959a 100644 --- a/gamevault/Helper/Web/WebHelper.cs +++ b/gamevault/Helper/Web/WebHelper.cs @@ -19,7 +19,7 @@ internal class WebHelper { DefaultRequestHeaders = { { "User-Agent", "GameVault" } } }; - private static RequestHeader[] AdditionalRequestHeaders; + private static List AdditionalRequestHeaders = new List(); static WebHelper() { } internal static void SetCredentials(string serverUrl, string username, string password) { @@ -47,7 +47,7 @@ internal static string GetRefreshToken() { return HttpClient.GetRefreshToken(); } - internal static void SetAdditionalRequestHeaders(RequestHeader[] additionalRequestHeaders) + internal static void SetAdditionalDefaultRequestHeaders(List additionalRequestHeaders) { AdditionalRequestHeaders = additionalRequestHeaders; @@ -62,40 +62,45 @@ internal static void SetAdditionalRequestHeaders(RequestHeader[] additionalReque internal static async Task BaseGetAsync(string url) { var response = await BaseHttpClient.GetAsync(url); - await WebExceptionHelper.EnsureSuccessStatusCode(response); + await WebExceptionHelper.EnsureSuccessStatusCode(response); return await response.Content.ReadAsStringAsync(); } internal static async Task BasePostAsync(string url, string payload) { var content = new StringContent(payload, Encoding.UTF8, "application/json"); var response = await BaseHttpClient.PostAsync(url, content); - await WebExceptionHelper.EnsureSuccessStatusCode(response); + await WebExceptionHelper.EnsureSuccessStatusCode(response); return await response.Content.ReadAsStringAsync(); } internal static async Task BaseSendRequest(HttpRequestMessage request) { var response = await BaseHttpClient.SendAsync(request); - await WebExceptionHelper.EnsureSuccessStatusCode(response); + await WebExceptionHelper.EnsureSuccessStatusCode(response); return await response.Content.ReadAsStringAsync(); } #endregion internal static async Task GetAsync(string url) { var response = await HttpClient.GetAsync(url, AdditionalRequestHeaders); - await WebExceptionHelper.EnsureSuccessStatusCode(response); + await WebExceptionHelper.EnsureSuccessStatusCode(response); return await response.Content.ReadAsStringAsync(); } - internal static async Task GetAsync(string url, HttpCompletionOption option = HttpCompletionOption.ResponseContentRead) + internal static async Task GetAsync(string url, Dictionary? additionalManualRequestHeaders = null, HttpCompletionOption option = HttpCompletionOption.ResponseContentRead) { - var response = await HttpClient.GetAsync(url, AdditionalRequestHeaders, option); - await WebExceptionHelper.EnsureSuccessStatusCode(response); + List additionalHeaders = null; + if (additionalManualRequestHeaders is { Count: > 0 }) + { + additionalHeaders = additionalManualRequestHeaders.Select(h => new RequestHeader { Name = h.Key, Value = h.Value }).Concat(AdditionalRequestHeaders).ToList(); + } + var response = await HttpClient.GetAsync(url, additionalHeaders ?? AdditionalRequestHeaders, option); + await WebExceptionHelper.EnsureSuccessStatusCode(response); return response; } internal static async Task PostAsync(string url, string payload) { var content = new StringContent(payload, Encoding.UTF8, "application/json"); var response = await HttpClient.PostAsync(url, content, AdditionalRequestHeaders); - await WebExceptionHelper.EnsureSuccessStatusCode(response); + await WebExceptionHelper.EnsureSuccessStatusCode(response); return await response.Content.ReadAsStringAsync(); } @@ -109,20 +114,20 @@ internal static async Task PutAsync(string url, string payload) internal static async Task DeleteAsync(string url) { var response = await HttpClient.DeleteAsync(url, AdditionalRequestHeaders); - await WebExceptionHelper.EnsureSuccessStatusCode(response); + await WebExceptionHelper.EnsureSuccessStatusCode(response); return await response.Content.ReadAsStringAsync(); } public static async Task DownloadImageFromUrlAsync(string imageUrl, string cacheFile) { var response = await HttpClient.GetAsync(imageUrl, AdditionalRequestHeaders); - await WebExceptionHelper.EnsureSuccessStatusCode(response); + await WebExceptionHelper.EnsureSuccessStatusCode(response); var imageBytes = await response.Content.ReadAsByteArrayAsync(); await File.WriteAllBytesAsync(cacheFile, imageBytes); } public static async Task DownloadImageFromUrlAsync(string imageUrl) { var response = await HttpClient.GetAsync(imageUrl, AdditionalRequestHeaders); - await WebExceptionHelper.EnsureSuccessStatusCode(response); + await WebExceptionHelper.EnsureSuccessStatusCode(response); var imageData = await response.Content.ReadAsByteArrayAsync(); using (var memoryStream = new MemoryStream(imageData)) { @@ -135,13 +140,13 @@ public static async Task DownloadImageFromUrlAsync(string imageUrl) return bitmap; } } - public static async Task UploadFileAsync(string apiUrl, Stream imageStream, string fileName, RequestHeader[]? additionalHeaders = null) + public static async Task UploadFileAsync(string apiUrl, Stream imageStream, string fileName, List? additionalHeaders = null) { //Mix request headers - RequestHeader[]? mixedHeaders = null; + List? mixedHeaders = null; if (additionalHeaders != null && AdditionalRequestHeaders != null) { - mixedHeaders = AdditionalRequestHeaders.Concat(additionalHeaders).ToArray(); + mixedHeaders = AdditionalRequestHeaders.Concat(additionalHeaders).ToList(); } else if (additionalHeaders == null) { @@ -158,7 +163,7 @@ public static async Task UploadFileAsync(string apiUrl, Stream imageStre imageContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); formData.Add(imageContent, "file", fileName); var response = await HttpClient.PostAsync(apiUrl, formData, mixedHeaders); - await WebExceptionHelper.EnsureSuccessStatusCode(response); + await WebExceptionHelper.EnsureSuccessStatusCode(response); var responseContent = await response.Content.ReadAsStringAsync(); return responseContent; } @@ -184,7 +189,7 @@ public static string RemoveSpecialCharactersFromUrl(string url) } return sb.ToString(); } - + } } diff --git a/gamevault/UserControls/GameDownloadUserControl.xaml.cs b/gamevault/UserControls/GameDownloadUserControl.xaml.cs index 1a733fb..fc17167 100644 --- a/gamevault/UserControls/GameDownloadUserControl.xaml.cs +++ b/gamevault/UserControls/GameDownloadUserControl.xaml.cs @@ -178,12 +178,12 @@ private async Task DownloadGame(bool tryResume = false) ViewModel.DownloadFailedVisibility = System.Windows.Visibility.Hidden; if (!Directory.Exists(m_DownloadPath)) { Directory.CreateDirectory(m_DownloadPath); } - KeyValuePair? header = null; + Dictionary additionalRequestHeaders = new Dictionary(); if (SettingsViewModel.Instance.DownloadLimit > 0) { - header = new KeyValuePair("X-Download-Speed-Limit", SettingsViewModel.Instance.DownloadLimit.ToString()); + additionalRequestHeaders.Add("X-Download-Speed-Limit", SettingsViewModel.Instance.DownloadLimit.ToString()); } - client = new HttpClientDownloadWithProgress($"{SettingsViewModel.Instance.ServerUrl}/api/games/{ViewModel.Game.ID}/download", m_DownloadPath, Path.GetFileName(ViewModel.Game.Path), header); + client = new HttpClientDownloadWithProgress($"{SettingsViewModel.Instance.ServerUrl}/api/games/{ViewModel.Game.ID}/download", m_DownloadPath, Path.GetFileName(ViewModel.Game.Path), additionalRequestHeaders); client.ProgressChanged += DownloadProgress; startTime = DateTime.Now; downloadSpeedCalc = new DownloadSpeedCalculator(); @@ -195,7 +195,6 @@ private async Task DownloadGame(bool tryResume = false) catch (Exception ex) { IsDownloadActive = false; - client.Dispose(); ViewModel.State = $"Error: '{ex.Message}'"; ViewModel.DownloadUIVisibility = System.Windows.Visibility.Hidden; ViewModel.DownloadFailedVisibility = System.Windows.Visibility.Visible; @@ -314,7 +313,6 @@ private void DownloadCompleted() UpdateDataSizeUI(); ViewModel.DownloadUIVisibility = System.Windows.Visibility.Hidden; - client.Dispose(); IsDownloadActive = false; ViewModel.State = "Downloaded"; uiBtnExtract.IsEnabled = true; diff --git a/gamevault/UserControls/SettingsComponents/BackupRestoreUserControl.xaml.cs b/gamevault/UserControls/SettingsComponents/BackupRestoreUserControl.xaml.cs index 6bf5fe4..8966a07 100644 --- a/gamevault/UserControls/SettingsComponents/BackupRestoreUserControl.xaml.cs +++ b/gamevault/UserControls/SettingsComponents/BackupRestoreUserControl.xaml.cs @@ -67,8 +67,12 @@ private async void StartBackup_Click(object sender, RoutedEventArgs e) this.IsEnabled = false; try { + Dictionary? additionalRequestHeaders = new Dictionary + { + { "X-Database-Password", uiBackupDatabasePassword.Password } + }; HttpClientDownloadWithProgress httpClientDownloadWithProgress = new HttpClientDownloadWithProgress(@$"{SettingsViewModel.Instance.ServerUrl}/api/admin/database/backup", - uiBackupDirectory.Text, $"DB_Backup_{DateTime.Now.ToString()}.db", new KeyValuePair("X-Database-Password", uiBackupDatabasePassword.Password)); + uiBackupDirectory.Text, $"DB_Backup_{DateTime.Now.ToString()}.db", additionalRequestHeaders); await httpClientDownloadWithProgress.StartDownload(); MainWindowViewModel.Instance.AppBarText = "Successfully performed database backup."; @@ -125,7 +129,7 @@ private async void StartRestore_Click(object sender, RoutedEventArgs e) this.IsEnabled = false; try { - await WebHelper.UploadFileAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/admin/database/restore", File.OpenRead(uiRestoreFile.Tag.ToString()), uiRestoreFile.Text, new RequestHeader[] { new RequestHeader() { Name = "X-Database-Password", Value = uiRestoreDatabasePassword.Password } }); + await WebHelper.UploadFileAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/admin/database/restore", File.OpenRead(uiRestoreFile.Tag.ToString()), uiRestoreFile.Text, new List { new RequestHeader() { Name = "X-Database-Password", Value = uiRestoreDatabasePassword.Password } }); MainWindowViewModel.Instance.AppBarText = "Successfully uploaded database file"; } catch (HttpRequestException httpEx) diff --git a/gamevault/Windows/LoginWindow.xaml.cs b/gamevault/Windows/LoginWindow.xaml.cs index 006a574..f30f5bf 100644 --- a/gamevault/Windows/LoginWindow.xaml.cs +++ b/gamevault/Windows/LoginWindow.xaml.cs @@ -72,7 +72,7 @@ private async void LoginWindow_Loaded(object sender, RoutedEventArgs e) string result = Preferences.Get(AppConfigKey.AdditionalRequestHeaders, ProfileManager.ProfileConfigFile); var objResult = JsonSerializer.Deserialize>(result); ViewModel.AdditionalRequestHeaders = objResult; - WebHelper.SetAdditionalRequestHeaders(ViewModel.AdditionalRequestHeaders?.ToArray()); + WebHelper.SetAdditionalDefaultRequestHeaders(ViewModel.AdditionalRequestHeaders?.ToList()); } catch { } if (!SkipBootTasks) @@ -646,7 +646,7 @@ private void SaveAdditionalHeaders_Click(object sender, RoutedEventArgs e) try { Preferences.Set(AppConfigKey.AdditionalRequestHeaders, ViewModel.AdditionalRequestHeaders.Where(rh => !string.IsNullOrWhiteSpace(rh.Name) && !string.IsNullOrWhiteSpace(rh.Value)), ProfileManager.ProfileConfigFile); - WebHelper.SetAdditionalRequestHeaders(ViewModel.AdditionalRequestHeaders?.ToArray()); + WebHelper.SetAdditionalDefaultRequestHeaders(ViewModel.AdditionalRequestHeaders?.ToList()); ViewModel.LoginStepIndex = (int)LoginStep.ChooseProfile; ViewModel.AppBarText = "Successfully saved additional request headers"; }