libsubconverter/utils/
http_std.rs

1use crate::utils::system::get_system_proxy;
2use awc::Client;
3use case_insensitive_string::CaseInsensitiveString;
4use std::collections::HashMap;
5use std::error::Error as StdError;
6use std::time::Duration;
7/// Default timeout for HTTP requests in seconds
8const DEFAULT_TIMEOUT: u64 = 15;
9
10#[derive(Debug, Clone)]
11pub struct ProxyConfig {
12    pub proxy: Option<String>,
13}
14
15impl Default for ProxyConfig {
16    fn default() -> Self {
17        ProxyConfig { proxy: None }
18    }
19}
20
21/// HTTP response structure
22#[derive(Debug, Clone)]
23pub struct HttpResponse {
24    /// HTTP status code
25    pub status: u16,
26    /// Response body
27    pub body: String,
28    /// Response headers
29    pub headers: HashMap<String, String>,
30}
31
32/// HTTP error structure
33#[derive(Debug, Clone)]
34pub struct HttpError {
35    /// Error message
36    pub message: String,
37    /// Optional status code if available
38    pub status: Option<u16>,
39}
40
41impl std::fmt::Display for HttpError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        if let Some(status) = self.status {
44            write!(f, "HTTP error {}: {}", status, self.message)
45        } else {
46            write!(f, "HTTP error: {}", self.message)
47        }
48    }
49}
50
51impl StdError for HttpError {
52    fn source(&self) -> Option<&(dyn StdError + 'static)> {
53        None
54    }
55}
56
57pub fn parse_proxy(proxy_str: &str) -> ProxyConfig {
58    if proxy_str == "SYSTEM" {
59        return ProxyConfig {
60            proxy: Some(get_system_proxy()),
61        };
62    } else if proxy_str == "NONE" {
63        return ProxyConfig { proxy: None };
64    } else if !proxy_str.is_empty() {
65        return ProxyConfig {
66            proxy: Some(proxy_str.to_string()),
67        };
68    }
69    ProxyConfig { proxy: None }
70}
71
72/// Makes an HTTP request to the specified URL
73///
74/// # Arguments
75/// * `url` - The URL to request
76/// * `proxy_str` - Optional proxy string (e.g., "http://127.0.0.1:8080")
77/// * `headers` - Optional custom headers
78///
79/// # Returns
80/// * `Ok(HttpResponse)` - The response with status, body, and headers
81/// * `Err(HttpError)` - Error details if the request failed
82pub async fn web_get_async(
83    url: &str,
84    proxy_config: &ProxyConfig,
85    headers: Option<&HashMap<CaseInsensitiveString, String>>,
86) -> Result<HttpResponse, HttpError> {
87    // Build client with proxy if specified
88
89    let mut client_builder = Client::builder().timeout(Duration::from_secs(DEFAULT_TIMEOUT));
90
91    // if let Some(proxy) = &proxy_config.proxy {
92    //     if !proxy.is_empty() {
93    //         match Proxy::all(proxy) {
94    //             Ok(proxy) => {
95    //                 client_builder = client_builder.proxy(proxy);
96    //             }
97    //             Err(e) => {
98    //                 return Err(HttpError {
99    //                     message: format!("Failed to set proxy: {}", e),
100    //                     status: None,
101    //                 });
102    //             }
103    //         }
104    //     }
105    // }
106
107    let client = client_builder.finish();
108
109    // Build request with headers if specified
110    let mut client_request = client
111        .get(url)
112        .insert_header(("User-Agent", "subconverter-rs"));
113    if let Some(custom_headers) = headers {
114        for (key, value) in custom_headers {
115            client_request = client_request.insert_header((key.to_string(), value.to_string()));
116        }
117    }
118
119    // Send request and get response
120    let mut response = match client_request.send().await {
121        Ok(resp) => resp,
122        Err(e) => {
123            return Err(HttpError {
124                message: format!("Failed to send request: {}", e),
125                status: None,
126            });
127        }
128    };
129
130    // Get status and headers before attempting to read the body
131    let status = response.status().as_u16();
132
133    // Get response headers
134    let mut resp_headers = HashMap::new();
135    for (key, value) in response.headers() {
136        if let Ok(v) = value.to_str() {
137            resp_headers.insert(key.to_string(), v.to_string());
138        }
139    }
140
141    // Get response body, even for error responses
142    match response.body().await {
143        Ok(body) => Ok(HttpResponse {
144            status,
145            body: String::from_utf8(body.to_vec()).unwrap(),
146            headers: resp_headers,
147        }),
148        Err(e) => Err(HttpError {
149            message: format!("Failed to read response body: {}", e),
150            status: Some(status),
151        }),
152    }
153}
154
155/// Synchronous version of web_get_async that uses tokio runtime to run the
156/// async function
157///
158/// This function is provided for compatibility with the existing codebase.
159pub fn web_get(
160    url: &str,
161    proxy_config: &ProxyConfig,
162    headers: Option<&HashMap<CaseInsensitiveString, String>>,
163) -> Result<HttpResponse, HttpError> {
164    // Create a tokio runtime for running the async function
165    let rt = match tokio::runtime::Builder::new_current_thread()
166        .enable_all()
167        .build()
168    {
169        Ok(rt) => rt,
170        Err(e) => {
171            return Err(HttpError {
172                message: format!("Failed to create tokio runtime: {}", e),
173                status: None,
174            });
175        }
176    };
177
178    // Run the async function in the runtime
179    rt.block_on(web_get_async(url, proxy_config, headers))
180}
181
182/// Asynchronous function that returns only the body content if status is 2xx,
183/// otherwise treats as error
184/// This provides backward compatibility with code expecting only successful
185/// responses
186pub async fn web_get_content_async(
187    url: &str,
188    proxy_config: &ProxyConfig,
189    headers: Option<&HashMap<CaseInsensitiveString, String>>,
190) -> Result<String, String> {
191    match web_get_async(url, proxy_config, headers).await {
192        Ok(response) => {
193            if (200..300).contains(&response.status) {
194                Ok(response.body)
195            } else {
196                Err(format!("HTTP error {}: {}", response.status, response.body))
197            }
198        }
199        Err(e) => Err(e.message),
200    }
201}
202
203/// Extract subscription info from HTTP headers
204///
205/// # Arguments
206/// * `headers` - HTTP response headers
207///
208/// # Returns
209/// * Subscription info string with key-value pairs
210pub fn get_sub_info_from_header(headers: &HashMap<String, String>) -> String {
211    let mut sub_info = String::new();
212
213    // Extract upload and download
214    let mut upload: u64 = 0;
215    let mut download: u64 = 0;
216    let mut total: u64 = 0;
217    let mut expire: String = String::new();
218
219    // Look for subscription-userinfo header
220    if let Some(userinfo) = headers.get("subscription-userinfo") {
221        for info_item in userinfo.split(';') {
222            let info_item = info_item.trim();
223            if info_item.starts_with("upload=") {
224                if let Ok(value) = info_item[7..].parse::<u64>() {
225                    upload = value;
226                }
227            } else if info_item.starts_with("download=") {
228                if let Ok(value) = info_item[9..].parse::<u64>() {
229                    download = value;
230                }
231            } else if info_item.starts_with("total=") {
232                if let Ok(value) = info_item[6..].parse::<u64>() {
233                    total = value;
234                }
235            } else if info_item.starts_with("expire=") {
236                expire = info_item[7..].to_string();
237            }
238        }
239    }
240
241    // Add traffic info
242    if upload > 0 || download > 0 {
243        sub_info.push_str(&format!("upload={}, download={}", upload, download));
244    }
245
246    // Add total traffic
247    if total > 0 {
248        if !sub_info.is_empty() {
249            sub_info.push_str(", ");
250        }
251        sub_info.push_str(&format!("total={}", total));
252    }
253
254    // Add expiry info
255    if !expire.is_empty() {
256        if !sub_info.is_empty() {
257            sub_info.push_str(", ");
258        }
259        sub_info.push_str(&format!("expire={}", expire));
260    }
261
262    sub_info
263}
264
265/// Get subscription info from response headers with additional formatting
266///
267/// # Arguments
268/// * `headers` - HTTP response headers
269/// * `sub_info` - Mutable string to append info to
270///
271/// # Returns
272/// * `true` if info was extracted, `false` otherwise
273pub fn get_sub_info_from_response(
274    headers: &HashMap<String, String>,
275    sub_info: &mut String,
276) -> bool {
277    let header_info = get_sub_info_from_header(headers);
278    if !header_info.is_empty() {
279        if !sub_info.is_empty() {
280            sub_info.push_str(", ");
281        }
282        sub_info.push_str(&header_info);
283        true
284    } else {
285        false
286    }
287}
288
289/// Makes an HTTP POST request to the specified URL
290///
291/// # Arguments
292/// * `url` - The URL to request
293/// * `data` - The request body data
294/// * `proxy_config` - Proxy configuration
295/// * `headers` - Optional custom headers
296///
297/// # Returns
298/// * `Ok(HttpResponse)` - The response with status, body, and headers
299/// * `Err(HttpError)` - Error details if the request failed
300pub async fn web_post_async(
301    url: &str,
302    data: String,
303    proxy_config: &ProxyConfig,
304    headers: Option<&HashMap<CaseInsensitiveString, String>>,
305) -> Result<HttpResponse, HttpError> {
306    let mut client_builder = Client::builder().timeout(Duration::from_secs(DEFAULT_TIMEOUT));
307
308    // TODO: Implement proxy support for awc if needed, similar to commented-out
309    // code in web_get_async if let Some(proxy) = &proxy_config.proxy {
310    //     if !proxy.is_empty() { ... }
311    // }
312
313    let client = client_builder.finish();
314
315    let mut client_request = client
316        .post(url)
317        .insert_header(("Content-Type", "application/json")); // Assume JSON for POST/PATCH
318
319    if let Some(custom_headers) = headers {
320        for (key, value) in custom_headers {
321            client_request = client_request.insert_header((key.to_string(), value.to_string()));
322        }
323    }
324
325    // Send request with body
326    let mut response = match client_request.send_body(data).await {
327        Ok(resp) => resp,
328        Err(e) => {
329            return Err(HttpError {
330                message: format!("Failed to send POST request: {}", e),
331                status: None,
332            });
333        }
334    };
335
336    let status = response.status().as_u16();
337
338    let mut resp_headers = HashMap::new();
339    for (key, value) in response.headers() {
340        if let Ok(v) = value.to_str() {
341            resp_headers.insert(key.to_string(), v.to_string());
342        }
343    }
344
345    match response.body().limit(10_000_000).await {
346        // Limit body size (e.g., 10MB)
347        Ok(body) => Ok(HttpResponse {
348            status,
349            body: String::from_utf8(body.to_vec())
350                .unwrap_or_else(|_| "Invalid UTF-8 body".to_string()),
351            headers: resp_headers,
352        }),
353        Err(e) => Err(HttpError {
354            message: format!("Failed to read POST response body: {}", e),
355            status: Some(status),
356        }),
357    }
358}
359
360/// Makes an HTTP PATCH request to the specified URL
361///
362/// # Arguments
363/// * `url` - The URL to request
364/// * `data` - The request body data
365/// * `proxy_config` - Proxy configuration
366/// * `headers` - Optional custom headers
367///
368/// # Returns
369/// * `Ok(HttpResponse)` - The response with status, body, and headers
370/// * `Err(HttpError)` - Error details if the request failed
371pub async fn web_patch_async(
372    url: &str,
373    data: String,
374    proxy_config: &ProxyConfig,
375    headers: Option<&HashMap<CaseInsensitiveString, String>>,
376) -> Result<HttpResponse, HttpError> {
377    let mut client_builder = Client::builder().timeout(Duration::from_secs(DEFAULT_TIMEOUT));
378
379    // TODO: Implement proxy support for awc if needed
380
381    let client = client_builder.finish();
382
383    let mut client_request = client
384        .patch(url)
385        .insert_header(("Content-Type", "application/json")); // Assume JSON
386
387    if let Some(custom_headers) = headers {
388        for (key, value) in custom_headers {
389            client_request = client_request.insert_header((key.to_string(), value.to_string()));
390        }
391    }
392
393    // Send request with body
394    let mut response = match client_request.send_body(data).await {
395        Ok(resp) => resp,
396        Err(e) => {
397            return Err(HttpError {
398                message: format!("Failed to send PATCH request: {}", e),
399                status: None,
400            });
401        }
402    };
403
404    let status = response.status().as_u16();
405
406    let mut resp_headers = HashMap::new();
407    for (key, value) in response.headers() {
408        if let Ok(v) = value.to_str() {
409            resp_headers.insert(key.to_string(), v.to_string());
410        }
411    }
412
413    match response.body().limit(10_000_000).await {
414        // Limit body size
415        Ok(body) => Ok(HttpResponse {
416            status,
417            body: String::from_utf8(body.to_vec())
418                .unwrap_or_else(|_| "Invalid UTF-8 body".to_string()),
419            headers: resp_headers,
420        }),
421        Err(e) => Err(HttpError {
422            message: format!("Failed to read PATCH response body: {}", e),
423            status: Some(status),
424        }),
425    }
426}