Skip to main content

tor_chanmgr/transport/
proxied.rs

1//! Connect to relays via a proxy.
2//!
3//! This code is here for two reasons:
4//!   1. To connect via external pluggable transports (for which we use SOCKS to
5//!      build our connections).
6//!   2. To support users who are behind a firewall that requires them to use a
7//!      SOCKS proxy to connect.
8//!
9//! Supports SOCKS4/4a/5 and HTTP CONNECT proxies.
10//
11// TODO: Maybe refactor this so that tor-ptmgr can exist in a more freestanding
12// way, with fewer arti dependencies.
13#![allow(dead_code)]
14
15use std::{
16    net::{IpAddr, SocketAddr},
17    sync::Arc,
18};
19
20use base64ct::{Base64, Encoding};
21use futures::io::{AsyncBufReadExt, BufReader};
22use futures::{AsyncReadExt, AsyncWriteExt};
23use httparse;
24use safelog::Sensitive;
25use tor_linkspec::PtTargetAddr;
26use tor_rtcompat::NetStreamProvider;
27use tor_socksproto::{
28    Handshake as _, SocksAddr, SocksAuth, SocksClientHandshake, SocksCmd, SocksRequest,
29    SocksStatus, SocksVersion,
30};
31use tracing::trace;
32
33#[cfg(feature = "pt-client")]
34use super::TransportImplHelper;
35#[cfg(feature = "pt-client")]
36use async_trait::async_trait;
37#[cfg(feature = "pt-client")]
38use tor_error::bad_api_usage;
39#[cfg(feature = "pt-client")]
40use tor_linkspec::{ChannelMethod, HasChanMethod, OwnedChanTarget};
41#[cfg(feature = "pt-client")]
42use tor_proto::peer::PeerAddr;
43
44/// Information about what proxy protocol to use, and how to use it.
45#[derive(Clone, Debug, Eq, PartialEq)]
46#[non_exhaustive]
47pub enum Protocol {
48    /// Connect via SOCKS 4, SOCKS 4a, or SOCKS 5.
49    Socks(SocksVersion, SocksAuth),
50    /// Connect via HTTP CONNECT proxy (RFC 7231).
51    HttpConnect {
52        /// Optional Basic auth credentials (username, password) for Proxy-Authorization header.
53        auth: Option<(Sensitive<String>, Sensitive<String>)>,
54    },
55}
56
57/// An address to use when told to connect to "no address."
58const NO_ADDR: IpAddr = IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 1));
59/// Maximum number of bytes allowed in HTTP response headers from proxy.
60const MAX_HTTP_HEADER_BYTES: usize = 16 * 1024;
61
62/// Open a connection to `target` via the proxy at `proxy`, using the protocol
63/// at `protocol`.
64///
65/// # Limitations
66///
67/// We will give an error if the proxy sends us any data on the connection along
68/// with its final handshake: due to our implementation, any such data will be
69/// discarded, and so we give an error rather than fail silently.
70///
71/// This limitation doesn't matter when the underlying protocol is Tor, or
72/// anything else where the initiator is expected to speak before the responder
73/// says anything.  To lift it, we would have to make this function's return
74/// type become something buffered.
75//
76// TODO: Perhaps we should refactor this someday so it can be a general-purpose
77// proxy function, not only for Arti.
78pub(crate) async fn connect_via_proxy<R: NetStreamProvider + Send + Sync>(
79    runtime: &R,
80    proxy: &SocketAddr,
81    protocol: &Protocol,
82    target: &PtTargetAddr,
83) -> Result<R::Stream, ProxyError> {
84    trace!(
85        "Launching a proxied connection to {} via proxy at {} using {:?}",
86        target, proxy, protocol
87    );
88    let stream = runtime
89        .connect(proxy)
90        .await
91        .map_err(|e| ProxyError::ProxyConnect(Arc::new(e)))?;
92
93    match protocol {
94        Protocol::Socks(version, auth) => {
95            do_socks_handshake::<R>(stream, version, auth, target).await
96        }
97        Protocol::HttpConnect { auth } => {
98            do_http_connect_handshake::<R>(stream, auth, target).await
99        }
100    }
101}
102
103/// Perform SOCKS proxy handshake.
104async fn do_socks_handshake<R: NetStreamProvider + Send + Sync>(
105    mut stream: R::Stream,
106    version: &SocksVersion,
107    auth: &SocksAuth,
108    target: &PtTargetAddr,
109) -> Result<R::Stream, ProxyError> {
110    let (target_addr, target_port): (SocksAddr, u16) = match target {
111        PtTargetAddr::IpPort(a) => (SocksAddr::Ip(a.ip()), a.port()),
112        #[cfg(feature = "pt-client")]
113        PtTargetAddr::HostPort(host, port) => (
114            SocksAddr::Hostname(
115                host.clone()
116                    .try_into()
117                    .map_err(ProxyError::InvalidSocksAddr)?,
118            ),
119            *port,
120        ),
121        #[cfg(feature = "pt-client")]
122        PtTargetAddr::None => (SocksAddr::Ip(NO_ADDR), 1),
123        _ => return Err(ProxyError::UnrecognizedAddr),
124    };
125
126    let request = SocksRequest::new(
127        *version,
128        SocksCmd::CONNECT,
129        target_addr,
130        target_port,
131        auth.clone(),
132    )
133    .map_err(ProxyError::InvalidSocksRequest)?;
134    let mut handshake = SocksClientHandshake::new(request);
135
136    let mut buf = tor_socksproto::Buffer::new();
137    let reply = loop {
138        use tor_socksproto::NextStep as NS;
139        match handshake.step(&mut buf).map_err(ProxyError::SocksProto)? {
140            NS::Send(send) => {
141                stream.write_all(&send).await?;
142                stream.flush().await?;
143            }
144            NS::Finished(fin) => {
145                break fin
146                    .into_output_forbid_pipelining()
147                    .map_err(ProxyError::SocksProto)?;
148            }
149            NS::Recv(mut recv) => {
150                let n = stream.read(recv.buf()).await?;
151                recv.note_received(n).map_err(ProxyError::SocksProto)?;
152            }
153        }
154    };
155
156    let status = reply.status();
157    trace!("SOCKS handshake succeeded, status {:?}", status);
158
159    if status != SocksStatus::SUCCEEDED {
160        return Err(ProxyError::SocksError(status));
161    }
162
163    Ok(stream)
164}
165
166/// Format target address for HTTP CONNECT request line and Host header.
167fn format_connect_target(target: &PtTargetAddr) -> Result<String, ProxyError> {
168    match target {
169        PtTargetAddr::IpPort(a) => {
170            let host = match a.ip() {
171                IpAddr::V4(ip) => ip.to_string(),
172                IpAddr::V6(ip) => format!("[{}]", ip),
173            };
174            Ok(format!("{}:{}", host, a.port()))
175        }
176        #[cfg(feature = "pt-client")]
177        PtTargetAddr::HostPort(host, port) => Ok(format!("{}:{}", host, port)),
178        #[cfg(feature = "pt-client")]
179        PtTargetAddr::None => Err(ProxyError::UnrecognizedAddr),
180        _ => Err(ProxyError::UnrecognizedAddr),
181    }
182}
183
184/// Build HTTP CONNECT request string with optional Basic auth.
185fn build_http_connect_request(
186    target_str: &str,
187    auth: &Option<(Sensitive<String>, Sensitive<String>)>,
188) -> String {
189    // Build CONNECT request: CONNECT host:port HTTP/1.1\r\nHost: host:port\r\n[...]\r\n
190    let mut request = format!(
191        "CONNECT {} HTTP/1.1\r\nHost: {}\r\n",
192        target_str, target_str
193    );
194
195    if let Some((user, pass)) = auth {
196        // Proxy-Authorization: Basic base64(username:password) per RFC 7617
197        let credentials = format!("{}:{}", user.as_ref(), pass.as_ref());
198        let encoded = Base64::encode_string(credentials.as_bytes());
199        request.push_str(&format!("Proxy-Authorization: Basic {}\r\n", encoded));
200    }
201
202    request.push_str("\r\n");
203    request
204}
205
206/// Parse HTTP CONNECT response and extract status code.
207///
208/// Uses httparse for spec-compliant parsing. Rejects:
209/// - Responses larger than 16KB (prevents header bomb attacks)
210/// - Pipelined data after headers (connection is dedicated to tunnel)
211fn parse_http_connect_response(response_bytes: &[u8]) -> Result<u16, ProxyError> {
212    if response_bytes.len() > MAX_HTTP_HEADER_BYTES {
213        return Err(ProxyError::HttpConnectMalformed);
214    }
215
216    let mut headers = [httparse::EMPTY_HEADER; 64];
217    let mut resp = httparse::Response::new(&mut headers);
218
219    match resp.parse(response_bytes) {
220        Ok(httparse::Status::Complete(header_end)) => {
221            let status = resp.code.ok_or(ProxyError::HttpConnectMalformed)?;
222
223            if !(200..300).contains(&status) {
224                return Err(ProxyError::HttpConnectError(status));
225            }
226
227            // Reject any pipelined data after headers
228            if header_end < response_bytes.len() {
229                return Err(ProxyError::UnexpectedData);
230            }
231
232            trace!("HTTP CONNECT successful, status {}", status);
233            Ok(status)
234        }
235        Ok(httparse::Status::Partial) => Err(ProxyError::HttpConnectMalformed),
236        Err(_) => Err(ProxyError::HttpConnectMalformed),
237    }
238}
239
240/// Send HTTP CONNECT request to proxy.
241async fn send_http_connect_request<R: NetStreamProvider + Send + Sync>(
242    stream: &mut R::Stream,
243    auth: &Option<(Sensitive<String>, Sensitive<String>)>,
244    target_str: &str,
245) -> Result<(), ProxyError> {
246    let request = build_http_connect_request(target_str, auth);
247    trace!("Sending HTTP CONNECT request for {}", target_str);
248    stream.write_all(request.as_bytes()).await?;
249    stream.flush().await?;
250    Ok(())
251}
252
253/// Perform HTTP CONNECT proxy handshake (RFC 7231, RFC 7617 for Basic auth).
254async fn do_http_connect_handshake<R: NetStreamProvider + Send + Sync>(
255    mut stream: R::Stream,
256    auth: &Option<(Sensitive<String>, Sensitive<String>)>,
257    target: &PtTargetAddr,
258) -> Result<R::Stream, ProxyError> {
259    let target_str = format_connect_target(target)?;
260    send_http_connect_request::<R>(&mut stream, auth, &target_str).await?;
261
262    // Read response until we see the double CRLF that terminates headers
263    let mut response_buffer = Vec::new();
264    let mut reader = BufReader::new(stream);
265    let mut line = String::new();
266
267    loop {
268        line.clear();
269        let n = reader.read_line(&mut line).await?;
270        if n == 0 {
271            return Err(ProxyError::HttpConnectMalformed);
272        }
273
274        response_buffer.extend_from_slice(line.as_bytes());
275        if response_buffer.len() > MAX_HTTP_HEADER_BYTES {
276            return Err(ProxyError::HttpConnectMalformed);
277        }
278
279        // Check for blank line (end of headers)
280        if line == "\r\n" || line == "\n" {
281            break;
282        }
283    }
284
285    // Parse and validate response
286    let _status_code = parse_http_connect_response(&response_buffer)?;
287
288    // Return the underlying stream (BufReader is dropped, stream continues)
289    Ok(reader.into_inner())
290}
291
292/// An error that occurs while negotiating a connection with a proxy.
293#[derive(Clone, Debug, thiserror::Error)]
294#[non_exhaustive]
295pub enum ProxyError {
296    /// We had an IO error while trying to open a connection to the proxy.
297    #[error("Problem while connecting to proxy")]
298    ProxyConnect(#[source] Arc<std::io::Error>),
299
300    /// We had an IO error while talking to the proxy.
301    #[error("Problem while communicating with proxy")]
302    ProxyIo(#[source] Arc<std::io::Error>),
303
304    /// We tried to use an address which socks doesn't support.
305    #[error("SOCKS proxy does not support target address")]
306    InvalidSocksAddr(#[source] tor_socksproto::Error),
307
308    /// We tried to use an address type which _we_ don't recognize.
309    #[error("Got an address type we don't recognize")]
310    UnrecognizedAddr,
311
312    /// Our SOCKS implementation told us that this request cannot be encoded.
313    #[error("Tried to make an invalid SOCKS request")]
314    InvalidSocksRequest(#[source] tor_socksproto::Error),
315
316    /// The peer refused our request, or spoke SOCKS incorrectly.
317    #[error("Protocol error while communicating with SOCKS proxy")]
318    SocksProto(#[source] tor_socksproto::Error),
319
320    /// We encountered an internal programming error.
321    #[error("Internal error")]
322    Bug(#[from] tor_error::Bug),
323
324    /// We got extra data immediately after our handshake, before we actually
325    /// sent anything.
326    ///
327    /// This is not a bug in the calling code or in the peer protocol: it just
328    /// means that the remote peer sent us data before we actually sent it any
329    /// data. Unfortunately, there's a limitation in our code that makes it
330    /// discard any such data, and therefore we have to give this error to
331    /// prevent bugs.
332    ///
333    /// We could someday remove this limitation.
334    #[error("Received unexpected early data from peer")]
335    UnexpectedData,
336
337    /// The proxy told us that our attempt failed.
338    #[error("SOCKS proxy reported an error: {0}")]
339    SocksError(SocksStatus),
340
341    /// HTTP CONNECT proxy returned a non-2xx status code.
342    #[error("HTTP CONNECT proxy returned status: {0}")]
343    HttpConnectError(u16),
344
345    /// HTTP CONNECT proxy returned a malformed response.
346    #[error("HTTP CONNECT proxy returned invalid response")]
347    HttpConnectMalformed,
348}
349
350impl From<std::io::Error> for ProxyError {
351    fn from(e: std::io::Error) -> Self {
352        ProxyError::ProxyIo(Arc::new(e))
353    }
354}
355
356impl From<ProxyError> for std::io::Error {
357    fn from(e: ProxyError) -> Self {
358        std::io::Error::other(e)
359    }
360}
361
362impl tor_error::HasKind for ProxyError {
363    fn kind(&self) -> tor_error::ErrorKind {
364        use ProxyError as E;
365        use tor_error::ErrorKind as EK;
366        match self {
367            E::ProxyConnect(_) | E::ProxyIo(_) => EK::LocalNetworkError,
368            E::InvalidSocksAddr(_) | E::InvalidSocksRequest(_) => EK::BadApiUsage,
369            E::UnrecognizedAddr => EK::NotImplemented,
370            E::SocksProto(_) => EK::LocalProtocolViolation,
371            E::Bug(e) => e.kind(),
372            E::UnexpectedData => EK::NotImplemented,
373            E::SocksError(_) => EK::LocalProtocolViolation,
374            E::HttpConnectError(_) | E::HttpConnectMalformed => EK::LocalProtocolViolation,
375        }
376    }
377}
378
379impl tor_error::HasRetryTime for ProxyError {
380    fn retry_time(&self) -> tor_error::RetryTime {
381        use ProxyError as E;
382        use SocksStatus as S;
383        use tor_error::RetryTime as RT;
384        match self {
385            E::ProxyConnect(_) | E::ProxyIo(_) => RT::AfterWaiting,
386            E::InvalidSocksAddr(_) => RT::Never,
387            E::UnrecognizedAddr => RT::Never,
388            E::InvalidSocksRequest(_) => RT::Never,
389            E::SocksProto(_) => RT::AfterWaiting,
390            E::Bug(_) => RT::Never,
391            E::UnexpectedData => RT::Never,
392            E::SocksError(e) => match *e {
393                S::CONNECTION_REFUSED
394                | S::GENERAL_FAILURE
395                | S::HOST_UNREACHABLE
396                | S::NETWORK_UNREACHABLE
397                | S::TTL_EXPIRED => RT::AfterWaiting,
398                _ => RT::Never,
399            },
400            E::HttpConnectError(code) => {
401                // 502/503/504 may be transient; auth and policy errors are not.
402                if *code == 502 || *code == 503 || *code == 504 {
403                    RT::AfterWaiting
404                } else {
405                    RT::Never
406                }
407            }
408            E::HttpConnectMalformed => RT::Never,
409        }
410    }
411}
412
413#[cfg(feature = "pt-client")]
414/// An object that connects to a Tor bridge via an external pluggable transport
415/// that provides a proxy.
416#[derive(Clone, Debug)]
417pub struct ExternalProxyPlugin<R> {
418    /// The runtime to use for connections.
419    runtime: R,
420    /// The location of the proxy.
421    proxy_addr: SocketAddr,
422    /// The SOCKS protocol version to use.
423    proxy_version: SocksVersion,
424}
425
426#[cfg(feature = "pt-client")]
427impl<R: NetStreamProvider + Send + Sync> ExternalProxyPlugin<R> {
428    /// Make a new `ExternalProxyPlugin`.
429    pub fn new(rt: R, proxy_addr: SocketAddr, proxy_version: SocksVersion) -> Self {
430        Self {
431            runtime: rt,
432            proxy_addr,
433            proxy_version,
434        }
435    }
436}
437
438#[cfg(feature = "pt-client")]
439#[async_trait]
440impl<R: NetStreamProvider + Send + Sync> TransportImplHelper for ExternalProxyPlugin<R> {
441    type Stream = R::Stream;
442
443    async fn connect(&self, target: &OwnedChanTarget) -> crate::Result<(PeerAddr, R::Stream)> {
444        let pt_target = match target.chan_method() {
445            ChannelMethod::Direct(_) => {
446                return Err(crate::Error::UnusableTarget(bad_api_usage!(
447                    "Used pluggable transport for a TCP connection."
448                )));
449            }
450            ChannelMethod::Pluggable(target) => target,
451            other => {
452                return Err(crate::Error::UnusableTarget(bad_api_usage!(
453                    "Used unknown, unsupported, transport {:?} for a TCP connection.",
454                    other,
455                )));
456            }
457        };
458
459        let protocol =
460            settings_to_protocol(self.proxy_version, encode_settings(pt_target.settings()))?;
461        let stream =
462            connect_via_proxy(&self.runtime, &self.proxy_addr, &protocol, pt_target.addr()).await?;
463
464        Ok((pt_target.into(), stream))
465    }
466}
467
468/// Encode the PT settings from `IT` in a format that a pluggable transport can use.
469#[cfg(feature = "pt-client")]
470fn encode_settings<'a, IT>(settings: IT) -> String
471where
472    IT: Iterator<Item = (&'a str, &'a str)>,
473{
474    /// Escape a character in the way expected by pluggable transports.
475    ///
476    /// This escape machinery is a mirror of that in the standard library.
477    enum EscChar {
478        /// Return a backslash then a character.
479        Backslash(char),
480        /// Return a character.
481        Literal(char),
482        /// Return nothing.
483        Done,
484    }
485    impl EscChar {
486        /// Create an iterator to escape one character.
487        fn new(ch: char, in_key: bool) -> Self {
488            match ch {
489                '\\' | ';' => EscChar::Backslash(ch),
490                '=' if in_key => EscChar::Backslash(ch),
491                _ => EscChar::Literal(ch),
492            }
493        }
494    }
495    impl Iterator for EscChar {
496        type Item = char;
497
498        fn next(&mut self) -> Option<Self::Item> {
499            match *self {
500                EscChar::Backslash(ch) => {
501                    *self = EscChar::Literal(ch);
502                    Some('\\')
503                }
504                EscChar::Literal(ch) => {
505                    *self = EscChar::Done;
506                    Some(ch)
507                }
508                EscChar::Done => None,
509            }
510        }
511    }
512
513    /// escape a key or value string.
514    fn esc(s: &str, in_key: bool) -> impl Iterator<Item = char> + '_ {
515        s.chars().flat_map(move |c| EscChar::new(c, in_key))
516    }
517
518    let mut result = String::new();
519    for (k, v) in settings {
520        result.extend(esc(k, true));
521        result.push('=');
522        result.extend(esc(v, false));
523        result.push(';');
524    }
525    result.pop(); // remove the final ';' if any. Yes this is ugly.
526
527    result
528}
529
530/// Transform a string into a representation that can be sent as SOCKS
531/// authentication.
532// NOTE(eta): I am very unsure of the logic in here.
533#[cfg(feature = "pt-client")]
534pub fn settings_to_protocol(vers: SocksVersion, s: String) -> Result<Protocol, ProxyError> {
535    let mut bytes: Vec<_> = s.into();
536    Ok(if bytes.is_empty() {
537        Protocol::Socks(vers, SocksAuth::NoAuth)
538    } else if vers == SocksVersion::V4 {
539        if bytes.contains(&0) {
540            return Err(ProxyError::InvalidSocksRequest(
541                tor_socksproto::Error::NotImplemented(
542                    "SOCKS 4 doesn't support internal NUL bytes (for PT settings list)".into(),
543                ),
544            ));
545        } else {
546            Protocol::Socks(SocksVersion::V4, SocksAuth::Socks4(bytes))
547        }
548    } else if bytes.len() <= 255 {
549        // The [0] here is mandatory according to the pt-spec.
550        Protocol::Socks(SocksVersion::V5, SocksAuth::Username(bytes, vec![0]))
551    } else if bytes.len() <= (255 * 2) {
552        let password = bytes.split_off(255);
553        Protocol::Socks(SocksVersion::V5, SocksAuth::Username(bytes, password))
554    } else {
555        return Err(ProxyError::InvalidSocksRequest(
556            tor_socksproto::Error::NotImplemented("PT settings list too long for SOCKS 5".into()),
557        ));
558    })
559}
560
561#[cfg(test)]
562mod test {
563    // @@ begin test lint list maintained by maint/add_warning @@
564    #![allow(clippy::bool_assert_comparison)]
565    #![allow(clippy::clone_on_copy)]
566    #![allow(clippy::dbg_macro)]
567    #![allow(clippy::mixed_attributes_style)]
568    #![allow(clippy::print_stderr)]
569    #![allow(clippy::print_stdout)]
570    #![allow(clippy::single_char_pattern)]
571    #![allow(clippy::unwrap_used)]
572    #![allow(clippy::unchecked_time_subtraction)]
573    #![allow(clippy::useless_vec)]
574    #![allow(clippy::needless_pass_by_value)]
575    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
576    #[allow(unused_imports)]
577    use super::*;
578
579    #[test]
580    fn protocol_debug_redacts_http_connect_auth() {
581        let proto = Protocol::HttpConnect {
582            auth: Some((
583                Sensitive::new("user_name".to_owned()),
584                Sensitive::new("pass_word".to_owned()),
585            )),
586        };
587
588        let formatted = format!("{proto:?}");
589        assert!(formatted.contains("HttpConnect"));
590        assert!(!formatted.contains("user_name"));
591        assert!(!formatted.contains("pass_word"));
592    }
593
594    #[cfg(feature = "pt-client")]
595    #[test]
596    fn setting_encoding() {
597        fn check(settings: Vec<(&str, &str)>, expected: &str) {
598            assert_eq!(encode_settings(settings.into_iter()), expected);
599        }
600
601        // Easy cases, no escapes.
602        check(vec![], "");
603        check(vec![("hello", "world")], "hello=world");
604        check(
605            vec![("hey", "verden"), ("hello", "world")],
606            "hey=verden;hello=world",
607        );
608        check(
609            vec![("hey", "verden"), ("hello", "world"), ("selv", "tak")],
610            "hey=verden;hello=world;selv=tak",
611        );
612
613        check(
614            vec![("semi;colon", "equals=sign")],
615            r"semi\;colon=equals=sign",
616        );
617        check(
618            vec![("equals=sign", "semi;colon")],
619            r"equals\=sign=semi\;colon",
620        );
621        check(
622            vec![("semi;colon", "equals=sign"), ("also", "back\\slash")],
623            r"semi\;colon=equals=sign;also=back\\slash",
624        );
625    }
626
627    #[cfg(feature = "pt-client")]
628    #[test]
629    fn split_settings() {
630        use SocksVersion::*;
631        let long_string = "examplestrg".to_owned().repeat(50);
632        assert_eq!(long_string.len(), 550);
633        let sv = |v, a, b| settings_to_protocol(v, long_string[a..b].to_owned()).unwrap();
634        let s = |a, b| sv(V5, a, b);
635        let v = |a, b| long_string.as_bytes()[a..b].to_vec();
636
637        assert_eq!(s(0, 0), Protocol::Socks(V5, SocksAuth::NoAuth));
638        assert_eq!(
639            s(0, 50),
640            Protocol::Socks(V5, SocksAuth::Username(v(0, 50), vec![0]))
641        );
642        assert_eq!(
643            s(0, 255),
644            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), vec![0]))
645        );
646        assert_eq!(
647            s(0, 256),
648            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 256)))
649        );
650        assert_eq!(
651            s(0, 300),
652            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 300)))
653        );
654        assert_eq!(
655            s(0, 510),
656            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 510)))
657        );
658
659        // This one needs to use socks4, or it won't fit. :P
660        assert_eq!(
661            sv(V4, 0, 511),
662            Protocol::Socks(V4, SocksAuth::Socks4(v(0, 511)))
663        );
664
665        // Small requests with "0" bytes work fine...
666        assert_eq!(
667            settings_to_protocol(V5, "\0".to_owned()).unwrap(),
668            Protocol::Socks(V5, SocksAuth::Username(vec![0], vec![0]))
669        );
670        assert_eq!(
671            settings_to_protocol(V5, "\0".to_owned().repeat(510)).unwrap(),
672            Protocol::Socks(V5, SocksAuth::Username(vec![0; 255], vec![0; 255]))
673        );
674
675        // Huge requests with "0" simply can't be encoded.
676        assert!(settings_to_protocol(V5, "\0".to_owned().repeat(511)).is_err());
677
678        // Huge requests without "0" can't be encoded as V5
679        assert!(settings_to_protocol(V5, long_string[0..512].to_owned()).is_err());
680
681        // Requests with "0" can't be encoded as V4.
682        assert!(settings_to_protocol(V4, "\0".to_owned()).is_err());
683    }
684
685    #[test]
686    fn parse_http_connect_200_ok() {
687        let response = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
688        assert_eq!(parse_http_connect_response(response).unwrap(), 200);
689    }
690
691    #[test]
692    fn parse_http_connect_407_auth_required() {
693        let response = b"HTTP/1.1 407 Proxy Authentication Required\r\n\r\n";
694        match parse_http_connect_response(response) {
695            Err(ProxyError::HttpConnectError(407)) => (), // Expected
696            other => panic!("Expected 407 error, got {:?}", other),
697        }
698    }
699
700    #[test]
701    fn parse_http_connect_malformed_no_status() {
702        let response = b"INVALID HTTP";
703        assert!(matches!(
704            parse_http_connect_response(response),
705            Err(ProxyError::HttpConnectMalformed)
706        ));
707    }
708
709    #[test]
710    fn parse_http_connect_with_headers() {
711        let response = b"HTTP/1.1 200 Connection Established\r\nConnection: close\r\nProxy-Agent: Proxy/1.0\r\n\r\n";
712        assert_eq!(parse_http_connect_response(response).unwrap(), 200);
713    }
714
715    #[test]
716    fn parse_http_connect_rejects_pipelined_data() {
717        let response = b"HTTP/1.1 200 OK\r\n\r\nEXTRA_DATA";
718        assert!(matches!(
719            parse_http_connect_response(response),
720            Err(ProxyError::UnexpectedData)
721        ));
722    }
723
724    #[test]
725    fn parse_http_connect_oversized_headers() {
726        let huge_header = vec![b'X'; MAX_HTTP_HEADER_BYTES + 1];
727        assert!(matches!(
728            parse_http_connect_response(&huge_header),
729            Err(ProxyError::HttpConnectMalformed)
730        ));
731    }
732}