From 70931b34914018b8a82712e41cb770da97991807 Mon Sep 17 00:00:00 2001 From: orange-bot Date: Thu, 8 Oct 2026 18:15:03 +0800 Subject: [PATCH] fix(download): route the download stack through env proxies and add read/write timeouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 下载链路(src-tauri/src/ureq.rs)之前用 ureq 的默认 agent:既不读代理,也没有读写 超时。前端接口走 reqwest(默认吃环境代理)能通,而文件下载是硬直连,于是网络受限时 会出现「接口正常、下载卡住」;即便连上了,ureq 默认没有读超时,服务端不再发数据时 读操作会一直阻塞,既不会报错也不会触发已有的重试。 - 打开 ureq 的 `proxy-from-env`:非回环目标的下载 agent 用 `try_proxy_from_env(true)`,`ALL_PROXY` / `HTTPS_PROXY` / `HTTP_PROXY`(大小写都认) 一个都没配时就是直连,不需要额外的设置项 - 回环地址(localhost / 127.0.0.0/8 / ::1)单独走直连 agent:本机地址不该被代理接管, 而且开了这个 feature 之后 `AgentBuilder::new()` 默认就吃环境代理,必须显式关掉 - 补上 ureq 默认没有的读/写超时(各 30s,连接超时沿用 30s):连接建立后服务端长时间 不发数据时读操作报错,交给已有的断点重试/单连接回退处理,而不是永久卡死 - 同时打开 `socks-proxy`:否则 `ALL_PROXY=socks5://…` 会在连接时报 `SOCKS feature disabled` --- Cargo.lock | 12 ++++++++ src-tauri/Cargo.toml | 2 +- src-tauri/src/ureq.rs | 72 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index caae7fa..0860ac8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4142,6 +4142,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + [[package]] name = "softbuffer" version = "0.4.8" @@ -5283,6 +5294,7 @@ dependencies = [ "rustls-pki-types", "serde", "serde_json", + "socks", "url", "webpki-roots 0.26.11", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f961308..f609afa 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -30,7 +30,7 @@ zip = { version = "4", default-features = false, features = ["deflate-flate2-zli serde = { version = "1", features = ["derive"] } serde_yaml = "0.9.25" serde_json = "1" -ureq = { version = "2", features = ["json", "gzip", "tls"] } +ureq = { version = "2", features = ["json", "gzip", "tls", "proxy-from-env", "socks-proxy"] } strip_bom = "1" open = "5" dirs = "6" diff --git a/src-tauri/src/ureq.rs b/src-tauri/src/ureq.rs index e8f84d4..6f70893 100644 --- a/src-tauri/src/ureq.rs +++ b/src-tauri/src/ureq.rs @@ -1,7 +1,8 @@ use std::io::{Read, Seek, SeekFrom, Write}; +use std::net::IpAddr; use std::path::{Path, PathBuf}; use std::sync::{ - Arc, Mutex, + Arc, Mutex, OnceLock, atomic::{AtomicBool, Ordering}, }; use std::time::{Duration, Instant}; @@ -26,8 +27,52 @@ fn user_agent() -> String { ) } +/// 所有下载请求共用的 agent:代理走 ureq 的 `proxy-from-env`(`ALL_PROXY` / +/// `HTTPS_PROXY` / `HTTP_PROXY`,大小写都认;一个都没配就是直连),并补上 ureq +/// 默认没有的读/写超时——连接建立后服务端不再发数据时读操作必须报错,否则会一直 +/// 卡住:既不触发已有的重试,分段下载里的 `join` 也永远不返回。 +/// +/// 回环地址单独用直连 agent:本机地址不该被代理接管(本地测试服务器、开发环境)。 +fn download_agent(url: &str) -> &'static ureq::Agent { + static DIRECT_AGENT: OnceLock = OnceLock::new(); + static PROXY_AGENT: OnceLock = OnceLock::new(); + + fn build(try_proxy_from_env: bool) -> ureq::Agent { + // 必须显式写 `try_proxy_from_env`:开了 `proxy-from-env` 之后 + // `AgentBuilder::new()` 的默认值就是 true。 + ureq::AgentBuilder::new() + .timeout_connect(Duration::from_secs(30)) + .timeout_read(Duration::from_secs(30)) + .timeout_write(Duration::from_secs(30)) + .try_proxy_from_env(try_proxy_from_env) + .build() + } + + if is_loopback_url(url) { + DIRECT_AGENT.get_or_init(|| build(false)) + } else { + PROXY_AGENT.get_or_init(|| build(true)) + } +} + +/// 目标是本机地址(`localhost`、`127.0.0.0/8`、`::1`)时不需要代理。 +fn is_loopback_url(url: &str) -> bool { + let Ok(url) = url::Url::parse(url) else { + return false; + }; + let Some(host) = url.host_str() else { + return false; + }; + let host = host.trim_matches(['[', ']']); + host.eq_ignore_ascii_case("localhost") + || host + .parse::() + .is_ok_and(|address| address.is_loopback()) +} + fn make_request(url: &str) -> ureq::Request { - ureq::get(url) + download_agent(url) + .get(url) .set("Connection", "keep-alive") .set("User-Agent", &user_agent()) .set("Accept", "*/*") @@ -327,7 +372,8 @@ fn download_multi_thread( cancel_flag: &Arc, pause_flag: &Arc, ) -> anyhow::Result<()> { - let head = ureq::head(url) + let head = download_agent(url) + .head(url) .set("User-Agent", &user_agent()) .set("Accept", "*/*") .set("Accept-Encoding", "identity") @@ -545,6 +591,26 @@ mod tests { String::from_utf8(request).unwrap() } + #[test] + fn loopback_targets_skip_the_proxy() { + for url in [ + "http://127.0.0.1:8080/x", + "http://127.1.2.3/x", + "http://[::1]:8080/x", + "http://localhost:8080/x", + ] { + assert!(super::is_loopback_url(url), "{url}"); + } + for url in [ + "https://gamebanana.com/dl/1", + "http://192.168.8.1/x", + "http://localhost.example.com/x", + "not a url", + ] { + assert!(!super::is_loopback_url(url), "{url}"); + } + } + #[test] fn parses_content_range() { assert_eq!(parse_content_range("bytes 10-19/100"), Some((10, 19, 100)));