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}