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 safelog::sensitive as sv;
39
#[cfg(feature = "pt-client")]
40
use tor_error::bad_api_usage;
41
#[cfg(feature = "pt-client")]
42
use tor_linkspec::{ChannelMethod, HasChanMethod, OwnedChanTarget};
43
#[cfg(feature = "pt-client")]
44
use tor_proto::peer::PeerAddr;
45

            
46
/// Information about what proxy protocol to use, and how to use it.
47
#[derive(Clone, Debug, Eq, PartialEq)]
48
#[non_exhaustive]
49
pub enum Protocol {
50
    /// Connect via SOCKS 4, SOCKS 4a, or SOCKS 5.
51
    Socks(SocksVersion, SocksAuth),
52
    /// Connect via HTTP CONNECT proxy (RFC 7231).
53
    HttpConnect {
54
        /// Optional Basic auth credentials (username, password) for Proxy-Authorization header.
55
        auth: Option<(Sensitive<String>, Sensitive<String>)>,
56
    },
57
}
58

            
59
/// An address to use when told to connect to "no address."
60
const NO_ADDR: IpAddr = IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 1));
61
/// Maximum number of bytes allowed in HTTP response headers from proxy.
62
const MAX_HTTP_HEADER_BYTES: usize = 16 * 1024;
63

            
64
/// Open a connection to `target` via the proxy at `proxy`, using the protocol
65
/// at `protocol`.
66
///
67
/// # Limitations
68
///
69
/// We will give an error if the proxy sends us any data on the connection along
70
/// with its final handshake: due to our implementation, any such data will be
71
/// discarded, and so we give an error rather than fail silently.
72
///
73
/// This limitation doesn't matter when the underlying protocol is Tor, or
74
/// anything else where the initiator is expected to speak before the responder
75
/// says anything.  To lift it, we would have to make this function's return
76
/// type become something buffered.
77
//
78
// TODO: Perhaps we should refactor this someday so it can be a general-purpose
79
// proxy function, not only for Arti.
80
pub(crate) async fn connect_via_proxy<R: NetStreamProvider + Send + Sync>(
81
    runtime: &R,
82
    proxy: &SocketAddr,
83
    protocol: &Protocol,
84
    target: &PtTargetAddr,
85
) -> Result<R::Stream, ProxyError> {
86
    trace!(
87
        "Launching a proxied connection to {} via proxy at {} using {:?}",
88
        target, proxy, protocol
89
    );
90
    // We don't (yet) use any custom options on the socket.
91
    let connect_options = Default::default();
92
    let stream = runtime
93
        .connect(proxy, &connect_options)
94
        .await
95
        .map_err(|e| ProxyError::ProxyConnect(Arc::new(e)))?;
96

            
97
    match protocol {
98
        Protocol::Socks(version, auth) => {
99
            do_socks_handshake::<R>(stream, version, auth, target).await
100
        }
101
        Protocol::HttpConnect { auth } => {
102
            do_http_connect_handshake::<R>(stream, auth, target).await
103
        }
104
    }
105
}
106

            
107
/// Perform SOCKS proxy handshake.
108
async fn do_socks_handshake<R: NetStreamProvider + Send + Sync>(
109
    mut stream: R::Stream,
110
    version: &SocksVersion,
111
    auth: &SocksAuth,
112
    target: &PtTargetAddr,
113
) -> Result<R::Stream, ProxyError> {
114
    let (target_addr, target_port): (SocksAddr, u16) = match target {
115
        PtTargetAddr::IpPort(a) => (SocksAddr::Ip(a.ip()), a.port()),
116
        #[cfg(feature = "pt-client")]
117
        PtTargetAddr::HostPort(host, port) => (
118
            SocksAddr::Hostname(
119
                host.clone()
120
                    .try_into()
121
                    .map_err(ProxyError::InvalidSocksAddr)?,
122
            ),
123
            *port,
124
        ),
125
        #[cfg(feature = "pt-client")]
126
        PtTargetAddr::None => (SocksAddr::Ip(NO_ADDR), 1),
127
        _ => return Err(ProxyError::UnrecognizedAddr),
128
    };
129

            
130
    let request = SocksRequest::new(
131
        *version,
132
        SocksCmd::CONNECT,
133
        target_addr,
134
        target_port,
135
        auth.clone(),
136
    )
137
    .map_err(ProxyError::InvalidSocksRequest)?;
138
    let mut handshake = SocksClientHandshake::new(request);
139

            
140
    let mut buf = tor_socksproto::Buffer::new();
141
    let reply = loop {
142
        use tor_socksproto::NextStep as NS;
143
        match handshake.step(&mut buf).map_err(ProxyError::SocksProto)? {
144
            NS::Send(send) => {
145
                stream.write_all(&send).await?;
146
                stream.flush().await?;
147
            }
148
            NS::Finished(fin) => {
149
                break fin
150
                    .into_output_forbid_pipelining()
151
                    .map_err(ProxyError::SocksProto)?;
152
            }
153
            NS::Recv(mut recv) => {
154
                let n = stream.read(recv.buf()).await?;
155
                recv.note_received(n).map_err(ProxyError::SocksProto)?;
156
            }
157
        }
158
    };
159

            
160
    let status = reply.status();
161
    trace!("SOCKS handshake succeeded, status {:?}", status);
162

            
163
    if status != SocksStatus::SUCCEEDED {
164
        return Err(ProxyError::SocksError(status));
165
    }
166

            
167
    Ok(stream)
168
}
169

            
170
/// Format target address for HTTP CONNECT request line and Host header.
171
fn format_connect_target(target: &PtTargetAddr) -> Result<String, ProxyError> {
172
    match target {
173
        PtTargetAddr::IpPort(a) => {
174
            let host = match a.ip() {
175
                IpAddr::V4(ip) => ip.to_string(),
176
                IpAddr::V6(ip) => format!("[{}]", ip),
177
            };
178
            Ok(format!("{}:{}", host, a.port()))
179
        }
180
        #[cfg(feature = "pt-client")]
181
        PtTargetAddr::HostPort(host, port) => Ok(format!("{}:{}", host, port)),
182
        #[cfg(feature = "pt-client")]
183
        PtTargetAddr::None => Err(ProxyError::UnrecognizedAddr),
184
        _ => Err(ProxyError::UnrecognizedAddr),
185
    }
186
}
187

            
188
/// Build HTTP CONNECT request string with optional Basic auth.
189
fn build_http_connect_request(
190
    target_str: &str,
191
    auth: &Option<(Sensitive<String>, Sensitive<String>)>,
192
) -> String {
193
    // Build CONNECT request: CONNECT host:port HTTP/1.1\r\nHost: host:port\r\n[...]\r\n
194
    let mut request = format!(
195
        "CONNECT {} HTTP/1.1\r\nHost: {}\r\n",
196
        target_str, target_str
197
    );
198

            
199
    if let Some((user, pass)) = auth {
200
        // Proxy-Authorization: Basic base64(username:password) per RFC 7617
201
        let credentials = format!("{}:{}", user.as_ref(), pass.as_ref());
202
        let encoded = Base64::encode_string(credentials.as_bytes());
203
        request.push_str(&format!("Proxy-Authorization: Basic {}\r\n", encoded));
204
    }
205

            
206
    request.push_str("\r\n");
207
    request
208
}
209

            
210
/// Parse HTTP CONNECT response and extract status code.
211
///
212
/// Uses httparse for spec-compliant parsing. Rejects:
213
/// - Responses larger than 16KB (prevents header bomb attacks)
214
/// - Pipelined data after headers (connection is dedicated to tunnel)
215
6
fn parse_http_connect_response(response_bytes: &[u8]) -> Result<u16, ProxyError> {
216
6
    if response_bytes.len() > MAX_HTTP_HEADER_BYTES {
217
1
        return Err(ProxyError::HttpConnectMalformed);
218
5
    }
219

            
220
5
    let mut headers = [httparse::EMPTY_HEADER; 64];
221
5
    let mut resp = httparse::Response::new(&mut headers);
222

            
223
5
    match resp.parse(response_bytes) {
224
4
        Ok(httparse::Status::Complete(header_end)) => {
225
4
            let status = resp.code.ok_or(ProxyError::HttpConnectMalformed)?;
226

            
227
4
            if !(200..300).contains(&status) {
228
1
                return Err(ProxyError::HttpConnectError(status));
229
3
            }
230

            
231
            // Reject any pipelined data after headers
232
3
            if header_end < response_bytes.len() {
233
1
                return Err(ProxyError::UnexpectedData);
234
2
            }
235

            
236
2
            trace!("HTTP CONNECT successful, status {}", status);
237
2
            Ok(status)
238
        }
239
        Ok(httparse::Status::Partial) => Err(ProxyError::HttpConnectMalformed),
240
1
        Err(_) => Err(ProxyError::HttpConnectMalformed),
241
    }
242
6
}
243

            
244
/// Send HTTP CONNECT request to proxy.
245
async fn send_http_connect_request<R: NetStreamProvider + Send + Sync>(
246
    stream: &mut R::Stream,
247
    auth: &Option<(Sensitive<String>, Sensitive<String>)>,
248
    target_str: &str,
249
) -> Result<(), ProxyError> {
250
    let request = build_http_connect_request(target_str, auth);
251
    trace!("Sending HTTP CONNECT request for {}", target_str);
252
    stream.write_all(request.as_bytes()).await?;
253
    stream.flush().await?;
254
    Ok(())
255
}
256

            
257
/// Perform HTTP CONNECT proxy handshake (RFC 7231, RFC 7617 for Basic auth).
258
async fn do_http_connect_handshake<R: NetStreamProvider + Send + Sync>(
259
    mut stream: R::Stream,
260
    auth: &Option<(Sensitive<String>, Sensitive<String>)>,
261
    target: &PtTargetAddr,
262
) -> Result<R::Stream, ProxyError> {
263
    let target_str = format_connect_target(target)?;
264
    send_http_connect_request::<R>(&mut stream, auth, &target_str).await?;
265

            
266
    // Read response until we see the double CRLF that terminates headers,
267
    // OR we reach MAX_HTTP_HEADER_BYTES.
268
    let mut response_buffer = Vec::new();
269
    let limit = MAX_HTTP_HEADER_BYTES as u64;
270
    let mut reader = BufReader::new(stream.take(limit));
271
    let mut line = String::new();
272

            
273
    loop {
274
        line.clear();
275
        let n = reader.read_line(&mut line).await?;
276
        if n == 0 {
277
            // We got an EOF (either from take(), or from the underlying stream),
278
            // before we could finish reading the headers.
279
            return Err(ProxyError::HttpConnectMalformed);
280
        }
281

            
282
        response_buffer.extend_from_slice(line.as_bytes());
283

            
284
        // Check for blank line (end of headers)
285
        if line == "\r\n" || line == "\n" {
286
            break;
287
        }
288
    }
289

            
290
    if !reader.buffer().is_empty() {
291
        // We have extraneous data after the HTTP response header; it is about to be discarded,
292
        // which would be an error.
293
        //
294
        // LIMITATION: Technically, this might be data from the Tor relay we're contacting.
295
        // However, we only use this method for negotiating Tor channels, which use TLS.
296
        // In TLS, the responder is supposed to be silent until the initiator says something,
297
        // so if we do have data here, then it is a TLS protocol violation.
298
        return Err(ProxyError::HttpConnectMalformed);
299
    }
300

            
301
    // Parse and validate response
302
    let _status_code = parse_http_connect_response(&response_buffer)?;
303

            
304
    // Return the underlying stream (BufReader is dropped, Take is dropped, stream continues)
305
    Ok(reader.into_inner().into_inner())
306
}
307

            
308
/// An error that occurs while negotiating a connection with a proxy.
309
#[derive(Clone, Debug, thiserror::Error)]
310
#[non_exhaustive]
311
pub enum ProxyError {
312
    /// We had an IO error while trying to open a connection to the proxy.
313
    #[error("Problem while connecting to proxy")]
314
    ProxyConnect(#[source] Arc<std::io::Error>),
315

            
316
    /// We had an IO error while talking to the proxy.
317
    #[error("Problem while communicating with proxy")]
318
    ProxyIo(#[source] Arc<std::io::Error>),
319

            
320
    /// We tried to use an address which socks doesn't support.
321
    #[error("SOCKS proxy does not support target address")]
322
    InvalidSocksAddr(#[source] tor_socksproto::Error),
323

            
324
    /// We tried to use an address type which _we_ don't recognize.
325
    #[error("Got an address type we don't recognize")]
326
    UnrecognizedAddr,
327

            
328
    /// Our SOCKS implementation told us that this request cannot be encoded.
329
    #[error("Tried to make an invalid SOCKS request")]
330
    InvalidSocksRequest(#[source] tor_socksproto::Error),
331

            
332
    /// The peer refused our request, or spoke SOCKS incorrectly.
333
    #[error("Protocol error while communicating with SOCKS proxy")]
334
    SocksProto(#[source] tor_socksproto::Error),
335

            
336
    /// We encountered an internal programming error.
337
    #[error("Internal error")]
338
    Bug(#[from] tor_error::Bug),
339

            
340
    /// We got extra data immediately after our handshake, before we actually
341
    /// sent anything.
342
    ///
343
    /// This is not a bug in the calling code or in the peer protocol: it just
344
    /// means that the remote peer sent us data before we actually sent it any
345
    /// data. Unfortunately, there's a limitation in our code that makes it
346
    /// discard any such data, and therefore we have to give this error to
347
    /// prevent bugs.
348
    ///
349
    /// We could someday remove this limitation.
350
    #[error("Received unexpected early data from peer")]
351
    UnexpectedData,
352

            
353
    /// The proxy told us that our attempt failed.
354
    #[error("SOCKS proxy reported an error: {0}")]
355
    SocksError(SocksStatus),
356

            
357
    /// HTTP CONNECT proxy returned a non-2xx status code.
358
    #[error("HTTP CONNECT proxy returned status: {0}")]
359
    HttpConnectError(u16),
360

            
361
    /// HTTP CONNECT proxy returned a malformed response.
362
    #[error("HTTP CONNECT proxy returned invalid response")]
363
    HttpConnectMalformed,
364
}
365

            
366
impl From<std::io::Error> for ProxyError {
367
    fn from(e: std::io::Error) -> Self {
368
        ProxyError::ProxyIo(Arc::new(e))
369
    }
370
}
371

            
372
impl From<ProxyError> for std::io::Error {
373
    fn from(e: ProxyError) -> Self {
374
        std::io::Error::other(e)
375
    }
376
}
377

            
378
impl tor_error::HasKind for ProxyError {
379
    fn kind(&self) -> tor_error::ErrorKind {
380
        use ProxyError as E;
381
        use tor_error::ErrorKind as EK;
382
        match self {
383
            E::ProxyConnect(_) | E::ProxyIo(_) => EK::LocalNetworkError,
384
            E::InvalidSocksAddr(_) | E::InvalidSocksRequest(_) => EK::BadApiUsage,
385
            E::UnrecognizedAddr => EK::NotImplemented,
386
            E::SocksProto(_) => EK::LocalProtocolViolation,
387
            E::Bug(e) => e.kind(),
388
            E::UnexpectedData => EK::NotImplemented,
389
            E::SocksError(_) => EK::LocalProtocolViolation,
390
            E::HttpConnectError(_) | E::HttpConnectMalformed => EK::LocalProtocolViolation,
391
        }
392
    }
393
}
394

            
395
impl tor_error::HasRetryTime for ProxyError {
396
    fn retry_time(&self) -> tor_error::RetryTime {
397
        use ProxyError as E;
398
        use SocksStatus as S;
399
        use tor_error::RetryTime as RT;
400
        match self {
401
            E::ProxyConnect(_) | E::ProxyIo(_) => RT::AfterWaiting,
402
            E::InvalidSocksAddr(_) => RT::Never,
403
            E::UnrecognizedAddr => RT::Never,
404
            E::InvalidSocksRequest(_) => RT::Never,
405
            E::SocksProto(_) => RT::AfterWaiting,
406
            E::Bug(_) => RT::Never,
407
            E::UnexpectedData => RT::Never,
408
            E::SocksError(e) => match *e {
409
                S::CONNECTION_REFUSED
410
                | S::GENERAL_FAILURE
411
                | S::HOST_UNREACHABLE
412
                | S::NETWORK_UNREACHABLE
413
                | S::TTL_EXPIRED => RT::AfterWaiting,
414
                _ => RT::Never,
415
            },
416
            E::HttpConnectError(code) => {
417
                // 502/503/504 may be transient; auth and policy errors are not.
418
                if *code == 502 || *code == 503 || *code == 504 {
419
                    RT::AfterWaiting
420
                } else {
421
                    RT::Never
422
                }
423
            }
424
            E::HttpConnectMalformed => RT::Never,
425
        }
426
    }
427
}
428

            
429
#[cfg(feature = "pt-client")]
430
/// An object that connects to a Tor bridge via an external pluggable transport
431
/// that provides a proxy.
432
#[derive(Clone, Debug)]
433
pub struct ExternalProxyPlugin<R> {
434
    /// The runtime to use for connections.
435
    runtime: R,
436
    /// The location of the proxy.
437
    proxy_addr: SocketAddr,
438
    /// The SOCKS protocol version to use.
439
    proxy_version: SocksVersion,
440
}
441

            
442
#[cfg(feature = "pt-client")]
443
impl<R: NetStreamProvider + Send + Sync> ExternalProxyPlugin<R> {
444
    /// Make a new `ExternalProxyPlugin`.
445
    pub fn new(rt: R, proxy_addr: SocketAddr, proxy_version: SocksVersion) -> Self {
446
        Self {
447
            runtime: rt,
448
            proxy_addr,
449
            proxy_version,
450
        }
451
    }
452
}
453

            
454
#[cfg(feature = "pt-client")]
455
#[async_trait]
456
impl<R: NetStreamProvider + Send + Sync> TransportImplHelper for ExternalProxyPlugin<R> {
457
    type Stream = R::Stream;
458

            
459
    async fn connect(&self, target: &OwnedChanTarget) -> crate::Result<(PeerAddr, R::Stream)> {
460
        let pt_target = match target.chan_method() {
461
            ChannelMethod::Direct(_) => {
462
                return Err(crate::Error::UnusableTarget(bad_api_usage!(
463
                    "Used pluggable transport for a TCP connection."
464
                )));
465
            }
466
            ChannelMethod::Pluggable(target) => target,
467
            other => {
468
                return Err(crate::Error::UnusableTarget(bad_api_usage!(
469
                    "Used unknown, unsupported, transport {:?} for a TCP connection.",
470
                    other,
471
                )));
472
            }
473
        };
474

            
475
        let into_err = |e: ProxyError| crate::Error::Connect {
476
            addresses: vec![(sv(pt_target.to_string()), e.into())],
477
        };
478
        let protocol =
479
            settings_to_protocol(self.proxy_version, encode_settings(pt_target.settings()))
480
                .map_err(into_err)?;
481
        let stream =
482
            connect_via_proxy(&self.runtime, &self.proxy_addr, &protocol, pt_target.addr())
483
                .await
484
                .map_err(into_err)?;
485

            
486
        Ok((pt_target.into(), stream))
487
    }
488
}
489

            
490
/// Encode the PT settings from `IT` in a format that a pluggable transport can use.
491
#[cfg(feature = "pt-client")]
492
7
fn encode_settings<'a, IT>(settings: IT) -> String
493
7
where
494
7
    IT: Iterator<Item = (&'a str, &'a str)>,
495
{
496
    /// Escape a character in the way expected by pluggable transports.
497
    ///
498
    /// This escape machinery is a mirror of that in the standard library.
499
    enum EscChar {
500
        /// Return a backslash then a character.
501
        Backslash(char),
502
        /// Return a character.
503
        Literal(char),
504
        /// Return nothing.
505
        Done,
506
    }
507
    impl EscChar {
508
        /// Create an iterator to escape one character.
509
132
        fn new(ch: char, in_key: bool) -> Self {
510
3
            match ch {
511
4
                '\\' | ';' => EscChar::Backslash(ch),
512
1
                '=' if in_key => EscChar::Backslash(ch),
513
127
                _ => EscChar::Literal(ch),
514
            }
515
132
        }
516
    }
517
    impl Iterator for EscChar {
518
        type Item = char;
519

            
520
269
        fn next(&mut self) -> Option<Self::Item> {
521
269
            match *self {
522
5
                EscChar::Backslash(ch) => {
523
5
                    *self = EscChar::Literal(ch);
524
5
                    Some('\\')
525
                }
526
132
                EscChar::Literal(ch) => {
527
132
                    *self = EscChar::Done;
528
132
                    Some(ch)
529
                }
530
132
                EscChar::Done => None,
531
            }
532
269
        }
533
    }
534

            
535
    /// escape a key or value string.
536
20
    fn esc(s: &str, in_key: bool) -> impl Iterator<Item = char> + '_ {
537
132
        s.chars().flat_map(move |c| EscChar::new(c, in_key))
538
20
    }
539

            
540
7
    let mut result = String::new();
541
10
    for (k, v) in settings {
542
10
        result.extend(esc(k, true));
543
10
        result.push('=');
544
10
        result.extend(esc(v, false));
545
10
        result.push(';');
546
10
    }
547
7
    result.pop(); // remove the final ';' if any. Yes this is ugly.
548

            
549
7
    result
550
7
}
551

            
552
/// Transform a string into a representation that can be sent as SOCKS
553
/// authentication.
554
// NOTE(eta): I am very unsure of the logic in here.
555
#[cfg(feature = "pt-client")]
556
12
pub fn settings_to_protocol(vers: SocksVersion, s: String) -> Result<Protocol, ProxyError> {
557
12
    let mut bytes: Vec<_> = s.into();
558
12
    Ok(if bytes.is_empty() {
559
1
        Protocol::Socks(vers, SocksAuth::NoAuth)
560
11
    } else if vers == SocksVersion::V4 {
561
2
        if bytes.contains(&0) {
562
1
            return Err(ProxyError::InvalidSocksRequest(
563
1
                tor_socksproto::Error::NotImplemented(
564
1
                    "SOCKS 4 doesn't support internal NUL bytes (for PT settings list)".into(),
565
1
                ),
566
1
            ));
567
        } else {
568
1
            Protocol::Socks(SocksVersion::V4, SocksAuth::Socks4(bytes))
569
        }
570
9
    } else if bytes.len() <= 255 {
571
        // The [0] here is mandatory according to the pt-spec.
572
3
        Protocol::Socks(SocksVersion::V5, SocksAuth::Username(bytes, vec![0]))
573
6
    } else if bytes.len() <= (255 * 2) {
574
4
        let password = bytes.split_off(255);
575
4
        Protocol::Socks(SocksVersion::V5, SocksAuth::Username(bytes, password))
576
    } else {
577
2
        return Err(ProxyError::InvalidSocksRequest(
578
2
            tor_socksproto::Error::NotImplemented("PT settings list too long for SOCKS 5".into()),
579
2
        ));
580
    })
581
12
}
582

            
583
#[cfg(test)]
584
mod test {
585
    // @@ begin test lint list maintained by maint/add_warning @@
586
    #![allow(clippy::bool_assert_comparison)]
587
    #![allow(clippy::clone_on_copy)]
588
    #![allow(clippy::dbg_macro)]
589
    #![allow(clippy::mixed_attributes_style)]
590
    #![allow(clippy::print_stderr)]
591
    #![allow(clippy::print_stdout)]
592
    #![allow(clippy::single_char_pattern)]
593
    #![allow(clippy::unwrap_used)]
594
    #![allow(clippy::unchecked_time_subtraction)]
595
    #![allow(clippy::useless_vec)]
596
    #![allow(clippy::needless_pass_by_value)]
597
    #![allow(clippy::string_slice)] // See arti#2571
598
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
599
    #[allow(unused_imports)]
600
    use super::*;
601

            
602
    #[test]
603
    fn protocol_debug_redacts_http_connect_auth() {
604
        let proto = Protocol::HttpConnect {
605
            auth: Some((
606
                Sensitive::new("user_name".to_owned()),
607
                Sensitive::new("pass_word".to_owned()),
608
            )),
609
        };
610

            
611
        let formatted = format!("{proto:?}");
612
        assert!(formatted.contains("HttpConnect"));
613
        assert!(!formatted.contains("user_name"));
614
        assert!(!formatted.contains("pass_word"));
615
    }
616

            
617
    #[cfg(feature = "pt-client")]
618
    #[test]
619
    fn setting_encoding() {
620
        fn check(settings: Vec<(&str, &str)>, expected: &str) {
621
            assert_eq!(encode_settings(settings.into_iter()), expected);
622
        }
623

            
624
        // Easy cases, no escapes.
625
        check(vec![], "");
626
        check(vec![("hello", "world")], "hello=world");
627
        check(
628
            vec![("hey", "verden"), ("hello", "world")],
629
            "hey=verden;hello=world",
630
        );
631
        check(
632
            vec![("hey", "verden"), ("hello", "world"), ("selv", "tak")],
633
            "hey=verden;hello=world;selv=tak",
634
        );
635

            
636
        check(
637
            vec![("semi;colon", "equals=sign")],
638
            r"semi\;colon=equals=sign",
639
        );
640
        check(
641
            vec![("equals=sign", "semi;colon")],
642
            r"equals\=sign=semi\;colon",
643
        );
644
        check(
645
            vec![("semi;colon", "equals=sign"), ("also", "back\\slash")],
646
            r"semi\;colon=equals=sign;also=back\\slash",
647
        );
648
    }
649

            
650
    #[cfg(feature = "pt-client")]
651
    #[test]
652
    fn split_settings() {
653
        use SocksVersion::*;
654
        let long_string = "examplestrg".to_owned().repeat(50);
655
        assert_eq!(long_string.len(), 550);
656
        let sv = |v, a, b| settings_to_protocol(v, long_string[a..b].to_owned()).unwrap();
657
        let s = |a, b| sv(V5, a, b);
658
        let v = |a, b| long_string.as_bytes()[a..b].to_vec();
659

            
660
        assert_eq!(s(0, 0), Protocol::Socks(V5, SocksAuth::NoAuth));
661
        assert_eq!(
662
            s(0, 50),
663
            Protocol::Socks(V5, SocksAuth::Username(v(0, 50), vec![0]))
664
        );
665
        assert_eq!(
666
            s(0, 255),
667
            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), vec![0]))
668
        );
669
        assert_eq!(
670
            s(0, 256),
671
            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 256)))
672
        );
673
        assert_eq!(
674
            s(0, 300),
675
            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 300)))
