Skip to main content

gooty_proxy/io/
http.rs

1//! # HTTP Module
2//!
3//! This module provides functionality for making HTTP requests with optional proxy support.
4//! It includes features for handling timeouts, error conversions, and response validation.
5//!
6//! ## Components
7//!
8//! * **Requestor** - A struct for making HTTP requests with or without proxy support
9//!
10//! ## Examples
11//!
12//! ```
13//! use gooty_proxy::io::http::Requestor;
14//!
15//! #[tokio::main]
16//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
17//!     let requestor = Requestor::new()?;
18//!     let response = requestor.get("https://example.com", "Mozilla/5.0").await?;
19//!     println!("Response: {}", response);
20//!     Ok(())
21//! }
22//! ```
23
24use crate::definitions::{
25    errors::{RequestResult, RequestorError},
26    proxy::Proxy,
27};
28use reqwest::{Client, Proxy as ReqwestProxy};
29use std::time::{Duration, Instant};
30
31/// Simple HTTP requestor with optional proxy support.
32///
33/// The Requestor provides methods to make HTTP requests with configurable
34/// timeout settings and optional proxy support. It handles error conversions,
35/// response validation, and timeouts in a consistent way.
36///
37/// # Examples
38///
39/// ```
40/// use gooty_proxy::io::requestor::Requestor;
41///
42/// #[tokio::main]
43/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
44///     // Create a new requestor with default timeout (30 seconds)
45///     let requestor = Requestor::new()?;
46///
47///     // Make a GET request
48///     let response = requestor.get(
49///         "https://example.com",
50///         "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
51///     ).await?;
52///
53///     println!("Response: {}", response);
54///     Ok(())
55/// }
56/// ```
57#[derive(Clone)]
58pub struct Requestor {
59    /// The HTTP client for making requests
60    client: Client,
61
62    /// Request timeout duration
63    timeout: Duration,
64}
65
66impl Requestor {
67    /// Creates a new requestor with a default timeout of 30 seconds.
68    ///
69    /// # Returns
70    ///
71    /// A new Requestor instance.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if the HTTP client cannot be created.
76    pub fn new() -> Result<Self, RequestorError> {
77        Self::with_timeout(30)
78    }
79
80    /// Creates a new requestor with a custom timeout in seconds.
81    ///
82    /// # Arguments
83    ///
84    /// * `timeout_secs` - The timeout duration in seconds
85    ///
86    /// # Returns
87    ///
88    /// A new Requestor instance with the specified timeout.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if the HTTP client cannot be created.
93    pub fn with_timeout(timeout_secs: u64) -> Result<Self, RequestorError> {
94        let client = Client::builder()
95            .timeout(Duration::from_secs(timeout_secs))
96            .build()?;
97
98        Ok(Requestor {
99            client,
100            timeout: Duration::from_secs(timeout_secs),
101        })
102    }
103
104    /// Makes a GET request to the specified URL with the provided user agent.
105    ///
106    /// This method makes a direct GET request without using a proxy.
107    ///
108    /// # Arguments
109    ///
110    /// * `url` - The URL to request
111    /// * `user_agent` - The User-Agent header value to use
112    ///
113    /// # Returns
114    ///
115    /// The response body as a String if successful.
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if:
120    /// * The request fails to send
121    /// * The response has a non-success status code
122    /// * The response body cannot be read as text
123    /// * The request times out
124    pub async fn get(&self, url: &str, user_agent: &str) -> RequestResult<String> {
125        let start_time = Instant::now();
126
127        let response = self
128            .client
129            .get(url)
130            .header(reqwest::header::USER_AGENT, user_agent)
131            .send()
132            .await?;
133
134        if start_time.elapsed() >= self.timeout {
135            return Err(RequestorError::Timeout(self.timeout.as_secs()));
136        }
137
138        let status = response.status();
139        if !status.is_success() {
140            return Err(RequestorError::StatusError(status, status.to_string()));
141        }
142
143        let body = response.text().await?;
144        Ok(body)
145    }
146
147    /// Makes a GET request using a proxy.
148    ///
149    /// This method creates a new client configured to use the specified proxy,
150    /// then makes a GET request through that proxy.
151    ///
152    /// # Arguments
153    ///
154    /// * `url` - The URL to request
155    /// * `user_agent` - The User-Agent header value to use
156    /// * `proxy` - The proxy to use for the request
157    ///
158    /// # Returns
159    ///
160    /// The response body as a String if successful.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if:
165    /// * The proxy configuration is invalid
166    /// * The request fails to send
167    /// * The response has a non-success status code
168    /// * The response body cannot be read as text
169    /// * The request times out
170    /// * There's a proxy connection error
171    pub async fn get_with_proxy(
172        &self,
173        url: &str,
174        user_agent: &str,
175        proxy: &Proxy,
176    ) -> RequestResult<String> {
177        // Build a client with the proxy configuration
178        let proxy_url = proxy.to_connection_string();
179        let mut proxy_builder = ReqwestProxy::all(&proxy_url)?;
180
181        // Add authentication if provided
182        if let (Some(username), Some(password)) = (&proxy.username, &proxy.password) {
183            proxy_builder = proxy_builder.basic_auth(username, password);
184        }
185
186        // Build a new client with the proxy
187        let client = Client::builder()
188            .proxy(proxy_builder)
189            .timeout(self.timeout)
190            .build()?;
191
192        let start_time = Instant::now();
193
194        let response = client
195            .get(url)
196            .header(reqwest::header::USER_AGENT, user_agent)
197            .send()
198            .await
199            .map_err(|e| {
200                if e.is_timeout() {
201                    RequestorError::Timeout(self.timeout.as_secs())
202                } else if e.is_connect() {
203                    RequestorError::ProxyError(e.to_string())
204                } else {
205                    RequestorError::RequestError(e)
206                }
207            })?;
208
209        if start_time.elapsed() >= self.timeout {
210            return Err(RequestorError::Timeout(self.timeout.as_secs()));
211        }
212
213        let status = response.status();
214        if !status.is_success() {
215            return Err(RequestorError::StatusError(status, status.to_string()));
216        }
217
218        let body = response.text().await?;
219        Ok(body)
220    }
221
222    /// Measures the latency to a URL in milliseconds.
223    ///
224    /// This method makes a lightweight HEAD request to the specified URL
225    /// and measures how long it takes to get a response.
226    ///
227    /// # Arguments
228    ///
229    /// * `url` - The URL to measure latency to
230    ///
231    /// # Returns
232    ///
233    /// The latency in milliseconds.
234    ///
235    /// # Errors
236    ///
237    /// Returns an error if the request fails to send or times out.
238    pub async fn measure_latency(&self, url: &str) -> RequestResult<u128> {
239        let start = Instant::now();
240
241        // Make a HEAD request to minimize data transfer
242        let _ = self.client.head(url).send().await?;
243
244        let elapsed = start.elapsed();
245        Ok(elapsed.as_millis())
246    }
247}