diff --git a/AppApi.cs b/AppApi.cs index e33e1a45..e24dd577 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -428,20 +428,6 @@ namespace VRCX broadcastSocket.SendTo(byteBuffer, endPoint); } - /// - /// Downloads the VRCX update executable from the specified URL and saves it to the AppData directory. - /// - /// The URL of the VRCX update to download. - public void DownloadVRCXUpdate(string url) - { - var Location = Path.Combine(Program.AppDataDirectory, "update.exe"); - using (var client = new WebClient()) - { - client.Headers.Add("user-agent", Program.Version); - client.DownloadFile(new Uri(url), Location); - } - } - /// /// Restarts the VRCX application for an update by launching a new process with the "/Upgrade" argument and exiting the current process. /// diff --git a/AssetBundleCacher.cs b/AssetBundleCacher.cs index a153cd7c..8e767f09 100644 --- a/AssetBundleCacher.cs +++ b/AssetBundleCacher.cs @@ -26,6 +26,7 @@ namespace VRCX public static string DownloadTempLocation; public static string DownloadDestinationLocation; + public static string DownloadHashLocation; public static int DownloadProgress; public static int DownloadSize; public static bool DownloadCanceled; @@ -104,18 +105,25 @@ namespace VRCX }; } - public void DownloadFile(string url, int size) + // old asset bundle cacher downloader method reused for updating, it's not pretty + public void DownloadFile(string fileUrl, string hashUrl, int size) { + client = new WebClient(); + client.Headers.Add("user-agent", Program.Version); DownloadProgress = 0; DownloadSize = size; DownloadCanceled = false; DownloadTempLocation = Path.Combine(Program.AppDataDirectory, "tempDownload.exe"); DownloadDestinationLocation = Path.Combine(Program.AppDataDirectory, "update.exe"); - client = new WebClient(); - client.Headers.Add("user-agent", Program.Version); + DownloadHashLocation = Path.Combine(Program.AppDataDirectory, "sha256sum.txt"); + if (File.Exists(DownloadHashLocation)) + File.Delete(DownloadHashLocation); + if (!string.IsNullOrEmpty(hashUrl)) + client.DownloadFile(new Uri(hashUrl), DownloadHashLocation); + client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(DownloadProgressCallback); client.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompletedCallback); - client.DownloadFileAsync(new System.Uri(url), DownloadTempLocation); + client.DownloadFileAsync(new Uri(fileUrl), DownloadTempLocation); } public void CancelDownload() @@ -163,6 +171,27 @@ namespace VRCX DownloadProgress = -15; return; } + if (File.Exists(DownloadHashLocation)) + { + logger.Info("Updater: Checking hash"); + var lines = File.ReadAllLines(DownloadHashLocation); + var hash = lines.Length > 0 ? lines[0].Split(' ') : new[] { "" }; + using (var sha256 = SHA256.Create()) + using (var stream = File.OpenRead(DownloadTempLocation)) + { + var hashBytes = sha256.ComputeHash(stream); + var hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + if (hashString != hash[0]) + { + logger.Error($"Updater: Hash check failed file:{hashString} remote:{hash[0]}"); + // can't delete file yet because it's in use + DownloadProgress = -14; + return; + } + } + logger.Info("Updater: Hash check passed"); + } + if (File.Exists(DownloadDestinationLocation)) File.Delete(DownloadDestinationLocation); File.Move(DownloadTempLocation, DownloadDestinationLocation); diff --git a/Update.cs b/Update.cs index ab4b6dbe..5fcd75cb 100644 --- a/Update.cs +++ b/Update.cs @@ -20,6 +20,12 @@ namespace VRCX { if (Process.GetProcessesByName("VRCX_Setup").Length > 0) Environment.Exit(0); + var setupHash = Path.Combine(Program.AppDataDirectory, "sha256sum.txt"); + if (File.Exists(setupHash)) + File.Delete(setupHash); + var tempDownload = Path.Combine(Program.AppDataDirectory, "tempDownload.exe"); + if (File.Exists(tempDownload)) + File.Delete(tempDownload); if (File.Exists(VRCX_Setup_Executable)) File.Delete(VRCX_Setup_Executable); if (File.Exists(Update_Executable)) diff --git a/html/src/app.js b/html/src/app.js index d5b43f57..70b8f72f 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -21952,9 +21952,10 @@ speechSynthesis.getVoices(); this.downloadQueue.delete(ref.id); this.downloadQueueTable.data = Array.from(this.downloadQueue.values()); - var url = this.downloadCurrent.updateZipUrl; + var fileUrl = this.downloadCurrent.updateSetupUrl; + var hashUrl = this.downloadCurrent.updateHashUrl; var size = this.downloadCurrent.size; - await AssetBundleCacher.DownloadFile(url, size); + await AssetBundleCacher.DownloadFile(fileUrl, hashUrl, size); this.downloadFileProgress(); }; @@ -21993,9 +21994,16 @@ speechSynthesis.getVoices(); }); this.downloadFileComplete('Canceled'); return; + case -14: + this.$message({ + message: 'Download failed, hash mismatch', + type: 'error' + }); + this.downloadFileComplete('Failed'); + return; case -15: this.$message({ - message: 'Download failed', + message: 'Download failed, size mismatch', type: 'error' }); this.downloadFileComplete('Failed'); @@ -22803,7 +22811,8 @@ speechSynthesis.getVoices(); }; $app.methods.downloadVRCXUpdate = function ( - updateZipUrl, + updateSetupUrl, + updateHashUrl, size, name, type, @@ -22816,7 +22825,8 @@ speechSynthesis.getVoices(); this.downloadQueue.set('VRCXUpdate', { ref, type, - updateZipUrl, + updateSetupUrl, + updateHashUrl, size, autoInstall }); @@ -22829,17 +22839,27 @@ speechSynthesis.getVoices(); $app.methods.installVRCXUpdate = function () { for (var release of this.VRCXUpdateDialog.releases) { if (release.name === this.VRCXUpdateDialog.release) { + var downloadUrl = ''; + var hashUrl = ''; + var size = 0; for (var asset of release.assets) { + if (asset.state !== 'uploaded') { + continue; + } if ( - (asset.content_type === 'application/x-msdownload' || - asset.content_type === - 'application/x-msdos-program') && - asset.state === 'uploaded' + asset.content_type === 'application/x-msdownload' || + asset.content_type === 'application/x-msdos-program' ) { - var downloadUrl = asset.browser_download_url; - var size = asset.size; + downloadUrl = asset.browser_download_url; + size = asset.size; break; } + if ( + asset.name === 'SHA256SUMS.txt' && + asset.content_type === 'text/plain' + ) { + hashUrl = asset.browser_download_url; + } } if (!downloadUrl) { return; @@ -22849,6 +22869,7 @@ speechSynthesis.getVoices(); var autoInstall = false; this.downloadVRCXUpdate( downloadUrl, + hashUrl, size, name, type, @@ -22960,17 +22981,27 @@ speechSynthesis.getVoices(); // update already downloaded this.VRCXUpdateDialog.updatePendingIsLatest = true; } else if (name > this.appVersion) { + var downloadUrl = ''; + var hashUrl = ''; + var size = 0; for (var asset of json.assets) { + if (asset.state !== 'uploaded') { + continue; + } if ( - (asset.content_type === 'application/x-msdownload' || - asset.content_type === - 'application/x-msdos-program') && - asset.state === 'uploaded' + asset.content_type === 'application/x-msdownload' || + asset.content_type === 'application/x-msdos-program' ) { - var downloadUrl = asset.browser_download_url; - var size = asset.size; + downloadUrl = asset.browser_download_url; + size = asset.size; break; } + if ( + asset.name === 'SHA256SUMS.txt' && + asset.content_type === 'text/plain' + ) { + hashUrl = asset.browser_download_url; + } } if (!downloadUrl) { return; @@ -22986,6 +23017,7 @@ speechSynthesis.getVoices(); var autoInstall = false; this.downloadVRCXUpdate( downloadUrl, + hashUrl, size, name, type, @@ -22995,6 +23027,7 @@ speechSynthesis.getVoices(); var autoInstall = true; this.downloadVRCXUpdate( downloadUrl, + hashUrl, size, name, type,