676
        );
677
        assert_eq!(
678
            s(0, 510),
679
            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 510)))
680
        );
681

            
682
        // This one needs to use socks4, or it won't fit. :P
683
        assert_eq!(
684
            sv(V4, 0, 511),
685
            Protocol::Socks(V4, SocksAuth::Socks4(v(0, 511)))
686
        );
687

            
688
        // Small requests with "0" bytes work fine...
689
        assert_eq!(
690
            settings_to_protocol(V5, "\0".to_owned()).unwrap(),
691
            Protocol::Socks(V5, SocksAuth::Username(vec![0], vec![0]))
692
        );
693
        assert_eq!(
694
            settings_to_protocol(V5, "\0".to_owned().repeat(510)).unwrap(),
695
            Protocol::Socks(V5, SocksAuth::Username(vec![0; 255], vec![0; 255]))
696
        );
697

            
698
        // Huge requests with "0" simply can't be encoded.
699
        assert!(settings_to_protocol(V5, "\0".to_owned().repeat(511)).is_err());
700

            
701
        // Huge requests without "0" can't be encoded as V5
702
        assert!(settings_to_protocol(V5, long_string[0..512].to_owned()).is_err());
703

            
704
        // Requests with "0" can't be encoded as V4.
705
        assert!(settings_to_protocol(V4, "\0".to_owned()).is_err());
