From c6030408ad66cab944d7035dcecb0f18f34f4455 Mon Sep 17 00:00:00 2001 From: Radiums <7265692+Radiums@users.noreply.github.com> Date: Sun, 23 Aug 2026 04:51:02 +0800 Subject: [PATCH 1/2] fix: respect proxy when probing download URLs --- src-tauri/src/commands/download.rs | 103 ++++++++++++++++++++++++----- src-tauri/src/lib.rs | 1 + src/services/updateService.ts | 22 +++--- 3 files changed, 101 insertions(+), 25 deletions(-) diff --git a/src-tauri/src/commands/download.rs b/src-tauri/src/commands/download.rs index 3a417640..e51764e7 100644 --- a/src-tauri/src/commands/download.rs +++ b/src-tauri/src/commands/download.rs @@ -59,6 +59,28 @@ static DOWNLOAD_CANCELLED: AtomicBool = AtomicBool::new(false); /// 当前下载的 session ID,用于区分不同的下载任务 static CURRENT_DOWNLOAD_SESSION: AtomicU64 = AtomicU64::new(0); +fn apply_proxy( + mut client_builder: reqwest::ClientBuilder, + proxy_url: Option<&str>, + log_prefix: &str, + target_url: &str, +) -> Result { + if let Some(proxy) = proxy_url.filter(|proxy| !proxy.is_empty()) { + info!("[{}] 使用代理: {}", log_prefix, proxy); + info!("[{}] 目标: {}", log_prefix, target_url); + let reqwest_proxy = reqwest::Proxy::all(proxy).map_err(|e| { + error!("代理配置失败: {} (代理地址: {})", e, proxy); + format!( + "代理配置失败: {}。请检查代理格式是否正确(支持 http:// 或 socks5://)", + e + ) + })?; + client_builder = client_builder.proxy(reqwest_proxy); + } + + Ok(client_builder) +} + /// 根据版本号获取 GitHub Release URL /// /// 使用 GitHub API 获取指定版本的 Release 信息,支持使用 GitHub PAT 和代理 @@ -79,21 +101,7 @@ pub async fn get_github_release_by_version( .timeout(std::time::Duration::from_secs(10)) .connect_timeout(std::time::Duration::from_secs(3)); - // 添加代理配置(如果提供) - if let Some(ref proxy) = proxy_url { - if !proxy.is_empty() { - info!("[检查更新] 使用代理: {}", proxy); - info!("[检查更新] 目标: {}", url); - let reqwest_proxy = reqwest::Proxy::all(proxy).map_err(|e| { - error!("代理配置失败: {} (代理地址: {})", e, proxy); - format!( - "代理配置失败: {}。请检查代理格式是否正确(支持 http:// 或 socks5://)", - e - ) - })?; - client_builder = client_builder.proxy(reqwest_proxy); - } - } + client_builder = apply_proxy(client_builder, proxy_url.as_deref(), "检查更新", &url)?; let client = client_builder .build() @@ -144,6 +152,32 @@ pub async fn get_github_release_by_version( Ok(None) } +/// 检查下载链接的 HTTP 状态码,支持使用代理 +#[tauri::command] +pub async fn probe_download_url(url: String, proxy_url: Option) -> Result { + let client_builder = reqwest::Client::builder() + .user_agent(build_user_agent()) + .timeout(std::time::Duration::from_secs(10)) + .connect_timeout(std::time::Duration::from_secs(3)); + let client_builder = apply_proxy( + client_builder, + proxy_url.as_deref(), + "检查直接下载链接", + &url, + )?; + let client = client_builder + .build() + .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; + + let response = client + .head(&url) + .send() + .await + .map_err(|e| format!("请求失败: {}", e))?; + + Ok(response.status().as_u16()) +} + /// 流式下载文件,支持进度回调和取消 /// /// 使用 reqwest 进行流式下载,直接写入文件而不经过内存缓冲, @@ -579,3 +613,42 @@ fn parse_content_disposition(header: &str) -> Option { None } + +#[cfg(test)] +mod tests { + use super::probe_download_url; + use std::io::{Read, Write}; + use std::net::TcpListener; + + #[test] + fn probe_download_url_uses_explicit_proxy() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind test proxy"); + let proxy_address = listener.local_addr().expect("get test proxy address"); + let proxy = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept proxied request"); + let mut request = [0_u8; 2048]; + let bytes_read = stream.read(&mut request).expect("read proxied request"); + let request = String::from_utf8_lossy(&request[..bytes_read]); + assert!(request.starts_with("HEAD http://example.invalid/update.zip HTTP/1.1")); + stream + .write_all( + b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + ) + .expect("write proxy response"); + }); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build Tokio runtime"); + let status = runtime + .block_on(probe_download_url( + "http://example.invalid/update.zip".to_string(), + Some(format!("http://{}", proxy_address)), + )) + .expect("probe through proxy"); + + assert_eq!(status, 204); + proxy.join().expect("join test proxy"); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f5db7803..a9c776ff 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -280,6 +280,7 @@ pub fn run() { commands::update::cleanup_update_artifacts, // 下载命令 commands::download::get_github_release_by_version, + commands::download::probe_download_url, commands::download::download_file, commands::download::cancel_download, // 系统相关命令 diff --git a/src/services/updateService.ts b/src/services/updateService.ts index 473da55a..8cb85acb 100644 --- a/src/services/updateService.ts +++ b/src/services/updateService.ts @@ -577,6 +577,7 @@ async function tryDirectDownloadUrls( repo: string, projectName: string, version: string, + proxyUrl?: string, ): Promise<{ url: string; filename: string } | null> { const extensions = getDownloadExtensions(); const arch = await getArch(); @@ -590,18 +591,13 @@ async function tryDirectDownloadUrls( try { log.info(`尝试直接下载链接: ${url}`); - const response = await tauriFetch(url, { - method: 'HEAD', - headers: { - 'User-Agent': await buildUserAgent(), - }, - }); + const status = await invoke('probe_download_url', { url, proxyUrl }); - if (response.ok) { + if (status >= 200 && status < 300) { log.info(`直接下载链接可用: ${filename}`); return { url, filename }; } - log.info(`直接下载链接不存在 (${response.status}): ${filename}`); + log.info(`直接下载链接不存在 (${status}): ${filename}`); } catch (error) { log.warn(`检查直接下载链接失败: ${filename}`, error); } @@ -663,7 +659,7 @@ export interface GetGitHubDownloadUrlOptions { targetVersion: string; // Mirror酱返回的目标版本号 githubPat?: string; // GitHub Personal Access Token (支持 classic 和 fine-grained) projectName?: string; // 项目名称,用于拼接直接下载链接(来自 interface.name) - proxyUrl?: string; // 代理 URL,用于 GitHub API 请求 + proxyUrl?: string; // 代理 URL,用于 GitHub API 请求和直接下载链接探测 } /** @@ -705,7 +701,13 @@ export async function getGitHubDownloadUrl( // API 失败或未匹配到 asset,尝试直接拼接下载链接 if (projectName) { - const directResult = await tryDirectDownloadUrls(owner, repo, projectName, targetVersion); + const directResult = await tryDirectDownloadUrls( + owner, + repo, + projectName, + targetVersion, + proxyUrl, + ); if (directResult) { log.info(`使用直接下载链接: ${directResult.filename}`); return { From 42f8b428c7d603979e86b3d2e801b8a4a560b15d Mon Sep 17 00:00:00 2001 From: Radiums <7265692+Radiums@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:59:48 +0800 Subject: [PATCH 2/2] test: handle fragmented proxy requests --- src-tauri/src/commands/download.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/commands/download.rs b/src-tauri/src/commands/download.rs index e51764e7..4d08ce71 100644 --- a/src-tauri/src/commands/download.rs +++ b/src-tauri/src/commands/download.rs @@ -626,9 +626,21 @@ mod tests { let proxy_address = listener.local_addr().expect("get test proxy address"); let proxy = std::thread::spawn(move || { let (mut stream, _) = listener.accept().expect("accept proxied request"); - let mut request = [0_u8; 2048]; - let bytes_read = stream.read(&mut request).expect("read proxied request"); - let request = String::from_utf8_lossy(&request[..bytes_read]); + let mut request = Vec::new(); + loop { + let mut chunk = [0_u8; 2048]; + let bytes_read = stream.read(&mut chunk).expect("read proxied request"); + assert!(bytes_read > 0, "proxy closed before request headers"); + request.extend_from_slice(&chunk[..bytes_read]); + if request.windows(4).any(|window| window == b"\r\n\r\n") { + break; + } + assert!( + request.len() <= 64 * 1024, + "proxy request headers too large" + ); + } + let request = String::from_utf8_lossy(&request); assert!(request.starts_with("HEAD http://example.invalid/update.zip HTTP/1.1")); stream .write_all(