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(®ex).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}