From c6f4c351551414ddccd90d861bd581a09d65e049 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 9 May 2026 22:41:24 +0800 Subject: [PATCH] fix(update): fix(updater): fetch and cache SHA256 before manual update download Signed-off-by: fufesou --- .../lib/desktop/widgets/update_progress.dart | 24 +++- src/flutter_ffi.rs | 31 +++-- src/updater.rs | 108 +++++++++++++++++- 3 files changed, 146 insertions(+), 17 deletions(-) diff --git a/flutter/lib/desktop/widgets/update_progress.dart b/flutter/lib/desktop/widgets/update_progress.dart index 856ab9c9c..b1a522a2a 100644 --- a/flutter/lib/desktop/widgets/update_progress.dart +++ b/flutter/lib/desktop/widgets/update_progress.dart @@ -9,6 +9,12 @@ import 'package:url_launcher/url_launcher.dart'; const _eventKeyUpdateMe = 'update-me'; const _eventKeyUpdateMeReady = 'update-me-ready'; +const _githubRateLimitErrorMarker = + 'GitHub API rate limit may have been reached'; +// Since this is a rare case, +// we will not add a translation for this user message and will simply show it in English. +const _githubRateLimitUserMessage = + 'The download frequency limit may have been reached. Please try again later.'; void handleUpdate(String releasePageUrl) { String downloadUrl = releasePageUrl.replaceAll('tag', 'download'); @@ -75,9 +81,12 @@ void handleUpdate(String releasePageUrl) { void _showUpdateError(String releasePageUrl, String error, {String messageKey = 'download-new-version-failed-tip', - bool showRetry = true}) { + bool showRetry = true, + bool showErrorDetail = true, + String? userMessage}) { debugPrint('Update error: $error'); final dialogManager = gFFI.dialogManager; + final visibleError = userMessage ?? (showErrorDetail ? error : null); jumplink() { launchUrl(Uri.parse(releasePageUrl)); @@ -99,8 +108,10 @@ void _showUpdateError(String releasePageUrl, String error, crossAxisAlignment: CrossAxisAlignment.start, children: [ msgboxContent('custom-nocancel-nook-hasclose', 'Error', messageKey), - const SizedBox(height: 8), - Text(error), + if (visibleError != null) ...[ + const SizedBox(height: 8), + Text(visibleError), + ], ], ), ), @@ -198,7 +209,12 @@ class UpdateProgressState extends State { void _onError(String error) { cancelQueryTimer(); - _showUpdateError(widget.releasePageUrl, error); + if (error.contains(_githubRateLimitErrorMarker)) { + _showUpdateError(widget.releasePageUrl, error, + userMessage: _githubRateLimitUserMessage); + } else { + _showUpdateError(widget.releasePageUrl, error, showErrorDetail: false); + } } void _updateDownloadData() { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ed63b9879..1afa77468 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2932,14 +2932,27 @@ pub fn main_set_common(_key: String, _value: String) { let download_url = _value.clone(); let event_key = "download-new-version".to_owned(); let data = if let Some(download_file) = get_download_file_from_url(&download_url) { - std::fs::remove_file(&download_file).ok(); - match crate::hbbs_http::downloader::download_file( - download_url, - Some(PathBuf::from(download_file)), - Some(Duration::from_secs(3)), - ) { - Ok(id) => HashMap::from([("name", event_key), ("id", id)]), - Err(e) => HashMap::from([("name", event_key), ("error", e.to_string())]), + match crate::updater::download_file_expected_sha256(&download_url) { + Ok(_) => { + std::fs::remove_file(&download_file).ok(); + match crate::hbbs_http::downloader::download_file( + download_url, + Some(PathBuf::from(download_file)), + Some(Duration::from_secs(3)), + ) { + Ok(id) => HashMap::from([("name", event_key), ("id", id)]), + Err(e) => { + HashMap::from([("name", event_key), ("error", e.to_string())]) + } + } + } + Err(e) => HashMap::from([ + ("name", event_key), + ( + "error", + format!("Failed to get new version file SHA256, {}", e), + ), + ]), } } else { HashMap::from([ @@ -2968,6 +2981,7 @@ pub fn main_set_common(_key: String, _value: String) { let error = format!("Failed to get new version file SHA256, {}", e); log::error!("{}", error); push_update_me_error(error); + crate::updater::clear_download_file_expected_sha256(&_value); fs::remove_file(f).ok(); return; } @@ -2990,6 +3004,7 @@ pub fn main_set_common(_key: String, _value: String) { let error = format!("Failed to update to new version, {}", e); log::error!("{}", error); push_update_me_error(error); + crate::updater::clear_download_file_expected_sha256(&_value); fs::remove_file(f).ok(); } } diff --git a/src/updater.rs b/src/updater.rs index 6f1e67aaa..ed51a8d83 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -1,6 +1,7 @@ use crate::{common::do_check_software_update, hbbs_http::create_http_client_with_url}; use hbb_common::{bail, config, log, ResultType}; use std::{ + collections::HashMap, io::{Read, Write}, path::{Path, PathBuf}, sync::{ @@ -18,6 +19,7 @@ enum UpdateMsg { lazy_static::lazy_static! { static ref TX_MSG : Mutex> = Mutex::new(start_auto_update_check()); + static ref DOWNLOAD_FILE_SHA256_CACHE: Mutex> = Default::default(); } static CONTROLLING_SESSION_COUNT: AtomicUsize = AtomicUsize::new(0); @@ -430,16 +432,75 @@ fn fetch_github_release_metadata(api_url: &str) -> ResultType { .header(reqwest::header::USER_AGENT, "rustdesk-updater") .send()?; if !response.status().is_success() { - bail!( - "Failed to get GitHub release metadata: {}", - response.status() - ); + let status = response.status(); + if status == reqwest::StatusCode::FORBIDDEN + || status == reqwest::StatusCode::TOO_MANY_REQUESTS + { + bail!( + "Failed to get GitHub release metadata: {}. GitHub API rate limit may have been reached. Please retry later or download from the release page.", + status + ); + } + bail!("Failed to get GitHub release metadata: {}", status); } Ok(response.text()?) } +fn normalize_sha256_hex(sha256: &str) -> ResultType { + let sha256 = sha256.trim().to_ascii_lowercase(); + if sha256.len() != 64 || !sha256.chars().all(|c| c.is_ascii_hexdigit()) { + bail!("Update file SHA256 is malformed"); + } + Ok(sha256) +} + +fn cache_download_file_expected_sha256( + download_url: &str, + expected_sha256: &str, +) -> ResultType { + let expected_sha256 = normalize_sha256_hex(expected_sha256)?; + DOWNLOAD_FILE_SHA256_CACHE + .lock() + .unwrap() + .insert(download_url.to_owned(), expected_sha256.clone()); + Ok(expected_sha256) +} + +fn cached_download_file_expected_sha256(download_url: &str) -> Option { + DOWNLOAD_FILE_SHA256_CACHE + .lock() + .unwrap() + .get(download_url) + .cloned() +} + +pub fn clear_download_file_expected_sha256(download_url: &str) { + DOWNLOAD_FILE_SHA256_CACHE + .lock() + .unwrap() + .remove(download_url); +} + +pub fn refresh_download_file_expected_sha256(download_url: &str) -> ResultType { + let expected_sha256 = fetch_github_asset_sha256(download_url, download_url)?; + cache_download_file_expected_sha256(download_url, &expected_sha256) +} + pub fn download_file_expected_sha256(download_url: &str) -> ResultType { - fetch_github_asset_sha256(download_url, download_url) + match refresh_download_file_expected_sha256(download_url) { + Ok(expected_sha256) => Ok(expected_sha256), + Err(e) => { + if let Some(expected_sha256) = cached_download_file_expected_sha256(download_url) { + log::warn!( + "Failed to refresh update file SHA256 for {}, using cached value: {}", + download_url, + e + ); + return Ok(expected_sha256); + } + Err(e) + } + } } fn github_release_api_url(update_url: &str) -> ResultType { @@ -634,6 +695,43 @@ mod tests { assert!(github_release_asset_sha256(malformed, "rustdesk.exe").is_err()); } + #[test] + fn download_file_sha256_cache_roundtrips_and_clears() { + let download_url = format!( + "https://github.com/rustdesk/rustdesk/releases/download/test/rustdesk-cache-test-{}-{}.exe", + std::process::id(), + hbb_common::rand::random::() + ); + let expected_sha256 = "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789"; + clear_download_file_expected_sha256(&download_url); + + let cached = cache_download_file_expected_sha256(&download_url, expected_sha256).unwrap(); + + assert_eq!( + cached, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + ); + assert_eq!( + cached_download_file_expected_sha256(&download_url), + Some(cached) + ); + clear_download_file_expected_sha256(&download_url); + assert_eq!(cached_download_file_expected_sha256(&download_url), None); + } + + #[test] + fn download_file_sha256_cache_rejects_malformed_digest() { + let download_url = format!( + "https://github.com/rustdesk/rustdesk/releases/download/test/rustdesk-cache-test-{}-{}.exe", + std::process::id(), + hbb_common::rand::random::() + ); + clear_download_file_expected_sha256(&download_url); + + assert!(cache_download_file_expected_sha256(&download_url, "sha256:not-hex").is_err()); + assert_eq!(cached_download_file_expected_sha256(&download_url), None); + } + #[test] fn verify_file_sha256_rejects_mismatched_file() { let file_path = std::env::temp_dir().join(format!(