Skip to main content

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(&regex_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(&params.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}