Skip to main content

gooty_proxy/
utils.rs

1//! # Utility Module
2//!
3//! This module provides common utility functions used throughout the gooty proxy system.
4//! It includes validation functions, string manipulation, and other helpers.
5//!
6//! ## Components
7//!
8//! * **URL utilities** - Functions for validating and working with URLs
9//! * **Regex utilities** - Functions for validating and working with regular expressions
10//! * **Random generators** - Functions for generating random values
11//!
12//! ## Examples
13//!
14//! ```
15//! use gooty_proxy::utils;
16//!
17//! let url = "https://example.com";
18//! assert!(utils::is_valid_url(url));
19//!
20//! let regex_pattern = r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):\d+";
21//! assert!(utils::validate_regex(regex_pattern).is_ok());
22//! ```
23
24use crate::definitions::{
25    defaults,
26    errors::{UtilError, UtilResult},
27};
28use fancy_regex::Regex;
29use rand::prelude::*;
30use serde::{self};
31use std::fmt;
32use std::hash::{Hash, Hasher};
33use std::str::FromStr;
34use url::Url;
35
36/// A wrapper type for `fancy_regex::Regex` that implements Serialize, Deserialize, `PartialEq`, Eq
37///
38/// This wrapper allows storing and serializing regular expressions by storing
39/// the pattern string alongside the compiled regex object.
40///
41/// # Examples
42///
43/// ```
44/// use gooty_proxy::utils::SerializableRegex;
45///
46/// let regex = SerializableRegex::new(r"\d{3}").unwrap();
47/// assert!(regex.is_match("123").unwrap());
48///
49/// // Serialize and deserialize with serde
50/// let serialized = serde_json::to_string(&regex).unwrap();
51/// let deserialized: SerializableRegex = serde_json::from_str(&serialized).unwrap();
52///
53/// assert_eq!(regex, deserialized);
54/// ```
55#[derive(Clone, Debug, serde::Serialize)]
56pub struct SerializableRegex {
57    /// The pattern string used to create the regex
58    pattern: String,
59
60    /// The compiled regex object
61    #[serde(skip_serializing, skip_deserializing)]
62    regex: Regex,
63}
64
65impl SerializableRegex {
66    /// Creates a new `SerializableRegex` from a pattern string
67    ///
68    /// # Arguments
69    ///
70    /// * `pattern` - The regex pattern to compile
71    ///
72    /// # Returns
73    ///
74    /// A Result containing the `SerializableRegex` if valid
75    ///
76    /// # Errors
77    ///
78    /// Returns a `UtilError::InvalidRegex` if the pattern is invalid
79    pub fn new(pattern: &str) -> UtilResult<Self> {
80        let regex = validate_regex(pattern)?;
81        Ok(SerializableRegex {
82            pattern: pattern.to_string(),
83            regex,
84        })
85    }
86
87    /// Gets the underlying regex pattern
88    ///
89    /// # Returns
90    ///
91    /// The pattern string used to create this regex
92    #[must_use]
93    pub fn pattern(&self) -> &str {
94        &self.pattern
95    }
96
97    /// Gets a reference to the compiled regex
98    ///
99    /// # Returns
100    ///
101    /// A reference to the compiled Regex object
102    #[must_use]
103    pub fn regex(&self) -> &Regex {
104        &self.regex
105    }
106
107    /// Checks if the given text matches this regex
108    ///
109    /// # Arguments
110    ///
111    /// * `text` - The text to check
112    ///
113    /// # Returns
114    ///
115    /// A Result containing a boolean indicating whether the pattern matches
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if the regex engine encounters an error during matching
120    pub fn is_match(&self, text: &str) -> Result<bool, Box<fancy_regex::Error>> {
121        self.regex.is_match(text).map_err(Box::new)
122    }
123
124    /// Finds all matches in the given text
125    ///
126    /// # Arguments
127    ///
128    /// * `text` - The text to search
129    ///
130    /// # Returns
131    ///
132    /// An iterator over all matches in the text
133    #[must_use]
134    pub fn find_iter<'r, 't>(&'r self, text: &'t str) -> fancy_regex::Matches<'r, 't> {
135        self.regex.find_iter(text)
136    }
137}
138
139impl PartialEq for SerializableRegex {
140    fn eq(&self, other: &Self) -> bool {
141        self.pattern == other.pattern
142    }
143}
144
145impl Eq for SerializableRegex {}
146
147impl Hash for SerializableRegex {
148    fn hash<H: Hasher>(&self, state: &mut H) {
149        self.pattern.hash(state);
150    }
151}
152
153impl fmt::Display for SerializableRegex {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        write!(f, "{}", self.pattern)
156    }
157}
158
159impl FromStr for SerializableRegex {
160    type Err = UtilError;
161
162    fn from_str(s: &str) -> Result<Self, Self::Err> {
163        SerializableRegex::new(s)
164    }
165}
166
167/// Validates whether a given string is a valid URL
168///
169/// # Arguments
170///
171/// * `url` - The URL string to validate
172///
173/// # Returns
174///
175/// `true` if the URL is valid, `false` otherwise
176#[must_use]
177pub fn is_valid_url(url: &str) -> bool {
178    match Url::parse(url) {
179        Ok(parsed) => parsed.scheme() == "http" || parsed.scheme() == "https",
180        Err(_) => false,
181    }
182}
183
184/// Validates and compiles a regex pattern
185///
186/// # Arguments
187///
188/// * `pattern` - The regex pattern to validate
189///
190/// # Returns
191///
192/// A compiled Regex if valid, or an error if the pattern is invalid
193///
194/// # Errors
195///
196/// Returns a `UtilError::InvalidRegex` if the pattern is invalid or cannot be compiled
197pub fn validate_regex(pattern: &str) -> UtilResult<Regex> {
198    match Regex::new(pattern) {
199        Ok(regex) => Ok(regex),
200        Err(e) => Err(UtilError::InvalidRegex(e.to_string())),
201    }
202}
203
204/// Returns a random User-Agent string from the default list
205///
206/// # Returns
207///
208/// A random User-Agent string
209#[must_use]
210pub fn get_random_user_agent() -> &'static str {
211    let mut rng = rand::rng();
212    defaults::DEFAULT_USER_AGENTS
213        .choose(&mut rng)
214        .unwrap_or(&"Mozilla/5.0 (compatible; Gooty-Proxy/0.1)")
215}
216
217/// Sanitizes a URL to be used as part of a filename
218///
219/// Removes protocol, replaces special characters, and shortens if necessary
220///
221/// # Arguments
222///
223/// * `url` - The URL to sanitize
224///
225/// # Returns
226///
227/// A string that can be safely used as part of a filename
228#[must_use]
229pub fn sanitize_url_for_filename(url: &str) -> String {
230    // Parse the URL to extract just the host and path
231    let Ok(parsed) = Url::parse(url) else {
232        return "invalid-url".to_string();
233    };
234
235    // Get just the hostname
236    let hostname = parsed.host_str().unwrap_or("unknown");
237
238    // Replace special characters with hyphens
239    let sanitized = hostname.replace(['.', '/', ':', '?', '&', '=', ' '], "-");
240
241    // Limit the length to avoid excessively long filenames
242    if sanitized.len() > 50 {
243        sanitized[0..50].to_string()
244    } else {
245        sanitized
246    }
247}
248
249/// Checks if a string is a valid IPv4 or IPv6 address
250///
251/// # Arguments
252///
253/// * `ip_str` - The IP address string to validate
254///
255/// # Returns
256///
257/// `true` if the string is a valid IP address, `false` otherwise
258#[must_use]
259pub fn is_valid_ip(ip_str: &str) -> bool {
260    ip_str.parse::<std::net::IpAddr>().is_ok()
261}
262
263/// Checks if a number is a valid port number (1-65535)
264///
265/// # Arguments
266///
267/// * `port` - The port number to validate
268///
269/// # Returns
270///
271/// `true` if the port number is valid, `false` otherwise
272#[must_use]
273pub fn is_valid_port(port: u16) -> bool {
274    port > 0
275}