mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: aether-proxy 自升级功能
新增 `aether-proxy upgrade [version]` 命令,支持从 GitHub Releases 下载指定或最新版本的二进制,校验 SHA256 后原子替换当前二进制, 并在 systemd 服务运行时自动重启。
This commit is contained in:
99
aether-proxy/Cargo.lock
generated
99
aether-proxy/Cargo.lock
generated
@@ -2,6 +2,12 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aether-proxy"
|
name = "aether-proxy"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -11,6 +17,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm 0.28.1",
|
"crossterm 0.28.1",
|
||||||
|
"flate2",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
@@ -29,6 +36,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
|
"tar",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
@@ -338,6 +346,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -586,6 +603,17 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"libredox",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -604,6 +632,16 @@ version = "0.4.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -1094,6 +1132,17 @@ version = "0.2.180"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall 0.7.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "line-clipping"
|
name = "line-clipping"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
@@ -1203,6 +1252,16 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -1330,7 +1389,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall 0.5.18",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
@@ -1549,7 +1608,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1738,6 +1797,15 @@ dependencies = [
|
|||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.3"
|
version = "1.12.3"
|
||||||
@@ -2055,6 +2123,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -2184,6 +2258,17 @@ dependencies = [
|
|||||||
"windows",
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tar"
|
||||||
|
version = "0.4.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
||||||
|
dependencies = [
|
||||||
|
"filetime",
|
||||||
|
"libc",
|
||||||
|
"xattr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "terminfo"
|
name = "terminfo"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -3118,6 +3203,16 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xattr"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rustix 1.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yasna"
|
name = "yasna"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ crossterm = "0.28"
|
|||||||
url = "2"
|
url = "2"
|
||||||
sysinfo = "0.32"
|
sysinfo = "0.32"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
flate2 = "1"
|
||||||
|
tar = "0.4"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
"restart" => return setup::service::cmd_restart(),
|
"restart" => return setup::service::cmd_restart(),
|
||||||
"stop" => return setup::service::cmd_stop(),
|
"stop" => return setup::service::cmd_stop(),
|
||||||
"uninstall" => return setup::service::cmd_uninstall(),
|
"uninstall" => return setup::service::cmd_uninstall(),
|
||||||
|
"upgrade" => {
|
||||||
|
let version = args.get(2).cloned();
|
||||||
|
return setup::upgrade::cmd_upgrade(version).await;
|
||||||
|
}
|
||||||
_ => {} // fall through to clap (--help, --version, config args)
|
_ => {} // fall through to clap (--help, --version, config args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub(crate) mod service;
|
pub(crate) mod service;
|
||||||
mod tui;
|
mod tui;
|
||||||
|
pub(crate) mod upgrade;
|
||||||
|
|
||||||
pub use self::tui::run;
|
pub use self::tui::run;
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ fn is_systemd_available() -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_root() -> bool {
|
pub(crate) fn is_root() -> bool {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
unsafe { libc::geteuid() == 0 }
|
unsafe { libc::geteuid() == 0 }
|
||||||
@@ -241,7 +241,7 @@ pub fn cmd_uninstall() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_cmd(program: &str, args: &[&str]) -> anyhow::Result<()> {
|
pub(crate) fn run_cmd(program: &str, args: &[&str]) -> anyhow::Result<()> {
|
||||||
let display = format!("{} {}", program, args.join(" "));
|
let display = format!("{} {}", program, args.join(" "));
|
||||||
eprintln!(" > {}", display);
|
eprintln!(" > {}", display);
|
||||||
|
|
||||||
|
|||||||
379
aether-proxy/src/setup/upgrade.rs
Normal file
379
aether-proxy/src/setup/upgrade.rs
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
//! Self-upgrade for aether-proxy.
|
||||||
|
//!
|
||||||
|
//! Downloads a release from GitHub, verifies SHA256 checksum, and atomically
|
||||||
|
//! replaces the running binary. Restarts the systemd service if active.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
const GITHUB_API_BASE: &str = "https://api.github.com";
|
||||||
|
const GITHUB_REPO: &str = "fawney19/Aether";
|
||||||
|
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
// ── GitHub API types ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct GithubRelease {
|
||||||
|
tag_name: String,
|
||||||
|
name: String,
|
||||||
|
assets: Vec<GithubAsset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct GithubAsset {
|
||||||
|
name: String,
|
||||||
|
id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Platform detection ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn detect_platform() -> &'static str {
|
||||||
|
if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
|
||||||
|
"linux-amd64"
|
||||||
|
} else if cfg!(target_os = "linux") && cfg!(target_arch = "aarch64") {
|
||||||
|
"linux-arm64"
|
||||||
|
} else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
|
||||||
|
"macos-amd64"
|
||||||
|
} else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
|
||||||
|
"macos-arm64"
|
||||||
|
} else if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") {
|
||||||
|
"windows-amd64"
|
||||||
|
} else {
|
||||||
|
// All supported targets are covered above; this is unreachable for
|
||||||
|
// any platform we actually build for.
|
||||||
|
panic!("unsupported platform: compile-time target not in the supported matrix")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GitHub HTTP client ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn build_github_client() -> anyhow::Result<reqwest::Client> {
|
||||||
|
let token = std::env::var("GITHUB_TOKEN").map_err(|_| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"GITHUB_TOKEN is required (private repo).\n Set it via: export GITHUB_TOKEN=ghp_xxx"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
reqwest::header::AUTHORIZATION,
|
||||||
|
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", token))?,
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
reqwest::header::ACCEPT,
|
||||||
|
reqwest::header::HeaderValue::from_static("application/vnd.github+json"),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(300))
|
||||||
|
.user_agent(format!("aether-proxy/{}", CURRENT_VERSION))
|
||||||
|
.default_headers(headers)
|
||||||
|
.build()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Release fetching ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async fn fetch_release(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
version: Option<&str>,
|
||||||
|
) -> anyhow::Result<GithubRelease> {
|
||||||
|
match version {
|
||||||
|
Some(ver) => {
|
||||||
|
// Accept both "proxy-v0.2.0" and bare "0.2.0"
|
||||||
|
let tag = if ver.starts_with("proxy-v") {
|
||||||
|
ver.to_string()
|
||||||
|
} else {
|
||||||
|
format!("proxy-v{}", ver)
|
||||||
|
};
|
||||||
|
let url = format!(
|
||||||
|
"{}/repos/{}/releases/tags/{}",
|
||||||
|
GITHUB_API_BASE, GITHUB_REPO, tag
|
||||||
|
);
|
||||||
|
let resp = client.get(&url).send().await?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
anyhow::bail!("release '{}' not found (HTTP {}): {}", tag, status, body);
|
||||||
|
}
|
||||||
|
Ok(resp.json().await?)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// List releases and find the latest proxy-v* tag
|
||||||
|
let url = format!(
|
||||||
|
"{}/repos/{}/releases?per_page=20",
|
||||||
|
GITHUB_API_BASE, GITHUB_REPO
|
||||||
|
);
|
||||||
|
let resp = client.get(&url).send().await?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
anyhow::bail!("failed to list releases (HTTP {}): {}", status, body);
|
||||||
|
}
|
||||||
|
let releases: Vec<GithubRelease> = resp.json().await?;
|
||||||
|
releases
|
||||||
|
.into_iter()
|
||||||
|
.find(|r| r.tag_name.starts_with("proxy-v"))
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("no proxy-v* release found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Download & verify ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async fn download_asset_bytes(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
asset: &GithubAsset,
|
||||||
|
) -> anyhow::Result<Vec<u8>> {
|
||||||
|
// Use GitHub API asset endpoint for reliable private repo downloads
|
||||||
|
let url = format!(
|
||||||
|
"{}/repos/{}/releases/assets/{}",
|
||||||
|
GITHUB_API_BASE, GITHUB_REPO, asset.id
|
||||||
|
);
|
||||||
|
let resp = client
|
||||||
|
.get(&url)
|
||||||
|
.header(reqwest::header::ACCEPT, "application/octet-stream")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"download failed for '{}' (HTTP {})",
|
||||||
|
asset.name,
|
||||||
|
resp.status(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(resp.bytes().await?.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_checksum(sums_text: &str, filename: &str) -> anyhow::Result<String> {
|
||||||
|
for line in sums_text.lines() {
|
||||||
|
// Format: "<hash> <filename>" (GNU coreutils convention)
|
||||||
|
let mut parts = line.split_ascii_whitespace();
|
||||||
|
let (Some(hash), Some(name)) = (parts.next(), parts.next()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if name == filename || name.ends_with(filename) {
|
||||||
|
return Ok(hash.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
anyhow::bail!("checksum for '{}' not found in SHA256SUMS.txt", filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn download_and_verify(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
release: &GithubRelease,
|
||||||
|
platform: &str,
|
||||||
|
dest: &Path,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let archive_name = format!("aether-proxy-{}.tar.gz", platform);
|
||||||
|
|
||||||
|
let archive_asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name == archive_name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("asset '{}' not found in release", archive_name))?;
|
||||||
|
|
||||||
|
let checksum_asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name == "SHA256SUMS.txt")
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("SHA256SUMS.txt not found in release"))?;
|
||||||
|
|
||||||
|
eprintln!(" Downloading {}...", archive_name);
|
||||||
|
let (archive_bytes, checksum_bytes) = tokio::try_join!(
|
||||||
|
download_asset_bytes(client, archive_asset),
|
||||||
|
download_asset_bytes(client, checksum_asset),
|
||||||
|
)?;
|
||||||
|
let checksum_text = String::from_utf8(checksum_bytes)?;
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
" Downloaded {} ({} bytes)",
|
||||||
|
archive_name,
|
||||||
|
archive_bytes.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify SHA256
|
||||||
|
let expected_hash = parse_checksum(&checksum_text, &archive_name)?;
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&archive_bytes);
|
||||||
|
let actual_hash = hex::encode(hasher.finalize());
|
||||||
|
|
||||||
|
if actual_hash != expected_hash {
|
||||||
|
anyhow::bail!(
|
||||||
|
"SHA256 mismatch for {}:\n expected: {}\n actual: {}",
|
||||||
|
archive_name,
|
||||||
|
expected_hash,
|
||||||
|
actual_hash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
eprintln!(" SHA256 verified: {}", &actual_hash[..16]);
|
||||||
|
|
||||||
|
extract_binary(&archive_bytes, dest)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Archive extraction ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn extract_binary(archive_bytes: &[u8], dest: &Path) -> anyhow::Result<()> {
|
||||||
|
use flate2::read::GzDecoder;
|
||||||
|
use tar::Archive;
|
||||||
|
|
||||||
|
// Guard against decompression bombs
|
||||||
|
const MAX_BINARY_SIZE: u64 = 100 * 1024 * 1024; // 100 MB
|
||||||
|
|
||||||
|
let decoder = GzDecoder::new(archive_bytes);
|
||||||
|
let mut archive = Archive::new(decoder);
|
||||||
|
|
||||||
|
let binary_name = if cfg!(target_os = "windows") {
|
||||||
|
"aether-proxy.exe"
|
||||||
|
} else {
|
||||||
|
"aether-proxy"
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in archive.entries()? {
|
||||||
|
let mut entry = entry?;
|
||||||
|
// Only accept regular files -- reject symlinks to prevent write-through attacks
|
||||||
|
if entry.header().entry_type() != tar::EntryType::Regular {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path = entry.path()?;
|
||||||
|
if path.file_name().and_then(|n| n.to_str()) == Some(binary_name) {
|
||||||
|
let size = entry.header().size()?;
|
||||||
|
if size > MAX_BINARY_SIZE {
|
||||||
|
anyhow::bail!(
|
||||||
|
"binary too large ({} bytes, max {} bytes)",
|
||||||
|
size,
|
||||||
|
MAX_BINARY_SIZE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut file = std::fs::File::create(dest)?;
|
||||||
|
std::io::copy(&mut entry, &mut file)?;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
std::fs::set_permissions(dest, std::fs::Permissions::from_mode(0o755))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::bail!("'{}' not found in archive", binary_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Atomic binary replacement ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn atomic_replace(new_binary: &Path) -> anyhow::Result<PathBuf> {
|
||||||
|
let current_exe = std::env::current_exe()?.canonicalize()?;
|
||||||
|
let backup_path = current_exe.with_extension("bak");
|
||||||
|
|
||||||
|
// Remove stale backup
|
||||||
|
let _ = std::fs::remove_file(&backup_path);
|
||||||
|
|
||||||
|
// current -> .bak
|
||||||
|
std::fs::rename(¤t_exe, &backup_path).map_err(|e| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"failed to backup current binary '{}' -> '{}': {}",
|
||||||
|
current_exe.display(),
|
||||||
|
backup_path.display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// new -> current
|
||||||
|
if let Err(e) = std::fs::rename(new_binary, ¤t_exe) {
|
||||||
|
eprintln!(" ERROR: failed to place new binary, rolling back...");
|
||||||
|
let _ = std::fs::rename(&backup_path, ¤t_exe);
|
||||||
|
anyhow::bail!(
|
||||||
|
"failed to install new binary '{}' -> '{}': {}",
|
||||||
|
new_binary.display(),
|
||||||
|
current_exe.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!(" Binary replaced: {}", current_exe.display());
|
||||||
|
Ok(backup_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public entry point ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// `aether-proxy upgrade [version]` -- self-upgrade from GitHub releases.
|
||||||
|
pub async fn cmd_upgrade(version: Option<String>) -> anyhow::Result<()> {
|
||||||
|
// Resolve exe path once; reuse throughout the function
|
||||||
|
let current_exe = std::env::current_exe()?.canonicalize()?;
|
||||||
|
let exe_dir = current_exe
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("cannot determine binary directory"))?;
|
||||||
|
let temp_path = exe_dir.join(".aether-proxy.upgrade.tmp");
|
||||||
|
|
||||||
|
// Check write permission to binary directory
|
||||||
|
if !super::service::is_root() {
|
||||||
|
let test_path = exe_dir.join(".aether-proxy.write-test");
|
||||||
|
match std::fs::File::create(&test_path) {
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = std::fs::remove_file(&test_path);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
anyhow::bail!(
|
||||||
|
"no write access to {}. Use: sudo aether-proxy upgrade",
|
||||||
|
exe_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let platform = detect_platform();
|
||||||
|
eprintln!(" Platform: {}", platform);
|
||||||
|
eprintln!(" Current version: {}", CURRENT_VERSION);
|
||||||
|
|
||||||
|
let client = build_github_client()?;
|
||||||
|
let release = fetch_release(&client, version.as_deref()).await?;
|
||||||
|
let target_tag = &release.tag_name;
|
||||||
|
let target_semver = target_tag.strip_prefix("proxy-v").unwrap_or(target_tag);
|
||||||
|
|
||||||
|
eprintln!(" Target version: {} ({})", target_tag, release.name);
|
||||||
|
|
||||||
|
if target_semver == CURRENT_VERSION {
|
||||||
|
eprintln!(
|
||||||
|
" Already running version {}, nothing to do.",
|
||||||
|
CURRENT_VERSION
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(" Upgrading: {} -> {}", CURRENT_VERSION, target_semver);
|
||||||
|
eprintln!();
|
||||||
|
|
||||||
|
if let Err(e) = download_and_verify(&client, &release, platform, &temp_path).await {
|
||||||
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
let backup_path = match atomic_replace(&temp_path) {
|
||||||
|
Ok(backup) => backup,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = std::fs::remove_file(&temp_path);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restart systemd service if running
|
||||||
|
if super::service::is_service_active() {
|
||||||
|
eprintln!(" Restarting systemd service...");
|
||||||
|
super::service::run_cmd("systemctl", &["restart", "aether-proxy"])?;
|
||||||
|
eprintln!(" Service restarted.");
|
||||||
|
} else {
|
||||||
|
eprintln!(" No active systemd service detected, skipping restart.");
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(" Upgrade complete!");
|
||||||
|
eprintln!(
|
||||||
|
" Backup kept at: {} (will be cleaned up on next upgrade)",
|
||||||
|
backup_path.display()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user