Skip to main content

mangofetch_core/core/
http_client.rs

1use std::sync::LazyLock;
2use std::sync::RwLock;
3
4use crate::models::settings::ProxySettings;
5
6static GLOBAL_PROXY: LazyLock<RwLock<ProxySettings>> =
7    LazyLock::new(|| RwLock::new(ProxySettings::default()));
8
9pub fn init_proxy(proxy: ProxySettings) {
10    if let Ok(mut guard) = GLOBAL_PROXY.write() {
11        *guard = proxy;
12    }
13}
14
15pub fn get_proxy_snapshot() -> ProxySettings {
16    GLOBAL_PROXY.read().map(|g| g.clone()).unwrap_or_default()
17}
18
19pub fn proxy_url() -> Option<String> {
20    let proxy = get_proxy_snapshot();
21    if !proxy.enabled || proxy.host.is_empty() {
22        return None;
23    }
24    let scheme = match proxy.proxy_type.as_str() {
25        "socks5" => "socks5",
26        "https" => "https",
27        _ => "http",
28    };
29    if !proxy.username.is_empty() {
30        Some(format!(
31            "{}://{}:{}@{}:{}",
32            scheme, proxy.username, proxy.password, proxy.host, proxy.port
33        ))
34    } else {
35        Some(format!("{}://{}:{}", scheme, proxy.host, proxy.port))
36    }
37}
38
39pub fn apply_proxy(
40    builder: reqwest::ClientBuilder,
41    proxy: &ProxySettings,
42) -> reqwest::ClientBuilder {
43    if !proxy.enabled || proxy.host.is_empty() {
44        return builder;
45    }
46    let scheme = match proxy.proxy_type.as_str() {
47        "socks5" => "socks5",
48        "https" => "https",
49        _ => "http",
50    };
51    let proxy_url = if !proxy.username.is_empty() {
52        format!(
53            "{}://{}:{}@{}:{}",
54            scheme, proxy.username, proxy.password, proxy.host, proxy.port
55        )
56    } else {
57        format!("{}://{}:{}", scheme, proxy.host, proxy.port)
58    };
59    match reqwest::Proxy::all(&proxy_url) {
60        Ok(p) => builder.proxy(p),
61        Err(e) => {
62            tracing::warn!("Invalid proxy URL: {}", e);
63            builder
64        }
65    }
66}
67
68pub fn apply_global_proxy(builder: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
69    let proxy = get_proxy_snapshot();
70    apply_proxy(builder, &proxy)
71}
72
73pub fn inject_ua_header(headers: &mut reqwest::header::HeaderMap, opts_ua: Option<&str>) {
74    if let Some(ua) = opts_ua {
75        if let Ok(v) = reqwest::header::HeaderValue::from_str(ua) {
76            headers.insert(reqwest::header::USER_AGENT, v);
77        }
78    }
79}
80
81pub fn ua_header_map(opts_ua: Option<&str>) -> Option<reqwest::header::HeaderMap> {
82    let ua = opts_ua?;
83    let value = reqwest::header::HeaderValue::from_str(ua).ok()?;
84    let mut headers = reqwest::header::HeaderMap::new();
85    headers.insert(reqwest::header::USER_AGENT, value);
86    Some(headers)
87}
88
89pub async fn download_with_progress<F>(url: &str, mut on_progress: F) -> anyhow::Result<Vec<u8>>
90where
91    F: FnMut(f32) + Send,
92{
93    let client = apply_global_proxy(reqwest::Client::builder())
94        .timeout(std::time::Duration::from_secs(300))
95        .build()?;
96
97    let response = client.get(url).send().await?;
98    if !response.status().is_success() {
99        return Err(anyhow::anyhow!("HTTP error: {}", response.status()));
100    }
101
102    let total_size = response.content_length().unwrap_or(0);
103    let mut downloaded: u64 = 0;
104    let mut buffer = Vec::with_capacity(total_size as usize);
105
106    use futures::StreamExt;
107    let mut stream = response.bytes_stream();
108
109    while let Some(chunk_result) = stream.next().await {
110        let chunk = chunk_result?;
111        buffer.extend_from_slice(&chunk);
112        downloaded += chunk.len() as u64;
113
114        if total_size > 0 {
115            let percent = (downloaded as f32 / total_size as f32) * 100.0;
116            on_progress(percent);
117        }
118    }
119
120    Ok(buffer)
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::models::settings::ProxySettings;
127
128    #[test]
129    fn test_proxy_url_generation() {
130        // Disabled
131        init_proxy(ProxySettings {
132            enabled: false,
133            proxy_type: "http".into(),
134            host: "127.0.0.1".into(),
135            port: 8080,
136            username: "".into(),
137            password: "".into(),
138        });
139        assert_eq!(proxy_url(), None);
140
141        // Missing host
142        init_proxy(ProxySettings {
143            enabled: true,
144            proxy_type: "http".into(),
145            host: "".into(),
146            port: 8080,
147            username: "".into(),
148            password: "".into(),
149        });
150        assert_eq!(proxy_url(), None);
151
152        // HTTP proxy without auth
153        init_proxy(ProxySettings {
154            enabled: true,
155            proxy_type: "http".into(),
156            host: "127.0.0.1".into(),
157            port: 8080,
158            username: "".into(),
159            password: "".into(),
160        });
161        assert_eq!(proxy_url(), Some("http://127.0.0.1:8080".into()));
162
163        // SOCKS5 proxy with auth
164        init_proxy(ProxySettings {
165            enabled: true,
166            proxy_type: "socks5".into(),
167            host: "127.0.0.1".into(),
168            port: 1080,
169            username: "user".into(),
170            password: "password".into(),
171        });
172        assert_eq!(
173            proxy_url(),
174            Some("socks5://user:password@127.0.0.1:1080".into())
175        );
176
177        // HTTPS proxy without auth
178        init_proxy(ProxySettings {
179            enabled: true,
180            proxy_type: "https".into(),
181            host: "127.0.0.1".into(),
182            port: 443,
183            username: "".into(),
184            password: "".into(),
185        });
186        assert_eq!(proxy_url(), Some("https://127.0.0.1:443".into()));
187
188        // Unknown proxy type (falls back to http)
189        init_proxy(ProxySettings {
190            enabled: true,
191            proxy_type: "unknown".into(),
192            host: "127.0.0.1".into(),
193            port: 8080,
194            username: "".into(),
195            password: "".into(),
196        });
197        assert_eq!(proxy_url(), Some("http://127.0.0.1:8080".into()));
198    }
199}