706
    }
707

            
708
    #[test]
709
    fn parse_http_connect_200_ok() {
710
        let response = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
711
        assert_eq!(parse_http_connect_response(response).unwrap(), 200);
712
    }
713

            
714
    #[test]
715
    fn parse_http_connect_407_auth_required() {
716
        let response = b"HTTP/1.1 407 Proxy Authentication Required\r\n\r\n";
717
        match parse_http_connect_response(response) {
718
            Err(ProxyError::HttpConnectError(407)) => (), // Expected
719
            other => panic!("Expected 407 error, got {:?}", other),
720
        }
721
    }
722

            
723
    #[test]
724
    fn parse_http_connect_malformed_no_status() {
725
        let response = b"INVALID HTTP";
726
        assert!(matches!(
727
            parse_http_connect_response(response),
728
            Err(ProxyError::HttpConnectMalformed)
729
        ));
730
    }
731

            
732
    #[test]
733
    fn parse_http_connect_with_headers() {
734
        let response = b"HTTP/1.1 200 Connection Established\r\nConnection: close\r\nProxy-Agent: Proxy/1.0\r\n\r\n";
735
        assert_eq!(parse_http_connect_response(response).unwrap(), 200);
736
    }
737

            
738
    #[test]
739
    fn parse_http_connect_rejects_pipelined_data() {
740
        let response = b"HTTP/1.1 200 OK\r\n\r\nEXTRA_DATA";
741
        assert!(matches!(
742
            parse_http_connect_response(response),
743
            Err(ProxyError::UnexpectedData)
744
        ));
745
    }
746

            
747
    #[test]
748
    fn parse_http_connect_oversized_headers() {
749
        let huge_header = vec![b'X'; MAX_HTTP_HEADER_BYTES + 1];
750
        assert!(matches!(
751
            parse_http_connect_response(&huge_header),
752
            Err(ProxyError::HttpConnectMalformed)
753
        ));
754
    }
755
}