Implement auto-update routine
This commit is contained in:
+45
-3
@@ -31,6 +31,13 @@ pub enum Action {
|
||||
/// — every `Data::FS(...)` frame the server sends is executed here, in
|
||||
/// the user's security context.
|
||||
Cm,
|
||||
/// `--update`. Self-replacement entry point launched as an elevated child
|
||||
/// by the running service's updater (see `librustdesk::updater`) after it
|
||||
/// has downloaded and SHA256-verified a new hello-agent.exe from the
|
||||
/// Gitea releases page. `current_exe()` here points at the staged new
|
||||
/// binary in `%TEMP%`; it copies itself over the installed location and
|
||||
/// restarts the service via `librustdesk::platform::update_me`.
|
||||
Update,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -47,6 +54,7 @@ impl ParsedArgs {
|
||||
let mut service = false;
|
||||
let mut server = false;
|
||||
let mut cm = false;
|
||||
let mut update = false;
|
||||
let mut config_blob: Option<String> = None;
|
||||
|
||||
let mut i = 0;
|
||||
@@ -56,6 +64,7 @@ impl ParsedArgs {
|
||||
"--uninstall" => uninstall = true,
|
||||
"--service" => service = true,
|
||||
"--server" => server = true,
|
||||
"--update" => update = true,
|
||||
// Connection-manager popup mode. Treat `--cm-no-ui` (the
|
||||
// Linux-headless variant librustdesk also tries) as a
|
||||
// synonym; either way we run cm_popup.
|
||||
@@ -81,14 +90,21 @@ impl ParsedArgs {
|
||||
}
|
||||
|
||||
// Mutual-exclusion rules. --install + --config is the MDM one-liner;
|
||||
// everything else is one-action-at-a-time.
|
||||
let exclusive = [uninstall, service, server, cm].iter().filter(|x| **x).count();
|
||||
// everything else is one-action-at-a-time. --update is launched by
|
||||
// the updater as a standalone elevated child, never combined.
|
||||
let exclusive = [uninstall, service, server, cm, update]
|
||||
.iter()
|
||||
.filter(|x| **x)
|
||||
.count();
|
||||
if exclusive > 1 {
|
||||
bail!("--uninstall, --service, --server, --cm are mutually exclusive");
|
||||
bail!("--uninstall, --service, --server, --cm, --update are mutually exclusive");
|
||||
}
|
||||
if uninstall && (install || config_blob.is_some()) {
|
||||
bail!("--uninstall cannot be combined with other flags");
|
||||
}
|
||||
if update && (install || config_blob.is_some()) {
|
||||
bail!("--update cannot be combined with other flags");
|
||||
}
|
||||
|
||||
let action = if uninstall {
|
||||
Action::Uninstall
|
||||
@@ -100,6 +116,8 @@ impl ParsedArgs {
|
||||
Action::Server
|
||||
} else if cm {
|
||||
Action::Cm
|
||||
} else if update {
|
||||
Action::Update
|
||||
} else if config_blob.is_some() {
|
||||
Action::ConfigOnly
|
||||
} else {
|
||||
@@ -131,6 +149,10 @@ OPTIONS:
|
||||
--service SCM entry point. Do not invoke manually.
|
||||
--server Worker mode (launched by the service shell into
|
||||
the active console session).
|
||||
--update Self-replacement entry point. Launched by the
|
||||
running service's updater after downloading and
|
||||
SHA256-verifying a new release from Gitea. Do
|
||||
not invoke manually.
|
||||
-h, --help Show this help.
|
||||
-V, --version Show version.
|
||||
|
||||
@@ -191,4 +213,24 @@ mod tests {
|
||||
fn unknown_arg() {
|
||||
assert!(parse(&["--no-such-flag"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_alone() {
|
||||
assert_eq!(parse(&["--update"]).unwrap().action, Action::Update);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_install_conflict() {
|
||||
assert!(parse(&["--update", "--install"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_service_conflict() {
|
||||
assert!(parse(&["--update", "--service"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_config_conflict() {
|
||||
assert!(parse(&["--update", "--config", "BLOB"]).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -90,10 +90,40 @@ fn main() {
|
||||
Action::Service => "service",
|
||||
Action::Server => "server",
|
||||
Action::Cm => "cm",
|
||||
Action::Update => "update",
|
||||
Action::ConfigOnly | Action::None => "hello-agent",
|
||||
};
|
||||
init_logging(mode);
|
||||
|
||||
// --update is the self-replacement re-entry: the running service's
|
||||
// updater downloads a new hello-agent.exe to %TEMP%, verifies its
|
||||
// SHA256, then launches `<temp>\hello-agent.exe --update` as an
|
||||
// elevated child. We are that child — `current_exe()` is the staged
|
||||
// new binary, and our only job is to copy ourselves over the
|
||||
// installed location and restart the service. Do it before the
|
||||
// config-import dance below so a corrupt-on-disk config can't block
|
||||
// an update from going through.
|
||||
if parsed.action == Action::Update {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
match librustdesk::platform::update_me(false) {
|
||||
Ok(()) => {
|
||||
log::info!("hello-agent: --update completed");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("hello-agent: --update failed: {e:#}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
eprintln!("hello-agent: --update is Windows-only.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --config is allowed to combine with --install (one-line MDM deploy)
|
||||
// but on its own is a separate operation. Apply it first so --install
|
||||
// sees the populated config.
|
||||
@@ -172,6 +202,11 @@ fn main() {
|
||||
// can watch logs. Production deployments use --install + --service.
|
||||
run_server();
|
||||
}
|
||||
Action::Update => {
|
||||
// Handled in the early-return block above (before config-import).
|
||||
// The match has to cover this variant for exhaustiveness.
|
||||
unreachable!("Action::Update is dispatched before this match");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+100
-22
@@ -94,11 +94,24 @@ pub mod input {
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref SOFTWARE_UPDATE_URL: Arc<Mutex<String>> = Default::default();
|
||||
// hello-agent local patch: assets resolved by the Gitea-backed
|
||||
// `do_check_software_update`. `SOFTWARE_UPDATE_URL` is kept holding the
|
||||
// human-facing tag URL (so `ui_interface::get_new_version` still works
|
||||
// by rsplit('/')) while the actual binary + sha256 download URLs live
|
||||
// here and are consumed by `updater::check_update`. None = no update.
|
||||
pub static ref SOFTWARE_UPDATE_ASSETS: Arc<Mutex<Option<UpdateAssets>>> = Default::default();
|
||||
pub static ref DEVICE_ID: Arc<Mutex<String>> = Default::default();
|
||||
pub static ref DEVICE_NAME: Arc<Mutex<String>> = Default::default();
|
||||
static ref PUBLIC_IPV6_ADDR: Arc<Mutex<(Option<SocketAddr>, Option<Instant>)>> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpdateAssets {
|
||||
pub binary_url: String,
|
||||
pub binary_name: String,
|
||||
pub sha256_url: String,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// Is server process, with "--server" args
|
||||
static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned());
|
||||
@@ -949,19 +962,45 @@ pub fn check_software_update() {
|
||||
}
|
||||
}
|
||||
|
||||
// No need to check `danger_accept_invalid_cert` for now.
|
||||
// Because the url is always `https://api.rustdesk.com/version/latest`.
|
||||
// hello-agent local patch: instead of POSTing to api.rustdesk.com (the
|
||||
// upstream endpoint that resolves the latest stock-RustDesk release), this
|
||||
// queries the Gitea Releases API on the hello-agent repo and resolves
|
||||
// the binary + sha256 asset URLs of the latest release. The original
|
||||
// rustdesk-api code path is intentionally gone: a stock-RustDesk auto-update
|
||||
// would happily replace hello-agent's installation with vanilla rustdesk.
|
||||
// The product version compared against the release tag is
|
||||
// `hbb_common::config::AGENT_VERSION` (populated from `CARGO_PKG_VERSION`
|
||||
// in hello-agent's `main`) — not `crate::VERSION`, which is the embedded
|
||||
// rustdesk core version and would always be much higher than hello-agent's
|
||||
// release tags, making the updater think no update is ever available.
|
||||
const HELLO_AGENT_RELEASES_API: &str =
|
||||
"https://gitea.cstudio.ch/api/v1/repos/mike/hello-agent/releases/latest";
|
||||
const HELLO_AGENT_TAG_URL_PREFIX: &str =
|
||||
"https://gitea.cstudio.ch/mike/hello-agent/releases/tag";
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct GiteaRelease {
|
||||
tag_name: String,
|
||||
#[serde(default)]
|
||||
assets: Vec<GiteaAsset>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct GiteaAsset {
|
||||
name: String,
|
||||
browser_download_url: String,
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn do_check_software_update() -> hbb_common::ResultType<()> {
|
||||
let (request, url) =
|
||||
hbb_common::version_check_request(hbb_common::VER_TYPE_RUSTDESK_CLIENT.to_string());
|
||||
let url = HELLO_AGENT_RELEASES_API;
|
||||
let proxy_conf = Config::get_socks();
|
||||
let tls_url = get_url_for_tls(&url, &proxy_conf);
|
||||
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||
let tls_type = get_cached_tls_type(tls_url);
|
||||
let is_tls_not_cached = tls_type.is_none();
|
||||
let tls_type = tls_type.unwrap_or(TlsType::Rustls);
|
||||
let client = create_http_client_async(tls_type, false);
|
||||
let latest_release_response = match client.post(&url).json(&request).send().await {
|
||||
let response = match client.get(url).send().await {
|
||||
Ok(resp) => {
|
||||
upsert_tls_cache(tls_url, tls_type, false);
|
||||
resp
|
||||
@@ -970,7 +1009,7 @@ pub async fn do_check_software_update() -> hbb_common::ResultType<()> {
|
||||
if is_tls_not_cached && err.is_request() {
|
||||
let tls_type = TlsType::NativeTls;
|
||||
let client = create_http_client_async(tls_type, false);
|
||||
let resp = client.post(&url).json(&request).send().await?;
|
||||
let resp = client.get(url).send().await?;
|
||||
upsert_tls_cache(tls_url, tls_type, false);
|
||||
resp
|
||||
} else {
|
||||
@@ -978,24 +1017,63 @@ pub async fn do_check_software_update() -> hbb_common::ResultType<()> {
|
||||
}
|
||||
}
|
||||
};
|
||||
let bytes = latest_release_response.bytes().await?;
|
||||
let resp: hbb_common::VersionCheckResponse = serde_json::from_slice(&bytes)?;
|
||||
let response_url = resp.url;
|
||||
let latest_release_version = response_url.rsplit('/').next().unwrap_or_default();
|
||||
if !response.status().is_success() {
|
||||
bail!("Gitea releases API returned HTTP {}", response.status());
|
||||
}
|
||||
let bytes = response.bytes().await?;
|
||||
let release: GiteaRelease = serde_json::from_slice(&bytes)?;
|
||||
|
||||
if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) {
|
||||
#[cfg(feature = "flutter")]
|
||||
{
|
||||
let mut m = HashMap::new();
|
||||
m.insert("name", "check_software_update_finish");
|
||||
m.insert("url", &response_url);
|
||||
if let Ok(data) = serde_json::to_string(&m) {
|
||||
let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data);
|
||||
}
|
||||
}
|
||||
*SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
|
||||
let latest_version = release.tag_name.trim_start_matches('v');
|
||||
let current_version = hbb_common::config::AGENT_VERSION.read().unwrap().clone();
|
||||
let current_version = if current_version.is_empty() {
|
||||
crate::VERSION.to_owned()
|
||||
} else {
|
||||
current_version
|
||||
};
|
||||
if get_version_number(latest_version) <= get_version_number(¤t_version) {
|
||||
*SOFTWARE_UPDATE_URL.lock().unwrap() = "".to_string();
|
||||
*SOFTWARE_UPDATE_ASSETS.lock().unwrap() = None;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Pick the Windows binary asset and its SHA256 companion. The release
|
||||
// is expected to carry a `*.exe` (or `*.exe.signed`) and a matching
|
||||
// `*.sha256` file. We don't pair them by name — there should only be
|
||||
// one of each per release.
|
||||
let binary = release.assets.iter().find(|a| {
|
||||
let n = a.name.to_lowercase();
|
||||
(n.ends_with(".exe") || n.ends_with(".exe.signed")) && !n.ends_with(".sha256")
|
||||
});
|
||||
let sha256 = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name.to_lowercase().ends_with(".sha256"));
|
||||
let (Some(binary), Some(sha256)) = (binary, sha256) else {
|
||||
log::warn!(
|
||||
"hello-agent release {} is missing a binary and/or .sha256 asset",
|
||||
release.tag_name
|
||||
);
|
||||
*SOFTWARE_UPDATE_URL.lock().unwrap() = "".to_string();
|
||||
*SOFTWARE_UPDATE_ASSETS.lock().unwrap() = None;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let tag_url = format!("{}/{}", HELLO_AGENT_TAG_URL_PREFIX, release.tag_name);
|
||||
*SOFTWARE_UPDATE_URL.lock().unwrap() = tag_url.clone();
|
||||
*SOFTWARE_UPDATE_ASSETS.lock().unwrap() = Some(UpdateAssets {
|
||||
binary_url: binary.browser_download_url.clone(),
|
||||
binary_name: binary.name.clone(),
|
||||
sha256_url: sha256.browser_download_url.clone(),
|
||||
});
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
{
|
||||
let mut m = HashMap::new();
|
||||
m.insert("name", "check_software_update_finish");
|
||||
m.insert("url", &tag_url);
|
||||
if let Ok(data) = serde_json::to_string(&m) {
|
||||
let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+38
-13
@@ -3110,21 +3110,27 @@ reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
|
||||
)
|
||||
}
|
||||
|
||||
// hello-agent local patch: only refresh the Add/Remove Programs entry if
|
||||
// the install path actually created one. Hello-agent's `--install`
|
||||
// (`src/service.rs::install`) does not write to
|
||||
// `HKLM\...\Uninstall\<APP_NAME>` — the agent is a headless service
|
||||
// that intentionally doesn't appear in Add/Remove Programs (there's
|
||||
// nothing meaningful to uninstall through the shell-integrated UI;
|
||||
// operators run `hello-agent.exe --uninstall`). Without this guard,
|
||||
// the upstream `update_me` would *create* the uninstall key on every
|
||||
// update — and then leave it behind as an orphan, since
|
||||
// `service::uninstall()` doesn't remove it either. Upstream rustdesk's
|
||||
// `install_me` does write the key, so stock-rustdesk installs continue
|
||||
// to get their version display refreshed as before.
|
||||
let subkey_exists = |sk: &str| {
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
hklm.open_subkey(sk.replace("HKEY_LOCAL_MACHINE\\", ""))
|
||||
.is_ok()
|
||||
};
|
||||
let reg_cmd = {
|
||||
let reg_cmd_main = get_reg_cmd(
|
||||
&subkey,
|
||||
is_msi,
|
||||
&display_icon,
|
||||
&version,
|
||||
&build_date,
|
||||
&version_major,
|
||||
&version_minor,
|
||||
&version_build,
|
||||
size,
|
||||
);
|
||||
let reg_cmd_msi = if let Some(reg_msi_key) = get_reg_msi_key(&subkey, is_msi) {
|
||||
let reg_cmd_main = if subkey_exists(&subkey) {
|
||||
get_reg_cmd(
|
||||
®_msi_key,
|
||||
&subkey,
|
||||
is_msi,
|
||||
&display_icon,
|
||||
&version,
|
||||
@@ -3137,6 +3143,25 @@ reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
|
||||
} else {
|
||||
"".to_owned()
|
||||
};
|
||||
let reg_cmd_msi = if let Some(reg_msi_key) = get_reg_msi_key(&subkey, is_msi) {
|
||||
if subkey_exists(®_msi_key) {
|
||||
get_reg_cmd(
|
||||
®_msi_key,
|
||||
is_msi,
|
||||
&display_icon,
|
||||
&version,
|
||||
&build_date,
|
||||
&version_major,
|
||||
&version_minor,
|
||||
&version_build,
|
||||
size,
|
||||
)
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
} else {
|
||||
"".to_owned()
|
||||
};
|
||||
format!("{}{}", reg_cmd_main, reg_cmd_msi)
|
||||
};
|
||||
|
||||
|
||||
Vendored
+101
-61
@@ -118,8 +118,16 @@ fn start_auto_update_check_(rx_msg: Receiver<UpdateMsg>) {
|
||||
}
|
||||
|
||||
fn check_update(manually: bool) -> ResultType<()> {
|
||||
// hello-agent local patch: `is_msi_installed()` reads HKLM\...\Uninstall\HelloAgent
|
||||
// and errors when the uninstall key (or `WindowsInstaller` value) is
|
||||
// absent, which is the common case for a hello-agent install. The
|
||||
// upstream code used `?` here, propagating the registry error and
|
||||
// killing the update before our Gitea check ever ran. Swallow the
|
||||
// error: `update_msi` will be `false` (correct — hello-agent is a
|
||||
// custom client so the MSI branch is never the right one anyway).
|
||||
#[cfg(target_os = "windows")]
|
||||
let update_msi = crate::platform::is_msi_installed()? && !crate::is_custom_client();
|
||||
let update_msi =
|
||||
crate::platform::is_msi_installed().unwrap_or(false) && !crate::is_custom_client();
|
||||
if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -128,70 +136,102 @@ fn check_update(manually: bool) -> ResultType<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let update_url = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone();
|
||||
if update_url.is_empty() {
|
||||
// hello-agent local patch: the upstream code reconstructed the download
|
||||
// URL from a GitHub "tag" URL and a hard-coded filename pattern. Both
|
||||
// are now resolved by `do_check_software_update` against the Gitea
|
||||
// Releases API and exposed via `SOFTWARE_UPDATE_ASSETS`.
|
||||
let assets = crate::common::SOFTWARE_UPDATE_ASSETS.lock().unwrap().clone();
|
||||
let Some(assets) = assets else {
|
||||
log::debug!("No update available.");
|
||||
} else {
|
||||
let download_url = update_url.replace("tag", "download");
|
||||
let version = download_url.split('/').last().unwrap_or_default();
|
||||
#[cfg(target_os = "windows")]
|
||||
let download_url = if cfg!(feature = "flutter") {
|
||||
format!(
|
||||
"{}/rustdesk-{}-x86_64.{}",
|
||||
download_url,
|
||||
version,
|
||||
if update_msi { "msi" } else { "exe" }
|
||||
)
|
||||
return Ok(());
|
||||
};
|
||||
let download_url = assets.binary_url;
|
||||
let sha256_url = assets.sha256_url;
|
||||
let version = assets.binary_name;
|
||||
log::debug!("New version available: {}", &version);
|
||||
let client = create_http_client_with_url(&download_url);
|
||||
let Some(file_path) = get_download_file_from_url(&download_url) else {
|
||||
bail!("Failed to get the file path from the URL: {}", download_url);
|
||||
};
|
||||
let mut is_file_exists = false;
|
||||
if file_path.exists() {
|
||||
// Check if the file size is the same as the server file size
|
||||
// If the file size is the same, we don't need to download it again.
|
||||
let file_size = std::fs::metadata(&file_path)?.len();
|
||||
let response = client.head(&download_url).send()?;
|
||||
if !response.status().is_success() {
|
||||
bail!("Failed to get the file size: {}", response.status());
|
||||
}
|
||||
let total_size = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_LENGTH)
|
||||
.and_then(|ct_len| ct_len.to_str().ok())
|
||||
.and_then(|ct_len| ct_len.parse::<u64>().ok());
|
||||
let Some(total_size) = total_size else {
|
||||
bail!("Failed to get content length");
|
||||
};
|
||||
if file_size == total_size {
|
||||
is_file_exists = true;
|
||||
} else {
|
||||
format!("{}/rustdesk-{}-x86-sciter.exe", download_url, version)
|
||||
};
|
||||
log::debug!("New version available: {}", &version);
|
||||
let client = create_http_client_with_url(&download_url);
|
||||
let Some(file_path) = get_download_file_from_url(&download_url) else {
|
||||
bail!("Failed to get the file path from the URL: {}", download_url);
|
||||
};
|
||||
let mut is_file_exists = false;
|
||||
if file_path.exists() {
|
||||
// Check if the file size is the same as the server file size
|
||||
// If the file size is the same, we don't need to download it again.
|
||||
let file_size = std::fs::metadata(&file_path)?.len();
|
||||
let response = client.head(&download_url).send()?;
|
||||
if !response.status().is_success() {
|
||||
bail!("Failed to get the file size: {}", response.status());
|
||||
}
|
||||
let total_size = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_LENGTH)
|
||||
.and_then(|ct_len| ct_len.to_str().ok())
|
||||
.and_then(|ct_len| ct_len.parse::<u64>().ok());
|
||||
let Some(total_size) = total_size else {
|
||||
bail!("Failed to get content length");
|
||||
};
|
||||
if file_size == total_size {
|
||||
is_file_exists = true;
|
||||
} else {
|
||||
std::fs::remove_file(&file_path)?;
|
||||
}
|
||||
std::fs::remove_file(&file_path)?;
|
||||
}
|
||||
if !is_file_exists {
|
||||
let response = client.get(&download_url).send()?;
|
||||
if !response.status().is_success() {
|
||||
bail!(
|
||||
"Failed to download the new version file: {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
let file_data = response.bytes()?;
|
||||
let mut file = std::fs::File::create(&file_path)?;
|
||||
file.write_all(&file_data)?;
|
||||
}
|
||||
// We have checked if the `conns` is empty before, but we need to check again.
|
||||
// No need to care about the downloaded file here, because it's rare case that the `conns` are empty
|
||||
// before the download, but not empty after the download.
|
||||
if has_no_active_conns() {
|
||||
#[cfg(target_os = "windows")]
|
||||
update_new_version(update_msi, &version, &file_path);
|
||||
}
|
||||
if !is_file_exists {
|
||||
let response = client.get(&download_url).send()?;
|
||||
if !response.status().is_success() {
|
||||
bail!(
|
||||
"Failed to download the new version file: {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
let file_data = response.bytes()?;
|
||||
let mut file = std::fs::File::create(&file_path)?;
|
||||
file.write_all(&file_data)?;
|
||||
}
|
||||
// hello-agent local patch: verify the downloaded binary's SHA256 against
|
||||
// the `.sha256` companion asset published by the same Gitea release
|
||||
// before launching. We're about to run this file with elevated rights —
|
||||
// a mismatch means something went wrong in transit or the release was
|
||||
// tampered with, and we must NOT launch it. The expected-hash file is a
|
||||
// standard `sha256sum` output (`<hex> <filename>`) or just `<hex>`.
|
||||
let sha_client = create_http_client_with_url(&sha256_url);
|
||||
let sha_resp = sha_client.get(&sha256_url).send()?;
|
||||
if !sha_resp.status().is_success() {
|
||||
let _ = std::fs::remove_file(&file_path);
|
||||
bail!("Failed to download SHA256 file: {}", sha_resp.status());
|
||||
}
|
||||
let sha_text = sha_resp.text()?;
|
||||
let expected = sha_text
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
if expected.len() != 64 || !expected.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
let _ = std::fs::remove_file(&file_path);
|
||||
bail!("Malformed SHA256 file: {:?}", sha_text);
|
||||
}
|
||||
let file_bytes = std::fs::read(&file_path)?;
|
||||
use sha2::Digest as _;
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(&file_bytes);
|
||||
let actual = hex::encode(hasher.finalize());
|
||||
if actual != expected {
|
||||
log::error!(
|
||||
"SHA256 mismatch for {}: expected {}, got {}",
|
||||
version,
|
||||
expected,
|
||||
actual
|
||||
);
|
||||
let _ = std::fs::remove_file(&file_path);
|
||||
bail!("SHA256 verification failed");
|
||||
}
|
||||
log::info!("SHA256 verified for {}", version);
|
||||
// We have checked if the `conns` is empty before, but we need to check again.
|
||||
// No need to care about the downloaded file here, because it's rare case that the `conns` are empty
|
||||
// before the download, but not empty after the download.
|
||||
if has_no_active_conns() {
|
||||
#[cfg(target_os = "windows")]
|
||||
update_new_version(update_msi, &version, &file_path);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
pub const VERSION: &str = "1.4.6";
|
||||
#[allow(dead_code)]
|
||||
pub const BUILD_DATE: &str = "2026-05-09 10:43";
|
||||
pub const BUILD_DATE: &str = "2026-05-21 13:02";
|
||||
|
||||
Reference in New Issue
Block a user