11use std:: sync:: LazyLock ;
22
3+ use ipnetwork:: IpNetwork ;
4+
35pub 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+
1623pub 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\n 127.0.0.1:9090\n invalid-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\t 10.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