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)));