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

            
15
use std::{
16
    net::{IpAddr, SocketAddr},
17
    sync::Arc,
18
};
19

            
20
use base64ct::{Base64, Encoding};
21
use futures::io::{AsyncBufReadExt, BufReader};
22
use futures::{AsyncReadExt, AsyncWriteExt};
23
use httparse;
24
use safelog::Sensitive;
25
use tor_linkspec::PtTargetAddr;
26
use tor_rtcompat::NetStreamProvider;
27
use tor_socksproto::{
28
    Handshake as _, SocksAddr, SocksAuth, SocksClientHandshake, SocksCmd, SocksRequest,
29
    SocksStatus, SocksVersion,
30
};
31
use tracing::trace;
32

            
33
#[cfg(feature = "pt-client")]
34
use super::TransportImplHelper;
35
#[cfg(feature = "pt-client")]
36
use async_trait::async_trait;
37
#[cfg(feature = "pt-client")]
38
use tor_error::bad_api_usage;
39
#[cfg(feature = "pt-client")]
40
use tor_linkspec::{ChannelMethod, HasChanMethod, OwnedChanTarget};
41
#[cfg(feature = "pt-client")]
42
use 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]
47
pub 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."
58
const 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.
60
const 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.
78
pub(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.
104
async 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.
167
fn 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.
185
fn 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)
211
12
fn parse_http_connect_response(response_bytes: &[u8]) -> Result<u16, ProxyError> {
212
12
    if response_bytes.len() > MAX_HTTP_HEADER_BYTES {
213
2
        return Err(ProxyError::HttpConnectMalformed);
214
10
    }
215

            
216
10
    let mut headers = [httparse::EMPTY_HEADER; 64];
217
10
    let mut resp = httparse::Response::new(&mut headers);
218

            
219
10
    match resp.parse(response_bytes) {
220
8
        Ok(httparse::Status::Complete(header_end)) => {
221
8
            let status = resp.code.ok_or(ProxyError::HttpConnectMalformed)?;
222

            
223
8
            if !(200..300).contains(&status) {
224
2
                return Err(ProxyError::HttpConnectError(status));
225
6
            }
226

            
227
            // Reject any pipelined data after headers
228
6
            if header_end < response_bytes.len() {
229
2
                return Err(ProxyError::UnexpectedData);
230
4
            }
231

            
232
4
            trace!("HTTP CONNECT successful, status {}", status);
233
4
            Ok(status)
234
        }
235
        Ok(httparse::Status::Partial) => Err(ProxyError::HttpConnectMalformed),
236
2
        Err(_) => Err(ProxyError::HttpConnectMalformed),
237
    }
238
12
}
239

            
240
/// Send HTTP CONNECT request to proxy.
241
async 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).
254
async 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]
295
pub 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

            
350
impl From<std::io::Error> for ProxyError {
351
10
    fn from(e: std::io::Error) -> Self {
352
10
        ProxyError::ProxyIo(Arc::new(e))
353
10
    }
354
}
355

            
356
impl From<ProxyError> for std::io::Error {
357
2
    fn from(e: ProxyError) -> Self {
358
2
        std::io::Error::other(e)
359
2
    }
360
}
361

            
362
impl 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

            
379
impl 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)]
417
pub 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")]
427
impl<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]
440
impl<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")]
470
14
fn encode_settings<'a, IT>(settings: IT) -> String
471
14
where
472
14
    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
264
        fn new(ch: char, in_key: bool) -> Self {
488
6
            match ch {
489
8
                '\\' | ';' => EscChar::Backslash(ch),
490
2
                '=' if in_key => EscChar::Backslash(ch),
491
254
                _ => EscChar::Literal(ch),
492
            }
493
264
        }
494
    }
495
    impl Iterator for EscChar {
496
        type Item = char;
497

            
498
538
        fn next(&mut self) -> Option<Self::Item> {
499
538
            match *self {
500
10
                EscChar::Backslash(ch) => {
501
10
                    *self = EscChar::Literal(ch);
502
10
                    Some('\\')
503
                }
504
264
                EscChar::Literal(ch) => {
505
264
                    *self = EscChar::Done;
506
264
                    Some(ch)
507
                }
508
264
                EscChar::Done => None,
509
            }
510
538
        }
511
    }
512

            
513
    /// escape a key or value string.
514
40
    fn esc(s: &str, in_key: bool) -> impl Iterator<Item = char> + '_ {
515
284
        s.chars().flat_map(move |c| EscChar::new(c, in_key))
516
40
    }
517

            
518
14
    let mut result = String::new();
519
34
    for (k, v) in settings {
520
20
        result.extend(esc(k, true));
521
20
        result.push('=');
522
20
        result.extend(esc(v, false));
523
20
        result.push(';');
524
20
    }
525
14
    result.pop(); // remove the final ';' if any. Yes this is ugly.
526

            
527
14
    result
528
14
}
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")]
534
24
pub fn settings_to_protocol(vers: SocksVersion, s: String) -> Result<Protocol, ProxyError> {
535
24
    let mut bytes: Vec<_> = s.into();
536
24
    Ok(if bytes.is_empty() {
537
2
        Protocol::Socks(vers, SocksAuth::NoAuth)
538
22
    } else if vers == SocksVersion::V4 {
539
4
        if bytes.contains(&0) {
540
2
            return Err(ProxyError::InvalidSocksRequest(
541
2
                tor_socksproto::Error::NotImplemented(
542
2
                    "SOCKS 4 doesn't support internal NUL bytes (for PT settings list)".into(),
543
2
                ),
544
2
            ));
545
        } else {
546
2
            Protocol::Socks(SocksVersion::V4, SocksAuth::Socks4(bytes))
547
        }
548
18
    } else if bytes.len() <= 255 {
549
        // The [0] here is mandatory according to the pt-spec.
550
6
        Protocol::Socks(SocksVersion::V5, SocksAuth::Username(bytes, vec![0]))
551
12
    } else if bytes.len() <= (255 * 2) {
552
8
        let password = bytes.split_off(255);
553
8
        Protocol::Socks(SocksVersion::V5, SocksAuth::Username(bytes, password))
554
    } else {
555
4
        return Err(ProxyError::InvalidSocksRequest(
556
4
            tor_socksproto::Error::NotImplemented("PT settings list too long for SOCKS 5".into()),
557
4
        ));
558
    })
559
24
}
560

            
561
#[cfg(test)]
562
mod 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
}