Skip to content

Commit bdecf19

Browse files
Add CIDR range scanning support for proxy discovery
1 parent c81a969 commit bdecf19

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

src/parsers.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::sync::LazyLock;
22

3+
use ipnetwork::IpNetwork;
4+
35
pub static PROXY_REGEX: LazyLock<fancy_regex::Regex> = LazyLock::new(|| {
46
let pattern = r"(?:^|[^0-9A-Za-z])(?:(?P<protocol>https?|socks[45]):\/\/)?(?:(?P<username>[0-9A-Za-z]{1,64}):(?P<password>[0-9A-Za-z]{1,64})@)?(?P<host>[A-Za-z][\-\.A-Za-z]{0,251}[A-Za-z]|[A-Za-z]|(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(?:\.(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])){3}):(?P<port>[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?=[^0-9A-Za-z]|$)";
57
fancy_regex::RegexBuilder::new(pattern)
@@ -13,10 +15,189 @@ static IPV4_REGEX: LazyLock<fancy_regex::Regex> = LazyLock::new(|| {
1315
fancy_regex::Regex::new(pattern).unwrap()
1416
});
1517

18+
static CIDR_REGEX: LazyLock<fancy_regex::Regex> = LazyLock::new(|| {
19+
let pattern = r"(?:^|[^0-9A-Za-z])(?P<network>(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(?:\.(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])){3})/(?P<prefix>[0-9]|[12][0-9]|3[0-2]):(?P<port>[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?=[^0-9A-Za-z]|$)";
20+
fancy_regex::Regex::new(pattern).unwrap()
21+
});
22+
1623
pub fn parse_ipv4(s: &str) -> Option<String> {
1724
if let Ok(Some(captures)) = IPV4_REGEX.captures(s) {
1825
captures.name("host").map(|capture| capture.as_str().to_owned())
1926
} else {
2027
None
2128
}
2229
}
30+
31+
/// Expands CIDR ranges in text into individual IP:port entries
32+
/// Supports format like "192.168.1.0/24:8080" which expands to all IPs in the range
33+
/// Handles various separators (spaces, commas, newlines, etc.) between entries
34+
pub fn expand_cidr_ranges(text: &str) -> String {
35+
let mut result = text.to_string();
36+
let mut offset: i32 = 0;
37+
38+
// Find all CIDR matches and expand them
39+
let captures: Vec<_> = CIDR_REGEX.captures_iter(text)
40+
.filter_map(|m| m.ok())
41+
.collect();
42+
43+
for capture in captures {
44+
if let (Some(network), Some(prefix), Some(port)) = (
45+
capture.name("network"),
46+
capture.name("prefix"),
47+
capture.name("port")
48+
) {
49+
let cidr_str = format!("{}/{}", network.as_str(), prefix.as_str());
50+
51+
match cidr_str.parse::<IpNetwork>() {
52+
Ok(network) => {
53+
// Generate expanded IPs
54+
let expanded_ips: Vec<String> = network.iter()
55+
.filter(|ip| ip.is_ipv4())
56+
.map(|ip| format!("{}:{}", ip, port.as_str()))
57+
.collect();
58+
59+
if !expanded_ips.is_empty() {
60+
// Get the full match including any leading non-alphanumeric character
61+
let full_match = capture.get(0).unwrap();
62+
let match_start = (full_match.start() as i32 + offset) as usize;
63+
let match_end = (full_match.end() as i32 + offset) as usize;
64+
65+
// Determine what separator to use by checking what follows
66+
let separator = if match_end < result.len() {
67+
let next_char = result.chars().nth(match_end);
68+
match next_char {
69+
Some('\n') => "\n",
70+
Some('\t') => "\t",
71+
Some(',') => ",",
72+
_ => " ",
73+
}
74+
} else {
75+
"\n"
76+
};
77+
78+
// Join expanded IPs with the detected separator
79+
let replacement = expanded_ips.join(separator);
80+
81+
// Handle case where match starts with a delimiter character
82+
let (_actual_start, prefix_char) = if match_start > 0 {
83+
let prev_char = result.chars().nth(match_start);
84+
if prev_char.map_or(false, |c| !c.is_ascii_alphanumeric()) {
85+
(match_start + 1, result.chars().nth(match_start).unwrap().to_string())
86+
} else {
87+
(match_start, String::new())
88+
}
89+
} else {
90+
(match_start, String::new())
91+
};
92+
93+
let final_replacement = format!("{}{}", prefix_char, replacement);
94+
95+
// Replace the CIDR pattern with expanded IPs
96+
result.replace_range(match_start..match_end, &final_replacement);
97+
98+
// Update offset for subsequent replacements
99+
let len_diff = final_replacement.len() as i32 - (match_end - match_start) as i32;
100+
offset += len_diff;
101+
}
102+
}
103+
Err(_) => {
104+
// If parsing fails, leave the original text unchanged
105+
continue;
106+
}
107+
}
108+
}
109+
}
110+
111+
result
112+
}
113+
114+
#[cfg(test)]
115+
mod tests {
116+
use super::*;
117+
118+
#[test]
119+
fn test_cidr_expansion() {
120+
// Test basic CIDR expansion
121+
let input = "192.168.1.0/30:8080";
122+
let result = expand_cidr_ranges(input);
123+
let lines: Vec<&str> = result.trim().split('\n').collect();
124+
125+
assert_eq!(lines.len(), 4);
126+
assert!(lines.contains(&"192.168.1.0:8080"));
127+
assert!(lines.contains(&"192.168.1.1:8080"));
128+
assert!(lines.contains(&"192.168.1.2:8080"));
129+
assert!(lines.contains(&"192.168.1.3:8080"));
130+
}
131+
132+
#[test]
133+
fn test_mixed_input() {
134+
let input = "192.168.1.0/31:8080\n127.0.0.1:9090\ninvalid-line";
135+
let result = expand_cidr_ranges(input);
136+
let lines: Vec<&str> = result.trim().split('\n').collect();
137+
138+
// Should have 2 CIDR-expanded IPs + 1 regular IP + 1 invalid line
139+
assert_eq!(lines.len(), 4);
140+
assert!(lines.contains(&"192.168.1.0:8080"));
141+
assert!(lines.contains(&"192.168.1.1:8080"));
142+
assert!(lines.contains(&"127.0.0.1:9090"));
143+
assert!(lines.contains(&"invalid-line"));
144+
}
145+
146+
#[test]
147+
fn test_single_ip_cidr() {
148+
let input = "10.0.0.1/32:3128";
149+
let result = expand_cidr_ranges(input);
150+
assert_eq!(result.trim(), "10.0.0.1:3128");
151+
}
152+
153+
#[test]
154+
fn test_non_newline_separated_behavior() {
155+
// Test space-separated entries with CIDR expansion
156+
let input = "192.168.1.0/31:8080 127.0.0.1:9090";
157+
let result = expand_cidr_ranges(input);
158+
159+
// Should expand the CIDR range and preserve the regular proxy
160+
assert!(result.contains("192.168.1.0:8080"));
161+
assert!(result.contains("192.168.1.1:8080"));
162+
assert!(result.contains("127.0.0.1:9090"));
163+
}
164+
165+
#[test]
166+
fn test_multiple_cidr_same_line_behavior() {
167+
// Test multiple CIDR ranges on same line
168+
let input = "192.168.1.0/31:8080 10.0.0.0/31:3128";
169+
let result = expand_cidr_ranges(input);
170+
171+
// Should expand both CIDR ranges
172+
assert!(result.contains("192.168.1.0:8080"));
173+
assert!(result.contains("192.168.1.1:8080"));
174+
assert!(result.contains("10.0.0.0:3128"));
175+
assert!(result.contains("10.0.0.1:3128"));
176+
}
177+
178+
#[test]
179+
fn test_comma_separated_cidr() {
180+
let input = "192.168.1.0/31:8080,10.0.0.0/31:3128";
181+
let result = expand_cidr_ranges(input);
182+
183+
// Should expand both CIDR ranges and preserve comma separation
184+
assert!(result.contains("192.168.1.0:8080"));
185+
assert!(result.contains("192.168.1.1:8080"));
186+
assert!(result.contains("10.0.0.0:3128"));
187+
assert!(result.contains("10.0.0.1:3128"));
188+
}
189+
190+
#[test]
191+
fn test_mixed_separators() {
192+
let input = "192.168.1.0/31:8080\t10.0.0.1:3128,203.0.113.0/31:1080 127.0.0.1:9090";
193+
let result = expand_cidr_ranges(input);
194+
195+
// Should expand CIDR ranges and preserve non-CIDR entries
196+
assert!(result.contains("192.168.1.0:8080"));
197+
assert!(result.contains("192.168.1.1:8080"));
198+
assert!(result.contains("10.0.0.1:3128"));
199+
assert!(result.contains("203.0.113.0:1080"));
200+
assert!(result.contains("203.0.113.1:1080"));
201+
assert!(result.contains("127.0.0.1:9090"));
202+
}
203+
}

0 commit comments

Comments
 (0)