Skip to main content

aria2_core/http/
socks_connector.rs

1use std::io::{Read, Write};
2use std::net::{IpAddr, SocketAddr};
3
4pub trait SocksConnector: Send + Sync {
5    fn connect<S: Read + Write>(&self, stream: S, target: &SocketAddr) -> Result<S, String>;
6}
7
8/// Represents a parsed proxy URL (e.g., socks5://user:pass@host:port)
9#[derive(Debug)]
10pub struct ProxyUrl {
11    pub protocol: ProxyProtocol,
12    pub host: String,
13    pub port: u16,
14    pub username: Option<String>,
15    pub password: Option<String>,
16}
17
18#[derive(Debug, Clone, PartialEq)]
19pub enum ProxyProtocol {
20    Socks4,
21    Socks5,
22    Http,
23    Https,
24}
25
26impl ProxyUrl {
27    /// Parse a proxy URL string into a ProxyUrl struct.
28    /// Supported formats:
29    ///   socks5://[username:password@]host[:port]
30    ///   socks4://[username@]host[:port]
31    ///   http://[username:password@]host[:port]
32    ///   https://[username:password@]host[:port]
33    pub fn parse(url: &str) -> Result<Self, String> {
34        let url = url.trim();
35
36        // Determine protocol
37        let (protocol, rest) = if let Some(rest) = url.strip_prefix("socks5://") {
38            (ProxyProtocol::Socks5, rest)
39        } else if let Some(rest) = url.strip_prefix("socks4://") {
40            (ProxyProtocol::Socks4, rest)
41        } else if let Some(rest) = url.strip_prefix("http://") {
42            (ProxyProtocol::Http, rest)
43        } else if let Some(rest) = url.strip_prefix("https://") {
44            (ProxyProtocol::Https, rest)
45        } else {
46            return Err(format!("Unsupported proxy protocol in URL: {}", url));
47        };
48
49        // Split auth info from host:port
50        let (auth_part, host_port) = match rest.find('@') {
51            Some(idx) => (&rest[..idx], &rest[idx + 1..]),
52            None => ("", rest),
53        };
54
55        // Parse username and password
56        let (username, password) = if !auth_part.is_empty() {
57            if let Some(colon_idx) = auth_part.find(':') {
58                (
59                    Some(auth_part[..colon_idx].to_string()),
60                    Some(auth_part[colon_idx + 1..].to_string()),
61                )
62            } else {
63                // SOCKS4 only uses username, no password
64                (Some(auth_part.to_string()), None)
65            }
66        } else {
67            (None, None)
68        };
69
70        // Parse host and port
71        let (host, port) = if let Some(colon_idx) = host_port.rfind(':') {
72            let h = &host_port[..colon_idx];
73            let p_str = &host_port[colon_idx + 1..];
74            let port: u16 = p_str
75                .parse()
76                .map_err(|_| format!("Invalid port number in proxy URL: {}", p_str))?;
77            (h.to_string(), port)
78        } else {
79            // Use default port based on protocol
80            let default_port = match protocol {
81                ProxyProtocol::Socks4 | ProxyProtocol::Socks5 => 1080u16,
82                ProxyProtocol::Http => 8080u16,
83                ProxyProtocol::Https => 443u16,
84            };
85            (host_port.to_string(), default_port)
86        };
87
88        if host.is_empty() {
89            return Err("Host is empty in proxy URL".to_string());
90        }
91
92        Ok(Self {
93            protocol,
94            host,
95            port,
96            username,
97            password,
98        })
99    }
100
101    /// Create the appropriate connector for this proxy URL
102    pub fn create_connector(&self) -> SocksConnectorEnum {
103        match self.protocol {
104            ProxyProtocol::Socks4 => SocksConnectorEnum::Socks4(Socks4Connector::new(
105                self.username.clone().unwrap_or_default(),
106            )),
107            ProxyProtocol::Socks5 => SocksConnectorEnum::Socks5(Socks5Connector::new(
108                self.username.clone(),
109                self.password.clone(),
110            )),
111            _ => panic!("HTTP/HTTPS connectors not yet implemented"),
112        }
113    }
114}
115
116/// Enum holding a concrete SOCKS4 or SOCKS5 connector (needed because SocksConnector trait has generic methods)
117pub enum SocksConnectorEnum {
118    Socks4(Socks4Connector),
119    Socks5(Socks5Connector),
120}
121
122impl SocksConnector for SocksConnectorEnum {
123    fn connect<S: Read + Write>(&self, stream: S, target: &SocketAddr) -> Result<S, String> {
124        match self {
125            Self::Socks4(c) => c.connect(stream, target),
126            Self::Socks5(c) => c.connect(stream, target),
127        }
128    }
129}
130
131/// Matcher for NO_PROXY / no_proxy environment variable patterns
132/// Supports patterns like:
133///   - Exact domain matches: "example.com"
134///   - Wildcard subdomain matches: ".example.com" (matches *.example.com)
135///   - IP address exact matches: "192.168.1.1"
136///   - IP/CIDR notation: "192.168.0.0/16"
137///   - Special token "*": matches everything (bypass all proxies)
138pub struct NoProxyMatcher {
139    entries: Vec<NoProxyEntry>,
140    match_all: bool,
141}
142
143enum NoProxyEntry {
144    Domain(String),        // Exact domain or .domain for wildcard subdomains
145    IpAddr(IpAddr),        // Exact IP address
146    IpNetwork(IpAddr, u8), // IP with prefix length (CIDR)
147}
148
149impl NoProxyMatcher {
150    /// Create a new NoProxyMatcher from the value of NO_PROXY/no_proxy env var.
151    /// The input is typically comma-separated list of patterns.
152    pub fn from_env_value(value: &str) -> Self {
153        let mut entries = Vec::new();
154        let mut match_all = false;
155
156        for pattern in value.split(',') {
157            let pattern = pattern.trim();
158            if pattern.is_empty() {
159                continue;
160            }
161
162            if pattern == "*" {
163                match_all = true;
164                continue;
165            }
166
167            // Check for CIDR notation
168            if let Some(slash_pos) = pattern.rfind('/') {
169                let addr_str = &pattern[..slash_pos];
170                let prefix_str = &pattern[slash_pos + 1..];
171                if let Ok(addr) = addr_str.parse::<IpAddr>()
172                    && let Ok(prefix) = prefix_str.parse::<u8>()
173                {
174                    entries.push(NoProxyEntry::IpNetwork(addr, prefix));
175                    continue;
176                }
177            }
178
179            // Try parsing as IP address
180            if let Ok(addr) = pattern.parse::<IpAddr>() {
181                entries.push(NoProxyEntry::IpAddr(addr));
182                continue;
183            }
184
185            // Treat as domain pattern (normalize *.prefix to .prefix for wildcard matching)
186            let normalized = if pattern.starts_with("*.") {
187                &pattern[1..]
188            } else {
189                pattern
190            };
191            entries.push(NoProxyEntry::Domain(normalized.to_lowercase()));
192        }
193
194        Self { entries, match_all }
195    }
196
197    /// Check if a given target address should bypass the proxy (i.e., matches NO_PROXY).
198    pub fn should_bypass(&self, target: &SocketAddr) -> bool {
199        if self.match_all {
200            return true;
201        }
202
203        let ip = target.ip();
204
205        for entry in &self.entries {
206            match entry {
207                NoProxyEntry::IpAddr(entry_ip) if *entry_ip == ip => return true,
208                NoProxyEntry::IpNetwork(network_addr, prefix_len)
209                    if Self::ip_in_network(ip, *network_addr, *prefix_len) =>
210                {
211                    return true;
212                }
213                _ => {}
214            }
215        }
216
217        false
218    }
219
220    /// Check if a hostname should bypass the proxy.
221    pub fn should_bypass_hostname(&self, hostname: &str) -> bool {
222        if self.match_all {
223            return true;
224        }
225
226        let hostname_lower = hostname.to_lowercase();
227
228        for entry in &self.entries {
229            if let NoProxyEntry::Domain(pattern) = entry {
230                if let Some(stripped) = pattern.strip_prefix('.') {
231                    // Wildcard subdomain match: .example.com matches *.example.com
232                    if hostname_lower.ends_with(pattern.as_str()) || hostname_lower == stripped {
233                        return true;
234                    }
235                } else {
236                    // Exact domain match
237                    if hostname_lower == *pattern {
238                        return true;
239                    }
240                }
241            }
242        }
243
244        false
245    }
246
247    /// Check if an IP falls within a CIDR network range.
248    fn ip_in_network(ip: IpAddr, network: IpAddr, prefix_len: u8) -> bool {
249        match (ip, network) {
250            (std::net::IpAddr::V4(ip_v4), std::net::IpAddr::V4(net_v4)) => {
251                let ip_u32 = u32::from_be_bytes(ip_v4.octets());
252                let net_u32 = u32::from_be_bytes(net_v4.octets());
253                let mask = if prefix_len >= 32 {
254                    0xFFFFFFFFu32
255                } else {
256                    !(0xFFFFFFFF >> prefix_len)
257                };
258                (ip_u32 & mask) == (net_u32 & mask)
259            }
260            (std::net::IpAddr::V6(ip_v6), std::net::IpAddr::V6(net_v6)) => {
261                let ip_octets = ip_v6.octets();
262                let net_octets = net_v6.octets();
263                let full_bytes = (prefix_len as usize) / 8;
264                let remaining_bits = (prefix_len as usize) % 8;
265
266                // Compare full bytes
267                if ip_octets[..full_bytes] != net_octets[..full_bytes] {
268                    return false;
269                }
270
271                // Compare remaining bits
272                if remaining_bits > 0 && full_bytes < 16 {
273                    let mask = !(0xFFu8 >> remaining_bits);
274                    if (ip_octets[full_bytes] & mask) != (net_octets[full_bytes] & mask) {
275                        return false;
276                    }
277                }
278
279                true
280            }
281            _ => false,
282        }
283    }
284}
285
286pub struct Socks4Connector {
287    pub user_id: String,
288}
289
290impl Socks4Connector {
291    pub fn new(user_id: impl Into<String>) -> Self {
292        Self {
293            user_id: user_id.into(),
294        }
295    }
296}
297
298impl SocksConnector for Socks4Connector {
299    fn connect<S: Read + Write>(&self, mut stream: S, target: &SocketAddr) -> Result<S, String> {
300        let ip = match target.ip() {
301            std::net::IpAddr::V4(v4) => v4,
302            std::net::IpAddr::V6(_) => {
303                return Err("SOCKS4 does not support IPv6 addresses".to_string());
304            }
305        };
306
307        let port = target.port();
308
309        let mut request = Vec::with_capacity(9 + self.user_id.len() + 1);
310        request.push(0x04);
311        request.push(0x01);
312        request.extend_from_slice(&port.to_be_bytes());
313        request.extend_from_slice(&ip.octets());
314        request.extend_from_slice(self.user_id.as_bytes());
315        request.push(0x00);
316
317        stream
318            .write_all(&request)
319            .map_err(|e| format!("SOCKS4 failed to send request: {}", e))?;
320
321        let mut response = [0u8; 8];
322        stream
323            .read_exact(&mut response)
324            .map_err(|e| format!("SOCKS4 failed to read response: {}", e))?;
325
326        if response[1] == 0x5A {
327            Ok(stream)
328        } else {
329            let msg = match response[1] {
330                0x91 => "request rejected or failed",
331                0x92 => "request rejected: SOCKS server cannot connect to identd on client",
332                0x93 => "request rejected: client program and identd report different user-ids",
333                code => return Err(format!("SOCKS4 unknown error code: 0x{:02X}", code)),
334            };
335            Err(msg.to_string())
336        }
337    }
338}
339
340pub struct Socks5Connector {
341    pub username: Option<String>,
342    pub password: Option<String>,
343}
344
345impl Socks5Connector {
346    pub fn new(username: Option<String>, password: Option<String>) -> Self {
347        Self { username, password }
348    }
349
350    pub fn no_auth() -> Self {
351        Self {
352            username: None,
353            password: None,
354        }
355    }
356
357    fn send_greeting<S: Read + Write>(&self, stream: &mut S) -> Result<u8, String> {
358        let has_credentials = self.username.is_some() && self.password.is_some();
359        let nmethods = if has_credentials { 2u8 } else { 1u8 };
360
361        let mut greeting = vec![0x05, nmethods];
362        greeting.push(0x00);
363        if has_credentials {
364            greeting.push(0x02);
365        }
366
367        stream
368            .write_all(&greeting)
369            .map_err(|e| format!("SOCKS5 failed to send greeting: {}", e))?;
370
371        let mut reply = [0u8; 2];
372        stream
373            .read_exact(&mut reply)
374            .map_err(|e| format!("SOCKS5 failed to read greeting response: {}", e))?;
375
376        if reply[0] != 0x05 {
377            return Err(format!(
378                "SOCKS5 invalid version in greeting response: 0x{:02X}",
379                reply[0]
380            ));
381        }
382
383        Ok(reply[1])
384    }
385
386    fn authenticate<S: Read + Write>(&self, stream: &mut S) -> Result<(), String> {
387        let username = self.username.as_deref().unwrap_or("");
388        let password = self.password.as_deref().unwrap_or("");
389
390        if username.len() > 255 || password.len() > 255 {
391            return Err(
392                "SOCKS5 username or password exceeds maximum length of 255 bytes".to_string(),
393            );
394        }
395
396        let mut auth_req = Vec::with_capacity(3 + username.len() + password.len());
397        auth_req.push(0x01);
398        auth_req.push(username.len() as u8);
399        auth_req.extend_from_slice(username.as_bytes());
400        auth_req.push(password.len() as u8);
401        auth_req.extend_from_slice(password.as_bytes());
402
403        stream
404            .write_all(&auth_req)
405            .map_err(|e| format!("SOCKS5 failed to send auth request: {}", e))?;
406
407        let mut auth_reply = [0u8; 2];
408        stream
409            .read_exact(&mut auth_reply)
410            .map_err(|e| format!("SOCKS5 failed to read auth response: {}", e))?;
411
412        if auth_reply[0] != 0x01 {
413            return Err(format!(
414                "SOCKS5 invalid auth response version: 0x{:02X}",
415                auth_reply[0]
416            ));
417        }
418
419        if auth_reply[1] != 0x00 {
420            return Err("SOCKS5 authentication failed".to_string());
421        }
422
423        Ok(())
424    }
425
426    fn send_connect_request<S: Read + Write>(
427        &self,
428        stream: &mut S,
429        target: &SocketAddr,
430    ) -> Result<(), String> {
431        let (atyp, addr_bytes) = match target.ip() {
432            std::net::IpAddr::V4(v4) => (0x01u8, v4.octets().to_vec()),
433            std::net::IpAddr::V6(v6) => (0x04u8, v6.octets().to_vec()),
434        };
435
436        let port = target.port();
437
438        let mut req = Vec::with_capacity(6 + addr_bytes.len());
439        req.push(0x05);
440        req.push(0x01);
441        req.push(0x00);
442        req.push(atyp);
443        req.extend_from_slice(&addr_bytes);
444        req.extend_from_slice(&port.to_be_bytes());
445
446        stream
447            .write_all(&req)
448            .map_err(|e| format!("SOCKS5 failed to send connect request: {}", e))?;
449
450        let ver =
451            read_u8(stream).map_err(|e| format!("SOCKS5 failed to read reply version: {}", e))?;
452        if ver != 0x05 {
453            return Err(format!("SOCKS5 invalid reply version: 0x{:02X}", ver));
454        }
455
456        let rep =
457            read_u8(stream).map_err(|e| format!("SOCKS5 failed to read reply code: {}", e))?;
458        if rep != 0x00 {
459            let msg = match rep {
460                0x01 => "general SOCKS server failure",
461                0x02 => "connection not allowed by ruleset",
462                0x03 => "network unreachable",
463                0x04 => "host unreachable",
464                0x05 => "connection refused",
465                0x06 => "TTL expired",
466                0x07 => "command not supported",
467                0x08 => "address type not supported",
468                code => return Err(format!("SOCKS5 unknown error code: 0x{:02X}", code)),
469            };
470            return Err(msg.to_string());
471        }
472
473        let _rsv =
474            read_u8(stream).map_err(|e| format!("SOCKS5 failed to read reserved byte: {}", e))?;
475        let atyp_reply =
476            read_u8(stream).map_err(|e| format!("SOCKS5 failed to read address type: {}", e))?;
477
478        match atyp_reply {
479            0x01 => {
480                let mut _bnd_addr = [0u8; 4];
481                stream
482                    .read_exact(&mut _bnd_addr)
483                    .map_err(|e| format!("SOCKS5 failed to read bound IPv4 address: {}", e))?;
484            }
485            0x03 => {
486                let len = read_u8(stream)
487                    .map_err(|e| format!("SOCKS5 failed to read domain length: {}", e))?;
488                let mut _bnd_domain = vec![0u8; len as usize];
489                stream
490                    .read_exact(&mut _bnd_domain)
491                    .map_err(|e| format!("SOCKS5 failed to read bound domain: {}", e))?;
492            }
493            0x04 => {
494                let mut _bnd_addr = [0u8; 16];
495                stream
496                    .read_exact(&mut _bnd_addr)
497                    .map_err(|e| format!("SOCKS5 failed to read bound IPv6 address: {}", e))?;
498            }
499            _ => {
500                return Err(format!(
501                    "SOCKS5 unsupported address type in reply: 0x{:02X}",
502                    atyp_reply
503                ));
504            }
505        }
506
507        let mut _bnd_port = [0u8; 2];
508        stream
509            .read_exact(&mut _bnd_port)
510            .map_err(|e| format!("SOCKS5 failed to read bound port: {}", e))?;
511
512        Ok(())
513    }
514}
515
516impl SocksConnector for Socks5Connector {
517    fn connect<S: Read + Write>(&self, mut stream: S, target: &SocketAddr) -> Result<S, String> {
518        let method = self.send_greeting(&mut stream)?;
519
520        match method {
521            0x00 => {}
522            0x02 => {
523                self.authenticate(&mut stream)?;
524            }
525            _ => {
526                return Err(format!(
527                    "SOCKS5 server returned unacceptable authentication method: 0x{:02X}",
528                    method
529                ));
530            }
531        }
532
533        self.send_connect_request(&mut stream, target)?;
534
535        Ok(stream)
536    }
537}
538
539fn read_u8<S: Read>(stream: &mut S) -> Result<u8, std::io::Error> {
540    let mut buf = [0u8; 1];
541    stream.read_exact(&mut buf)?;
542    Ok(buf[0])
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548    use std::io::Cursor;
549    use std::net::{Ipv4Addr, SocketAddrV4};
550
551    #[derive(Debug)]
552    struct MockTcpStream {
553        reader: Cursor<Vec<u8>>,
554        writer: Vec<u8>,
555    }
556
557    impl MockTcpStream {
558        fn new(read_data: Vec<u8>) -> Self {
559            Self {
560                reader: Cursor::new(read_data),
561                writer: Vec::new(),
562            }
563        }
564
565        fn into_write(self) -> Vec<u8> {
566            self.writer
567        }
568    }
569
570    impl Read for MockTcpStream {
571        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
572            self.reader.read(buf)
573        }
574    }
575
576    impl Write for MockTcpStream {
577        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
578            self.writer.extend_from_slice(buf);
579            Ok(buf.len())
580        }
581
582        fn flush(&mut self) -> std::io::Result<()> {
583            Ok(())
584        }
585    }
586
587    #[test]
588    fn test_socks4_happy_path() {
589        let connector = Socks4Connector::new("testuser");
590        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 8080).into();
591
592        let mock_response: Vec<u8> = vec![0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
593        let mock_stream = MockTcpStream::new(mock_response);
594
595        let result = connector.connect(mock_stream, &target);
596        assert!(result.is_ok(), "SOCKS4 happy path should succeed");
597
598        let written = result.unwrap().into_write();
599        assert_eq!(written[0], 0x04, "version byte should be 0x04");
600        assert_eq!(written[1], 0x01, "command byte should be 0x01 (connect)");
601        assert_eq!(&written[4..8], &[127, 0, 0, 1], "IP should be 127.0.0.1");
602        assert_eq!(
603            &written[8..17],
604            b"testuser\0",
605            "user ID should be null-terminated"
606        );
607    }
608
609    #[test]
610    fn test_socks4_rejected_error() {
611        let connector = Socks4Connector::new("user");
612        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1234).into();
613
614        let mock_response: Vec<u8> = vec![0x00, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
615        let mock_stream = MockTcpStream::new(mock_response);
616
617        let result = connector.connect(mock_stream, &target);
618        assert!(result.is_err(), "SOCKS4 rejection should return error");
619        let err_msg = result.err().unwrap();
620        assert!(
621            err_msg.contains("rejected"),
622            "error message should mention rejection: {}",
623            err_msg
624        );
625    }
626
627    #[test]
628    fn test_socks4_identd_error() {
629        let connector = Socks4Connector::new("user");
630        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1234).into();
631
632        let mock_response: Vec<u8> = vec![0x00, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
633        let mock_stream = MockTcpStream::new(mock_response);
634
635        let result = connector.connect(mock_stream, &target);
636        assert!(result.is_err());
637        let err_msg = result.err().unwrap();
638        assert!(
639            err_msg.contains("identd"),
640            "error message should mention identd: {}",
641            err_msg
642        );
643    }
644
645    #[test]
646    fn test_socks4_userid_mismatch_error() {
647        let connector = Socks4Connector::new("user");
648        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1234).into();
649
650        let mock_response: Vec<u8> = vec![0x00, 0x93, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
651        let mock_stream = MockTcpStream::new(mock_response);
652
653        let result = connector.connect(mock_stream, &target);
654        assert!(result.is_err());
655        let err_msg = result.err().unwrap();
656        assert!(
657            err_msg.contains("identd") && err_msg.contains("different"),
658            "error message should mention identd/user-id mismatch: {}",
659            err_msg
660        );
661    }
662
663    #[test]
664    fn test_socks4_empty_user_id() {
665        let connector = Socks4Connector::new("");
666        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 1), 443).into();
667
668        let mock_response: Vec<u8> = vec![0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
669        let mock_stream = MockTcpStream::new(mock_response);
670
671        let result = connector.connect(mock_stream, &target);
672        assert!(result.is_ok(), "empty user ID should work");
673        let written = result.unwrap().into_write();
674        assert_eq!(
675            &written[4..9],
676            &[192, 168, 1, 1, 0x00],
677            "null terminator after IP when no user ID"
678        );
679    }
680
681    #[test]
682    fn test_socks5_no_auth_happy_path() {
683        let connector = Socks5Connector::no_auth();
684        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(93, 184, 216, 34), 443).into();
685
686        let mock_response: Vec<u8> = vec![
687            0x05, 0x00, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x90,
688        ];
689        let mock_stream = MockTcpStream::new(mock_response);
690
691        let result = connector.connect(mock_stream, &target);
692        assert!(result.is_ok(), "SOCKS5 no-auth happy path should succeed");
693
694        let written = result.unwrap().into_write();
695        assert_eq!(written[0], 0x05, "greeting version should be 0x05");
696        assert_eq!(written[1], 0x01, "should offer exactly 1 method (no-auth)");
697        assert_eq!(written[2], 0x00, "method offered should be 0x00 (no-auth)");
698        let conn_offset = 3;
699        assert_eq!(written[conn_offset], 0x05, "connect request version");
700        assert_eq!(written[conn_offset + 1], 0x01, "connect command");
701        assert_eq!(written[conn_offset + 2], 0x00, "reserved");
702        assert_eq!(written[conn_offset + 3], 0x01, "ATYP IPv4");
703        assert_eq!(
704            &written[conn_offset + 4..conn_offset + 8],
705            &[93, 184, 216, 34],
706            "target IP"
707        );
708        assert_eq!(
709            &written[conn_offset + 8..conn_offset + 10],
710            &0x01BBu16.to_be_bytes(),
711            "target port 443"
712        );
713    }
714
715    #[test]
716    fn test_socks5_username_password_auth_happy_path() {
717        let connector =
718            Socks5Connector::new(Some("myuser".to_string()), Some("mypass".to_string()));
719        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(10, 20, 30, 40), 8080).into();
720
721        let mock_response: Vec<u8> = vec![
722            0x05, 0x02, 0x01, 0x00, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x90,
723        ];
724        let mock_stream = MockTcpStream::new(mock_response);
725
726        let result = connector.connect(mock_stream, &target);
727        assert!(
728            result.is_ok(),
729            "SOCKS5 username/password auth should succeed"
730        );
731
732        let written = result.unwrap().into_write();
733        assert_eq!(written[0], 0x05, "greeting version");
734        assert_eq!(written[1], 0x02, "should offer 2 methods");
735        assert_eq!(written[2], 0x00, "no-auth method");
736        assert_eq!(written[3], 0x02, "username/password method");
737        let auth_offset = 4;
738        assert_eq!(written[auth_offset], 0x01, "auth sub-negotiation version");
739        assert_eq!(written[auth_offset + 1], 6, "username length");
740        assert_eq!(
741            &written[auth_offset + 2..auth_offset + 8],
742            b"myuser",
743            "username"
744        );
745        assert_eq!(written[auth_offset + 8], 6, "password length");
746        assert_eq!(
747            &written[auth_offset + 9..auth_offset + 15],
748            b"mypass",
749            "password"
750        );
751    }
752
753    #[test]
754    fn test_socks5_auth_failure() {
755        let connector = Socks5Connector::new(Some("bad".to_string()), Some("cred".to_string()));
756        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 5678).into();
757
758        let mock_response: Vec<u8> = vec![0x05, 0x02, 0x01, 0x01];
759        let mock_stream = MockTcpStream::new(mock_response);
760
761        let result = connector.connect(mock_stream, &target);
762        assert!(result.is_err(), "auth failure should be an error");
763        let err_msg = result.err().unwrap();
764        assert!(
765            err_msg.contains("authentication failed") || err_msg.contains("failed"),
766            "error should mention auth failure: {}",
767            err_msg
768        );
769    }
770
771    #[test]
772    fn test_socks5_connection_refused() {
773        let connector = Socks5Connector::no_auth();
774        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(8, 8, 8, 8), 53).into();
775
776        let mock_response: Vec<u8> = vec![
777            0x05, 0x00, 0x05, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
778        ];
779        let mock_stream = MockTcpStream::new(mock_response);
780
781        let result = connector.connect(mock_stream, &target);
782        assert!(result.is_err(), "connection refused should be an error");
783        let err_msg = result.err().unwrap();
784        assert!(
785            err_msg.contains("refused"),
786            "error should mention connection refused: {}",
787            err_msg
788        );
789    }
790
791    #[test]
792    fn test_socks5_unacceptable_method() {
793        let connector = Socks5Connector::no_auth();
794        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(1, 1, 1, 1), 80).into();
795
796        let mock_response: Vec<u8> = vec![0x05, 0xFF];
797        let mock_stream = MockTcpStream::new(mock_response);
798
799        let result = connector.connect(mock_stream, &target);
800        assert!(result.is_err(), "unacceptable method should be an error");
801        let err_msg = result.err().unwrap();
802        assert!(
803            err_msg.contains("unacceptable") || err_msg.contains("0xFF"),
804            "error should mention unacceptable method: {}",
805            err_msg
806        );
807    }
808
809    #[test]
810    fn test_socks5_general_failure() {
811        let connector = Socks5Connector::no_auth();
812        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 1).into();
813
814        let mock_response: Vec<u8> = vec![
815            0x05, 0x00, 0x05, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
816        ];
817        let mock_stream = MockTcpStream::new(mock_response);
818
819        let result = connector.connect(mock_stream, &target);
820        assert!(result.is_err(), "general failure should be an error");
821        let err_msg = result.err().unwrap();
822        assert!(
823            err_msg.contains("general") || err_msg.contains("failure"),
824            "error should mention general failure: {}",
825            err_msg
826        );
827    }
828
829    // ==================== E7: New Proxy Tests ====================
830
831    // Test 1: SOCKS4 connect success (valid response bytes -> Ok)
832    #[test]
833    fn e7_test_socks4_connect_success() {
834        let connector = Socks4Connector::new("proxyuser");
835        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(172, 16, 0, 1), 443).into();
836
837        let mock_response: Vec<u8> = vec![0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
838        let mock_stream = MockTcpStream::new(mock_response);
839
840        let result = connector.connect(mock_stream, &target);
841        assert!(
842            result.is_ok(),
843            "SOCKS4 connect with valid response should succeed"
844        );
845
846        let written = result.unwrap().into_write();
847        assert_eq!(written[0], 0x04, "SOCKS version byte");
848        assert_eq!(written[1], 0x01, "CONNECT command");
849    }
850
851    // Test 2: SOCKS4 connect fail (error code 0x91 -> Err)
852    #[test]
853    fn e7_test_socks4_connect_fail_rejected() {
854        let connector = Socks4Connector::new("test");
855        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 100), 80).into();
856
857        let mock_response: Vec<u8> = vec![0x00, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
858        let mock_stream = MockTcpStream::new(mock_response);
859
860        let result = connector.connect(mock_stream, &target);
861        assert!(result.is_err(), "SOCKS4 error 0x91 should return Err");
862        assert!(
863            result.unwrap_err().contains("rejected"),
864            "error message must contain 'rejected'"
865        );
866    }
867
868    // Test 3: SOCKS5 no-auth connect (Greeting 0x00 + Connect success 0x00)
869    #[test]
870    fn e7_test_socks5_no_auth_connect() {
871        let connector = Socks5Connector::no_auth();
872        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 8080).into();
873
874        let mock_response: Vec<u8> = vec![
875            0x05, 0x00, 0x05, 0x00, 0x00, 0x01, 0x0a, 0x00, 0x00, 0x01, 0x1f, 0x90,
876        ];
877        let mock_stream = MockTcpStream::new(mock_response);
878
879        let result = connector.connect(mock_stream, &target);
880        assert!(result.is_ok(), "SOCKS5 no-auth connect should succeed");
881
882        let written = result.unwrap().into_write();
883        assert_eq!(written[0], 0x05, "greeting version");
884        assert_eq!(written[1], 0x01, "one method offered");
885        assert_eq!(written[2], 0x00, "no-auth method");
886    }
887
888    // Test 4: SOCKS5 password auth (Greeting 0x02 + Auth success + Connect success)
889    #[test]
890    fn e7_test_socks5_password_auth_connect() {
891        let connector =
892            Socks5Connector::new(Some("admin".to_string()), Some("secret123".to_string()));
893        let target: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 9090).into();
894
895        let mock_response: Vec<u8> = vec![
896            0x05, 0x02, 0x01, 0x00, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x23, 0x82,
897        ];
898        let mock_stream = MockTcpStream::new(mock_response);
899
900        let result = connector.connect(mock_stream, &target);
901        assert!(
902            result.is_ok(),
903            "SOCKS5 password auth connect should succeed"
904        );
905
906        let written = result.unwrap().into_write();
907        assert_eq!(written[0], 0x05, "greeting version");
908        assert_eq!(written[1], 0x02, "two methods offered");
909        assert_eq!(written[3], 0x02, "username/password method offered");
910
911        let auth_offset = 4;
912        assert_eq!(written[auth_offset], 0x01, "auth sub-version");
913        assert_eq!(written[auth_offset + 1], 5, "username length 'admin'");
914        assert_eq!(
915            &written[auth_offset + 2..auth_offset + 7],
916            b"admin",
917            "username bytes"
918        );
919        assert_eq!(written[auth_offset + 7], 9, "password length 'secret123'");
920        assert_eq!(
921            &written[auth_offset + 8..auth_offset + 17],
922            b"secret123",
923            "password bytes"
924        );
925    }
926
927    // Test 5: No-proxy bypass matcher
928    #[test]
929    fn e7_test_no_proxy_bypass_matcher() {
930        let matcher = NoProxyMatcher::from_env_value("*.local,localhost,example.com,.internal");
931
932        // Should bypass: wildcard *.local matches api.local
933        assert!(
934            matcher.should_bypass_hostname("api.local"),
935            "*.local should match api.local"
936        );
937
938        // Should NOT bypass: example.com is not in the list (example.org is different)
939        assert!(
940            !matcher.should_bypass_hostname("example.org"),
941            "example.org should not bypass"
942        );
943
944        // Should bypass: exact match localhost
945        assert!(
946            matcher.should_bypass_hostname("localhost"),
947            "exact match localhost should bypass"
948        );
949
950        // Should bypass: exact match example.com
951        assert!(
952            matcher.should_bypass_hostname("example.com"),
953            "exact match example.com should bypass"
954        );
955
956        // Should bypass: .internal wildcard matches sub.internal
957        assert!(
958            matcher.should_bypass_hostname("sub.internal"),
959            ".internal should match sub.internal"
960        );
961
962        // Should bypass: .internal matches internal itself
963        assert!(
964            matcher.should_bypass_hostname("internal"),
965            ".internal should also match bare domain"
966        );
967
968        // Should NOT bypass: random external host
969        assert!(
970            !matcher.should_bypass_hostname("google.com"),
971            "google.com should not bypass"
972        );
973    }
974
975    // Test 5b: No-proxy IP-based matching
976    #[test]
977    fn e7_test_no_proxy_ip_matching() {
978        use std::net::{IpAddr, Ipv4Addr};
979
980        let matcher = NoProxyMatcher::from_env_value("192.168.1.1,10.0.0.0/8");
981
982        let addr_v4_192: SocketAddr =
983            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 80);
984        assert!(
985            matcher.should_bypass(&addr_v4_192),
986            "exact IP 192.168.1.1 should bypass"
987        );
988
989        let addr_v4_10: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 5, 3, 1)), 8080);
990        assert!(
991            matcher.should_bypass(&addr_v4_10),
992            "10.5.3.1 should be within 10.0.0.0/8"
993        );
994
995        let addr_v4_external: SocketAddr =
996            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1)), 443);
997        assert!(
998            !matcher.should_bypass(&addr_v4_external),
999            "172.16.0.1 should NOT be within 10.0.0.0/8"
1000        );
1001    }
1002
1003    // Test 5c: No-proxy wildcard * matches everything
1004    #[test]
1005    fn e7_test_no_proxy_wildcard_all() {
1006        let matcher = NoProxyMatcher::from_env_value("*");
1007
1008        assert!(
1009            matcher.should_bypass_hostname("anything"),
1010            "* should bypass any hostname"
1011        );
1012        assert!(
1013            matcher.should_bypass(&"127.0.0.1:80".parse::<SocketAddr>().unwrap()),
1014            "* should bypass any IP address"
1015        );
1016    }
1017
1018    // Test 6: Proxy URL parsing
1019    #[test]
1020    fn e7_test_proxy_url_parsing_socks5_with_credentials() {
1021        let url = "socks5://user:pass@127.0.0.1:1080";
1022        let parsed = ProxyUrl::parse(url).expect("should parse socks5 URL");
1023
1024        assert_eq!(parsed.protocol, ProxyProtocol::Socks5);
1025        assert_eq!(parsed.host, "127.0.0.1");
1026        assert_eq!(parsed.port, 1080);
1027        assert_eq!(parsed.username, Some("user".to_string()));
1028        assert_eq!(parsed.password, Some("pass".to_string()));
1029    }
1030
1031    // Test 6b: Parse socks4 URL without credentials
1032    #[test]
1033    fn e7_test_proxy_url_parsing_socks4_no_credentials() {
1034        let parsed =
1035            ProxyUrl::parse("socks4://proxy.example.com:1080").expect("should parse socks4 URL");
1036
1037        assert_eq!(parsed.protocol, ProxyProtocol::Socks4);
1038        assert_eq!(parsed.host, "proxy.example.com");
1039        assert_eq!(parsed.port, 1080);
1040        assert!(parsed.username.is_none());
1041        assert!(parsed.password.is_none());
1042    }
1043
1044    // Test 6c: Parse HTTP proxy URL
1045    #[test]
1046    fn e7_test_proxy_url_parsing_http() {
1047        let parsed = ProxyUrl::parse("http://admin:secret@proxy.corp.com:3128")
1048            .expect("should parse http proxy URL");
1049
1050        assert_eq!(parsed.protocol, ProxyProtocol::Http);
1051        assert_eq!(parsed.host, "proxy.corp.com");
1052        assert_eq!(parsed.port, 3128);
1053        assert_eq!(parsed.username, Some("admin".to_string()));
1054        assert_eq!(parsed.password, Some("secret".to_string()));
1055    }
1056
1057    // Test 6d: Default port when omitted
1058    #[test]
1059    fn e7_test_proxy_url_default_port() {
1060        let socks5_parsed = ProxyUrl::parse("socks5://proxy.local").expect("should parse");
1061        assert_eq!(socks5_parsed.port, 1080, "SOCKS default port is 1080");
1062
1063        let http_parsed = ProxyUrl::parse("http://webproxy.local").expect("should parse");
1064        assert_eq!(http_parsed.port, 8080, "HTTP default port is 8080");
1065
1066        let https_parsed = ProxyUrl::parse("https://secure.local").expect("should parse");
1067        assert_eq!(https_parsed.port, 443, "HTTPS default port is 443");
1068    }
1069
1070    // Test 6e: Invalid protocol returns error
1071    #[test]
1072    fn e7_test_proxy_url_invalid_protocol() {
1073        let result = ProxyUrl::parse("ftp://host:21");
1074        assert!(result.is_err(), "unsupported protocol should return error");
1075        assert!(
1076            result.unwrap_err().contains("Unsupported"),
1077            "error should mention unsupported protocol"
1078        );
1079    }
1080
1081    // Test 6f: Create connector from parsed URL
1082    #[test]
1083    fn e7_test_create_connector_from_url() {
1084        let url = ProxyUrl::parse("socks5://myuser:mypass@10.0.0.1:9050").expect("should parse");
1085        let _connector = url.create_connector();
1086    }
1087}