gooty_proxy/definitions/source.rs
1//! # Source Module
2//!
3//! This module defines types and functionality for managing proxy sources - locations
4//! from which proxy servers can be discovered and retrieved.
5//!
6//! ## Overview
7//!
8//! The module is centered around the `Source` struct, which represents an external
9//! source of proxy servers. It provides functionality for:
10//!
11//! - Defining sources with URLs and regex patterns for proxy extraction
12//! - Fetching and parsing proxy lists from various sources
13//! - Tracking source reliability and performance metrics
14//! - Managing source parameters for customized requests
15//!
16//! Sources typically represent web pages or APIs that provide lists of proxy servers,
17//! which can then be validated and used throughout the application.
18//!
19//! ## Examples
20//!
21//! ```
22//! use gooty_proxy::definitions::source::Source;
23//! use gooty_proxy::io::http::Requestor;
24//!
25//! #[tokio::main]
26//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
27//! // Create a source that extracts proxies in IP:PORT format
28//! let source = Source::new(
29//! "https://example.com/proxy-list".to_string(),
30//! "Mozilla/5.0 (compatible; Gooty/1.0)".to_string(),
31//! r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{2,5})".to_string(),
32//! )?;
33//!
34//! // Create an HTTP client and fetch proxies
35//! let requestor = Requestor::new()?;
36//! let proxies = source.fetch_proxies(&requestor).await?;
37//!
38//! println!("Found {} proxies", proxies.len());
39//! Ok(())
40//! }
41//! ```
42
43use crate::definitions::{
44 enums::{AnonymityLevel, ProxyType},
45 errors::{SourceError, SourceResult},
46 proxy::Proxy,
47};
48use crate::io::http::Requestor;
49use crate::utils::{self, SerializableRegex};
50use chrono::{DateTime, Utc};
51use serde::{Deserialize, Serialize};
52use std::collections::HashMap;
53use std::net::IpAddr;
54use std::str::FromStr;
55
56/// Represents a source of proxy servers.
57///
58/// A source defines where and how to obtain proxy server information, including
59/// the URL to fetch from, the user agent to use in requests, and the regex pattern
60/// for extracting proxy information from the response.
61///
62/// The struct also tracks usage statistics such as success rates and failure counts
63/// to help evaluate source reliability over time.
64///
65/// # Examples
66///
67/// ```
68/// use gooty_proxy::definitions::source::Source;
69///
70/// let source = Source::new(
71/// "https://example.com/proxy-list".to_string(),
72/// "Mozilla/5.0 (compatible; Gooty/1.0)".to_string(),
73/// r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{2,5})".to_string(),
74/// ).unwrap();
75///
76/// assert_eq!(source.url, "https://example.com/proxy-list");
77/// assert_eq!(source.success_rate(), 0.0); // New source with no usage yet
78/// ```
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct Source {
81 /// The URL of the proxy source.
82 pub url: String,
83
84 /// The User-Agent string to use when making requests to the source.
85 pub user_agent: String,
86
87 /// The regex pattern to use for extracting proxy information from the source.
88 pub regex_pattern: String,
89
90 /// Compiled regex object for performance
91 #[serde(skip)]
92 pub compiled_regex: Option<SerializableRegex>,
93
94 /// When the source was last used
95 pub last_used_at: Option<DateTime<Utc>>,
96
97 /// Number of times the source has been used
98 pub use_count: usize,
99
100 /// Number of times the source has failed
101 pub failure_count: usize,
102
103 /// Last failure reason
104 pub last_failure_reason: Option<String>,
105
106 /// Last failure HTTP status code if applicable
107 pub last_failure_code: Option<u16>,
108
109 /// Additional parameters for the source
110 pub parameters: HashMap<String, String>,
111
112 /// Number of proxies found from this source
113 pub proxies_found: usize,
114}
115
116impl Source {
117 /// Creates a new proxy source with the required fields.
118 ///
119 /// This constructor validates both the URL and regex pattern to ensure
120 /// they're well-formed before creating the source instance.
121 ///
122 /// # Arguments
123 ///
124 /// * `url` - The URL where proxy information can be obtained
125 /// * `user_agent` - The User-Agent string to use in HTTP requests
126 /// * `regex_pattern` - A regular expression pattern that extracts proxy data from responses
127 ///
128 /// # Returns
129 ///
130 /// A new `Source` instance if validation succeeds
131 ///
132 /// # Errors
133 ///
134 /// This function will return an error if:
135 /// * The URL is malformed or invalid
136 /// * The regex pattern is invalid or cannot be compiled
137 pub fn new(
138 url: String,
139 user_agent: String,
140 regex_pattern: String,
141 ) -> Result<Self, SourceError> {
142 // Validate the URL
143 if !utils::is_valid_url(&url) {
144 return Err(SourceError::InvalidUrl(url));
145 }
146
147 // Validate and compile the regex
148 let compiled_regex = match utils::SerializableRegex::new(®ex_pattern) {
149 Ok(regex) => Some(regex),
150 Err(err) => return Err(SourceError::InvalidRegexPattern(err.to_string())),
151 };
152
153 Ok(Source {
154 url,
155 user_agent,
156 regex_pattern,
157 compiled_regex,
158 last_used_at: None,
159 use_count: 0,
160 failure_count: 0,
161 last_failure_reason: None,
162 last_failure_code: None,
163 parameters: HashMap::new(),
164 proxies_found: 0,
165 })
166 }
167
168 /// Adds a parameter to the source configuration.
169 ///
170 /// Parameters will be appended to the source URL as query parameters
171 /// when making HTTP requests.
172 ///
173 /// # Arguments
174 ///
175 /// * `key` - The parameter name
176 /// * `value` - The parameter value
177 ///
178 /// # Examples
179 ///
180 /// ```
181 /// # use gooty_proxy::definitions::source::Source;
182 /// # let mut source = Source::new(
183 /// # "https://example.com/proxies".to_string(),
184 /// # "Mozilla/5.0".to_string(),
185 /// # r"(\d+\.\d+\.\d+\.\d+:\d+)".to_string()
186 /// # ).unwrap();
187 /// source.add_parameter("country".to_string(), "US".to_string());
188 /// source.add_parameter("type".to_string(), "https".to_string());
189 ///
190 /// let url = source.get_full_url();
191 /// assert!(url.contains("country=US"));
192 /// assert!(url.contains("type=https"));
193 /// ```
194 pub fn add_parameter(&mut self, key: String, value: String) {
195 self.parameters.insert(key, value);
196 }
197
198 /// Removes a parameter from the source configuration.
199 ///
200 /// # Arguments
201 ///
202 /// * `key` - The name of the parameter to remove
203 ///
204 /// # Returns
205 ///
206 /// The previous value of the parameter if it was set, or `None` if it wasn't present
207 ///
208 /// # Examples
209 ///
210 /// ```
211 /// # use gooty_proxy::definitions::source::Source;
212 /// # let mut source = Source::new(
213 /// # "https://example.com/proxies".to_string(),
214 /// # "Mozilla/5.0".to_string(),
215 /// # r"(\d+\.\d+\.\d+\.\d+:\d+)".to_string()
216 /// # ).unwrap();
217 /// source.add_parameter("country".to_string(), "US".to_string());
218 ///
219 /// let value = source.remove_parameter("country");
220 /// assert_eq!(value, Some("US".to_string()));
221 /// ```
222 pub fn remove_parameter(&mut self, key: &str) -> Option<String> {
223 self.parameters.remove(key)
224 }
225
226 /// Records a successful use of the source.
227 ///
228 /// This method updates usage statistics by incrementing the use count
229 /// and recording the current time as the last used timestamp.
230 pub fn record_use(&mut self) {
231 self.last_used_at = Some(Utc::now());
232 self.use_count += 1;
233 }
234
235 /// Records a failure when using the source.
236 ///
237 /// This method updates failure statistics and records the reason
238 /// and optional status code for the failure.
239 ///
240 /// # Arguments
241 ///
242 /// * `reason` - A description of why the source failed
243 /// * `status_code` - Optional HTTP status code if the failure was related to an HTTP response
244 pub fn record_failure(&mut self, reason: String, status_code: Option<u16>) {
245 self.failure_count += 1;
246 self.last_failure_reason = Some(reason);
247 self.last_failure_code = status_code;
248 }
249
250 /// Returns the success rate of using this source.
251 ///
252 /// The success rate is calculated as the ratio of successful uses
253 /// to total uses. If the source has never been used, returns 0.0.
254 ///
255 /// # Returns
256 ///
257 /// A float between 0.0 and 1.0 representing the success rate
258 /// where 1.0 means 100% success.
259 #[must_use]
260 pub fn success_rate(&self) -> usize {
261 if self.use_count == 0 {
262 return 0;
263 }
264
265 let success_count = self.use_count - self.failure_count;
266 100 * success_count / self.use_count
267 }
268
269 /// Updates the regex pattern and recompiles it.
270 ///
271 /// This is useful when the pattern needs to be adjusted based on
272 /// changes to the source format.
273 ///
274 /// # Arguments
275 ///
276 /// * `new_pattern` - The new regex pattern to use
277 ///
278 /// # Returns
279 ///
280 /// `Ok(())` if the pattern was valid and updated successfully
281 ///
282 /// # Errors
283 ///
284 /// Returns an error if the new regex pattern is invalid
285 pub fn update_regex_pattern(&mut self, new_pattern: String) -> Result<(), SourceError> {
286 match utils::SerializableRegex::new(&new_pattern) {
287 Ok(regex) => {
288 self.regex_pattern = new_pattern;
289 self.compiled_regex = Some(regex);
290 Ok(())
291 }
292 Err(err) => Err(SourceError::InvalidRegexPattern(err.to_string())),
293 }
294 }
295
296 /// Validates the source configuration.
297 ///
298 /// This method checks that the URL is well-formed and the
299 /// regex pattern is valid.
300 ///
301 /// # Returns
302 ///
303 /// `Ok(())` if the source is valid
304 ///
305 /// # Errors
306 ///
307 /// Returns an error if:
308 /// * The URL is invalid
309 /// * The regex pattern is invalid
310 pub fn validate(&self) -> Result<(), SourceError> {
311 // Validate URL
312 if !utils::is_valid_url(&self.url) {
313 return Err(SourceError::InvalidUrl(self.url.clone()));
314 }
315
316 // Validate regex by compiling it
317 match utils::SerializableRegex::new(&self.regex_pattern) {
318 Ok(_) => Ok(()),
319 Err(err) => Err(SourceError::InvalidRegexPattern(err.to_string())),
320 }
321 }
322
323 /// Returns a constructed URL with parameters.
324 ///
325 /// This method takes the base URL and appends any parameters
326 /// that have been added to the source as query parameters.
327 ///
328 /// # Returns
329 ///
330 /// The complete URL including query parameters
331 ///
332 /// # Examples
333 ///
334 /// ```
335 /// # use gooty_proxy::definitions::source::Source;
336 /// # let mut source = Source::new(
337 /// # "https://example.com/api".to_string(),
338 /// # "Mozilla/5.0".to_string(),
339 /// # r"(\d+\.\d+\.\d+\.\d+:\d+)".to_string()
340 /// # ).unwrap();
341 /// source.add_parameter("token".to_string(), "abc123".to_string());
342 ///
343 /// let url = source.get_full_url();
344 /// assert_eq!(url, "https://example.com/api?token=abc123");
345 /// ```
346 #[must_use]
347 pub fn get_full_url(&self) -> String {
348 if self.parameters.is_empty() {
349 return self.url.clone();
350 }
351
352 let mut url = self.url.clone();
353 let params: Vec<String> = self
354 .parameters
355 .iter()
356 .map(|(k, v)| format!("{k}={v}"))
357 .collect();
358
359 if url.contains('?') {
360 url.push('&');
361 } else {
362 url.push('?');
363 }
364
365 url.push_str(¶ms.join("&"));
366 url
367 }
368
369 /// Fetches proxies from this source.
370 ///
371 /// Makes an HTTP request to the source URL and extracts proxies from
372 /// the response using the defined regex pattern.
373 ///
374 /// # Arguments
375 ///
376 /// * `requestor` - The HTTP client to use for making requests
377 ///
378 /// # Returns
379 ///
380 /// A vector of `Proxy` objects extracted from the source
381 ///
382 /// # Errors
383 ///
384 /// This function will return an error if:
385 /// * The HTTP request fails
386 /// * The regex pattern isn't compiled properly
387 /// * The response can't be parsed
388 pub async fn fetch_proxies(&self, requestor: &Requestor) -> SourceResult<Vec<Proxy>> {
389 let url = self.get_full_url();
390
391 // Make the HTTP request
392 let response = requestor
393 .get(&url, &self.user_agent)
394 .await
395 .map_err(|e| SourceError::FetchFailure(e.to_string()))?;
396
397 // Extract proxies using regex
398 let Some(regex) = &self.compiled_regex else {
399 return Err(SourceError::InvalidRegexPattern(
400 "Regex not compiled".to_string(),
401 ));
402 };
403
404 // Parse proxies from the response
405 let mut proxies = Vec::new();
406
407 // Use the SerializableRegex's find_iter method
408 let matches_iterator = regex.find_iter(&response);
409
410 for match_result in matches_iterator {
411 // Each match is a Result that needs to be handled
412 match match_result {
413 Ok(m) => {
414 let proxy_str = m.as_str();
415
416 // Try to parse the proxy string
417 if let Some(proxy) = Self::parse_proxy(proxy_str) {
418 proxies.push(proxy);
419 }
420 }
421 Err(e) => {
422 return Err(SourceError::ParseError(e.to_string()));
423 }
424 }
425 }
426
427 Ok(proxies)
428 }
429
430 /// Fetches proxies and returns both the proxies and raw response.
431 ///
432 /// Similar to `fetch_proxies` but also returns the raw response text,
433 /// which can be useful for debugging or further processing.
434 ///
435 /// # Arguments
436 ///
437 /// * `requestor` - The HTTP client to use for making requests
438 ///
439 /// # Returns
440 ///
441 /// A tuple containing:
442 /// * A vector of `Proxy` objects extracted from the source
443 /// * The raw response text
444 ///
445 /// # Errors
446 ///
447 /// This function will return an error if:
448 /// * The HTTP request fails
449 /// * The regex pattern isn't compiled properly
450 /// * The response can't be parsed
451 pub async fn fetch_proxies_with_response(
452 &self,
453 requestor: &Requestor,
454 ) -> SourceResult<(Vec<Proxy>, String)> {
455 let url = self.get_full_url();
456
457 // Make the HTTP request
458 let response = requestor
459 .get(&url, &self.user_agent)
460 .await
461 .map_err(|e| SourceError::FetchFailure(e.to_string()))?;
462
463 // Extract proxies using regex
464 let Some(regex) = &self.compiled_regex else {
465 return Err(SourceError::InvalidRegexPattern(
466 "Regex not compiled".to_string(),
467 ));
468 };
469
470 // Parse proxies from the response
471 let mut proxies = Vec::new();
472
473 let matches_iterator = regex.find_iter(&response);
474
475 for match_result in matches_iterator {
476 match match_result {
477 Ok(m) => {
478 let proxy_str = m.as_str();
479 if let Some(proxy) = Self::parse_proxy(proxy_str) {
480 proxies.push(proxy);
481 }
482 }
483 Err(e) => {
484 return Err(SourceError::ParseError(e.to_string()));
485 }
486 }
487 }
488
489 Ok((proxies, response))
490 }
491
492 /// Parse a proxy from a string match.
493 ///
494 /// Attempts to parse a string like "127.0.0.1:8080" into a Proxy object.
495 /// Currently handles only the simple IP:PORT format.
496 ///
497 /// # Arguments
498 ///
499 /// * `proxy_str` - String containing proxy information, expected in IP:PORT format
500 ///
501 /// # Returns
502 ///
503 /// Some(Proxy) if parsing succeeds, None otherwise
504 fn parse_proxy(proxy_str: &str) -> Option<Proxy> {
505 // Simple IP:PORT parsing
506 if let Some((ip_str, port_str)) = proxy_str.split_once(':') {
507 if let (Ok(ip), Ok(port)) = (IpAddr::from_str(ip_str), port_str.parse::<u16>()) {
508 // Default to HTTP proxy type if not specified
509 return Some(Proxy::new(
510 ProxyType::Http,
511 ip,
512 port,
513 AnonymityLevel::Anonymous, // Default anonymity level, will be checked later
514 ));
515 }
516 }
517
518 None
519 }
520}
521
522/// Functions for serialization and deserialization
523impl Source {
524 /// Serializes the source to a JSON string.
525 ///
526 /// # Returns
527 ///
528 /// A JSON string representation of the Source if successful
529 ///
530 /// # Errors
531 ///
532 /// Returns a `serde_json::Error` if serialization fails
533 pub fn to_json(&self) -> Result<String, serde_json::Error> {
534 serde_json::to_string(self)
535 }
536
537 /// Deserializes a source from a JSON string.
538 ///
539 /// This method also recompiles the regex pattern after deserialization.
540 ///
541 /// # Arguments
542 ///
543 /// * `json` - A JSON string representation of a Source
544 ///
545 /// # Returns
546 ///
547 /// A Source object if deserialization succeeds
548 ///
549 /// # Errors
550 ///
551 /// Returns a `serde_json::Error` if deserialization fails
552 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
553 let mut source: Source = serde_json::from_str(json)?;
554
555 // Recompile the regex after deserialization
556 if let Ok(regex) = utils::SerializableRegex::new(&source.regex_pattern) {
557 source.compiled_regex = Some(regex);
558 }
559
560 Ok(source)
561 }
562}