fix(update): fix(updater): fetch and cache SHA256 before manual update download

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou 2026-05-09 22:41:24 +08:00
parent ee78cc18fd
commit c6f4c35155
3 changed files with 146 additions and 17 deletions

View file

@ -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<UpdateProgress> {
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() {

View file

@ -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();
}
}

View file

@ -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<Sender<UpdateMsg>> = Mutex::new(start_auto_update_check());
static ref DOWNLOAD_FILE_SHA256_CACHE: Mutex<HashMap<String, String>> = Default::default();
}
static CONTROLLING_SESSION_COUNT: AtomicUsize = AtomicUsize::new(0);
@ -430,16 +432,75 @@ fn fetch_github_release_metadata(api_url: &str) -> ResultType<String> {
.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<String> {
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<String> {
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<String> {
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<String> {
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<String> {
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<String> {
@ -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::<u64>()
);
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::<u64>()
);
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!(