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}