Skip to main content

tor_dirclient/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3// @@ begin lint list maintained by maint/add_warning @@
4#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6#![warn(missing_docs)]
7#![warn(noop_method_call)]
8#![warn(unreachable_pub)]
9#![warn(clippy::all)]
10#![deny(clippy::await_holding_lock)]
11#![deny(clippy::cargo_common_metadata)]
12#![deny(clippy::cast_lossless)]
13#![deny(clippy::checked_conversions)]
14#![allow(clippy::cognitive_complexity)] // See arti#2556
15#![deny(clippy::debug_assert_with_mut_call)]
16#![deny(clippy::exhaustive_enums)]
17#![deny(clippy::exhaustive_structs)]
18#![deny(clippy::expl_impl_clone_on_copy)]
19#![deny(clippy::fallible_impl_from)]
20#![deny(clippy::implicit_clone)]
21#![deny(clippy::large_stack_arrays)]
22#![warn(clippy::manual_ok_or)]
23#![deny(clippy::missing_docs_in_private_items)]
24#![warn(clippy::needless_borrow)]
25#![warn(clippy::needless_pass_by_value)]
26#![warn(clippy::option_option)]
27#![deny(clippy::print_stderr)]
28#![deny(clippy::print_stdout)]
29#![warn(clippy::rc_buffer)]
30#![deny(clippy::ref_option_ref)]
31#![warn(clippy::semicolon_if_nothing_returned)]
32#![warn(clippy::trait_duplication_in_bounds)]
33#![deny(clippy::unchecked_time_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::mod_module_files)]
38#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39#![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43#![allow(clippy::needless_lifetimes)] // See arti#1765
44#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45#![allow(clippy::collapsible_if)] // See arti#2342
46#![deny(clippy::unused_async)]
47#![deny(clippy::string_slice)] // See arti#2571
48//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
49
50// TODO probably remove this at some point - see tpo/core/arti#1060
51#![cfg_attr(
52    not(all(feature = "full", feature = "experimental")),
53    allow(unused_imports)
54)]
55
56mod body;
57mod err;
58pub mod request;
59mod response;
60mod util;
61
62use tor_circmgr::{CircMgr, DirInfo};
63use tor_error::bad_api_usage;
64use tor_rtcompat::{Runtime, SleepProvider, SleepProviderExt};
65
66// Zlib is required; the others are optional.
67#[cfg(feature = "xz")]
68use async_compression::futures::bufread::XzDecoder;
69use async_compression::futures::bufread::ZlibDecoder;
70#[cfg(feature = "zstd")]
71use async_compression::futures::bufread::ZstdDecoder;
72
73use futures::FutureExt;
74use futures::io::{
75    AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader,
76};
77use std::sync::Arc;
78use std::time::Duration;
79use tracing::{info, instrument};
80
81pub use err::{Error, RequestError, RequestFailedError};
82pub use response::{DirResponse, SourceInfo};
83
84/// Type for results returned in this crate.
85pub type Result<T> = std::result::Result<T, Error>;
86
87/// Type for internal results  containing a RequestError.
88pub type RequestResult<T> = std::result::Result<T, RequestError>;
89
90/// Flag to declare whether a request is always anonymized or not.
91///
92/// This is used by tor-dirclient to control whether *other* deanonymizing metadata
93/// might be added to the request (eg in request headers):
94/// Some requests (like those to download onion service descriptors) are always
95/// anonymized, and should never be sent in a way that leaks information about
96/// our settings or configuration.
97///
98/// It is up to the *caller* of `tor-dirclient` to ensure that
99///
100///   - every request whose anonymization status is `AnonymizedRequest::Direct`
101///     is sent only over non-anonymous connections.
102///
103///     (Sending an `AnonymizedRequest::Direct` request over an anonymized connection
104///     would weaken the connection's anonymity, and can therefore weaken the anonymity
105///     of user traffic sharing the same circuit.)
106///
107///   - every request whose anonymization status is `AnonymizedRequest::Anonymized`
108///     is sent over only anonymous connections (ie, multi-hop circuits).
109///
110///     (Sending an `AnonymizedRequest::Anonymized` request over a direct connection
111///     would directly reveal user behaviour data to the directory server.)
112///
113/// TODO the calling code cannot easily be sure to get this right this because
114/// the anonymization status is a run-time property and the choice of connection kind
115/// is statically defined in the calling code.  (Perhaps this could be checked in tests?)
116#[derive(Copy, Clone, Debug, Eq, PartialEq)]
117#[non_exhaustive]
118pub enum AnonymizedRequest {
119    /// This request's content or semantics reveals or is correlated with sensitive information.
120    ///
121    /// For example, requests for hidden service descriptors reveal which hidden services
122    /// the client is connecting to.
123    ///
124    /// The request must be sent over an anonymous circuit by the caller
125    /// and no additional deanonymizing information should be added to it by `tor-dirclient`.
126    /// (For example, no client-version-specific information should be
127    /// sent in HTTP headers when the request is made.)
128    Anonymized,
129
130    /// Making this request does not reveal anything sensitive, nor any user behaviour.
131    ///
132    /// The request body is uncorrelated with such things as the websites the user might visit,
133    /// the onion services the user is visiting or running, etc.
134    ///
135    /// For example, requests for all router microdescriptors are made by all clients,
136    /// so which microdescriptor(s) are requested reveals nothing to any attacker.
137    ///
138    /// tor-dirclient is allowed to add include information about our capabilities
139    /// when sending this request.
140    /// The request must *not* be sent over an anonymous circuit by the caller
141    /// (at least, not one used for anything else).
142    Direct,
143}
144
145/// Fetch the resource described by `req` over the Tor network.
146///
147/// Circuits are built or found using `circ_mgr`, using paths
148/// constructed using `dirinfo`.
149///
150/// For more fine-grained control over the circuit and stream used,
151/// construct them yourself, and then call [`send_request`] instead.
152///
153/// # TODO
154///
155/// This is the only function in this crate that knows about CircMgr and
156/// DirInfo.  Perhaps this function should move up a level into DirMgr?
157#[instrument(level = "trace", skip_all)]
158pub async fn get_resource<CR, R, SP>(
159    req: &CR,
160    dirinfo: DirInfo<'_>,
161    runtime: &SP,
162    circ_mgr: Arc<CircMgr<R>>,
163) -> Result<DirResponse>
164where
165    CR: request::Requestable + ?Sized,
166    R: Runtime,
167    SP: SleepProvider,
168{
169    let tunnel = circ_mgr.get_or_launch_dir(dirinfo).await?;
170
171    if req.anonymized() == AnonymizedRequest::Anonymized {
172        return Err(bad_api_usage!("Tried to use get_resource for an anonymized request").into());
173    }
174
175    // TODO(nickm) This should be an option, and is too long.
176    let begin_timeout = Duration::from_secs(5);
177    let source = match SourceInfo::from_tunnel(&tunnel) {
178        Ok(source) => source,
179        Err(e) => {
180            return Err(Error::RequestFailed(RequestFailedError {
181                source: None,
182                error: e.into(),
183            }));
184        }
185    };
186
187    let wrap_err = |error| {
188        Error::RequestFailed(RequestFailedError {
189            source: source.clone(),
190            error,
191        })
192    };
193
194    req.check_circuit_skip_optimization(&tunnel)
195        .await
196        .map_err(wrap_err)?;
197
198    // Launch the stream.
199    let mut stream = runtime
200        .timeout(begin_timeout, tunnel.begin_dir_stream())
201        .await
202        .map_err(RequestError::from)
203        .map_err(wrap_err)?
204        .map_err(RequestError::from)
205        .map_err(wrap_err)?; // TODO(nickm) handle fatalities here too
206
207    // TODO: Perhaps we want separate timeouts for each phase of this.
208    // For now, we just use higher-level timeouts in `dirmgr`.
209    let r = send_request(runtime, req, &mut stream, source.clone()).await;
210
211    if should_retire_circ(&r) {
212        retire_circ(&circ_mgr, &tunnel.unique_id(), "Partial response");
213    }
214
215    r
216}
217
218/// Return true if `result` holds an error indicating that we should retire the
219/// circuit used for the corresponding request.
220fn should_retire_circ(result: &Result<DirResponse>) -> bool {
221    match result {
222        Err(e) => e.should_retire_circ(),
223        Ok(dr) => dr.error().map(RequestError::should_retire_circ) == Some(true),
224    }
225}
226
227/// Fetch a Tor directory object from a provided stream.
228#[deprecated(since = "0.8.1", note = "Use send_request instead.")]
229pub async fn download<R, S, SP>(
230    runtime: &SP,
231    req: &R,
232    stream: &mut S,
233    source: Option<SourceInfo>,
234) -> Result<DirResponse>
235where
236    R: request::Requestable + ?Sized,
237    S: AsyncRead + AsyncWrite + Send + Unpin,
238    SP: SleepProvider,
239{
240    send_request(runtime, req, stream, source).await
241}
242
243/// Fetch or upload a Tor directory object using the provided stream.
244///
245/// To do this, we send a simple HTTP/1.0 request for the described
246/// object in `req` over `stream`, and then wait for a response.  In
247/// log messages, we describe the origin of the data as coming from
248/// `source`.
249///
250/// # Notes
251///
252/// It's kind of bogus to have a 'source' field here at all; we may
253/// eventually want to remove it.
254///
255/// This function doesn't close the stream; you may want to do that
256/// yourself.
257///
258/// The only error variant returned is [`Error::RequestFailed`].
259// TODO: should the error return type change to `RequestFailedError`?
260// If so, that would simplify some code in_dirmgr::bridgedesc.
261pub async fn send_request<R, S, SP>(
262    runtime: &SP,
263    req: &R,
264    stream: &mut S,
265    source: Option<SourceInfo>,
266) -> Result<DirResponse>
267where
268    R: request::Requestable + ?Sized,
269    S: AsyncRead + AsyncWrite + Send + Unpin,
270    SP: SleepProvider,
271{
272    let wrap_err = |error| {
273        Error::RequestFailed(RequestFailedError {
274            source: source.clone(),
275            error,
276        })
277    };
278
279    let partial_ok = req.partial_response_body_ok();
280    let maxlen = req.max_response_len();
281    let anonymized = req.anonymized();
282    let req = req.make_request().map_err(wrap_err)?;
283    let method = req.method().clone();
284    let encoded = util::encode_request(&req);
285
286    // Write the request.
287    for chunk in encoded.iter() {
288        stream
289            .write_all(chunk)
290            .await
291            .map_err(RequestError::from)
292            .map_err(wrap_err)?;
293    }
294    stream
295        .flush()
296        .await
297        .map_err(RequestError::from)
298        .map_err(wrap_err)?;
299
300    let mut buffered = BufReader::new(stream);
301
302    // Handle the response
303    let header = read_headers(runtime, &mut buffered)
304        .await
305        .map_err(wrap_err)?;
306    if header.status != Some(200) {
307        return Ok(DirResponse::new(
308            method,
309            header.status.unwrap_or(0),
310            header.status_message,
311            None,
312            vec![],
313            source,
314        ));
315    }
316
317    let mut decoder =
318        get_decoder(buffered, header.encoding.as_deref(), anonymized).map_err(wrap_err)?;
319
320    let mut result = Vec::new();
321    let ok = read_and_decompress(runtime, &mut decoder, maxlen, &mut result).await;
322
323    let ok = match (partial_ok, ok, result.len()) {
324        (true, Err(e), n) if n > 0 => {
325            // Note that we _don't_ return here: we want the partial response.
326            Err(e)
327        }
328        (_, Err(e), _) => {
329            return Err(wrap_err(e));
330        }
331        (_, Ok(()), _) => Ok(()),
332    };
333
334    Ok(DirResponse::new(
335        method,
336        200,
337        None,
338        ok.err(),
339        result,
340        source,
341    ))
342}
343
344/// Maximum length for the HTTP headers in a single request or response.
345///
346/// Chosen more or less arbitrarily.
347const MAX_HEADERS_LEN: usize = 16384;
348
349/// Read and parse HTTP/1 headers from `stream`.
350///
351/// Wraps around [`read_headers_internal()`] with a timeout.
352async fn read_headers<S, SP>(runtime: &SP, stream: &mut S) -> RequestResult<HeaderStatus>
353where
354    S: AsyncBufRead + Unpin,
355    SP: SleepProvider,
356{
357    // Just like with the other timeouts in this code, this should probably be
358    // configurable and may even be too much.
359    let read_timeout = Duration::from_secs(10);
360    runtime
361        .timeout(read_timeout, read_headers_internal(stream))
362        .await
363        .map_err(|_| RequestError::DirTimeout)
364        .flatten()
365}
366
367/// Internal version of [`read_headers()`] without a timeout.
368async fn read_headers_internal<S>(stream: &mut S) -> RequestResult<HeaderStatus>
369where
370    S: AsyncBufRead + Unpin,
371{
372    let mut buf = Vec::with_capacity(1024);
373
374    // Note: Using `take` here will ensure that we will get an EOF if the header length
375    // would exceed the maximum.  No extra bytes will actually be extracted.
376    let mut limited_stream = stream.take(MAX_HEADERS_LEN as u64);
377
378    loop {
379        // TODO: it's inefficient to do this a line at a time; it would
380        // probably be better to read until the CRLF CRLF ending of the
381        // response.  But this should be fast enough.
382        let n = limited_stream.read_until(b'\n', &mut buf).await?;
383
384        // TODO(nickm): Better maximum and/or let this expand.
385        let mut headers = [httparse::EMPTY_HEADER; 32];
386        let mut response = httparse::Response::new(&mut headers);
387
388        match response.parse(&buf[..])? {
389            httparse::Status::Partial => {
390                // We didn't get a whole response; we may need to try again.
391
392                if n == 0 {
393                    // We hit an EOF; no more progress can be made.
394                    return Err(RequestError::TruncatedHeaders);
395                }
396
397                if buf.len() >= MAX_HEADERS_LEN {
398                    return Err(RequestError::HeadersTooLong(buf.len()));
399                }
400            }
401            httparse::Status::Complete(n_parsed) => {
402                if response.code != Some(200) {
403                    return Ok(HeaderStatus {
404                        status: response.code,
405                        status_message: response.reason.map(str::to_owned),
406                        encoding: None,
407                    });
408                }
409                let encoding = if let Some(enc) = response
410                    .headers
411                    .iter()
412                    .find(|h| h.name == "Content-Encoding")
413                {
414                    Some(String::from_utf8(enc.value.to_vec())?)
415                } else {
416                    None
417                };
418                /*
419                if let Some(clen) = response.headers.iter().find(|h| h.name == "Content-Length") {
420                    let clen = std::str::from_utf8(clen.value)?;
421                    length = Some(clen.parse()?);
422                }
423                 */
424                assert!(n_parsed == buf.len());
425                return Ok(HeaderStatus {
426                    status: Some(200),
427                    status_message: None,
428                    encoding,
429                });
430            }
431        }
432        if n == 0 {
433            return Err(RequestError::TruncatedHeaders);
434        }
435    }
436}
437
438/// Return value from read_headers
439#[derive(Debug, Clone)]
440struct HeaderStatus {
441    /// HTTP status code.
442    status: Option<u16>,
443    /// HTTP status message associated with the status code.
444    status_message: Option<String>,
445    /// The Content-Encoding header, if any.
446    encoding: Option<String>,
447}
448
449/// Helper: download directory information from `stream` and
450/// decompress it into a result buffer.  Assumes that `buf` is empty.
451///
452/// If we get more than maxlen bytes after decompression, give an error.
453///
454/// Returns the status of our download attempt, stores any data that
455/// we were able to download into `result`.  Existing contents of
456/// `result` are overwritten.
457async fn read_and_decompress<S, SP>(
458    runtime: &SP,
459    mut stream: S,
460    maxlen: usize,
461    result: &mut Vec<u8>,
462) -> RequestResult<()>
463where
464    S: AsyncRead + Unpin,
465    SP: SleepProvider,
466{
467    let buffer_window_size = 1024;
468    let mut written_total: usize = 0;
469    // TODO(nickm): This should be an option, and is maybe too long.
470    // Though for some users it may be too short?
471    let read_timeout = Duration::from_secs(10);
472    let timer = runtime.sleep(read_timeout).fuse();
473    futures::pin_mut!(timer);
474
475    loop {
476        // allocate buffer for next read
477        result.resize(written_total + buffer_window_size, 0);
478        let buf: &mut [u8] = &mut result[written_total..written_total + buffer_window_size];
479
480        let status = futures::select! {
481            status = stream.read(buf).fuse() => status,
482            _ = timer => {
483                result.resize(written_total, 0); // truncate as needed
484                return Err(RequestError::DirTimeout);
485            }
486        };
487        let written_in_this_loop = match status {
488            Ok(n) => n,
489            Err(other) => {
490                result.resize(written_total, 0); // truncate as needed
491                return Err(other.into());
492            }
493        };
494
495        written_total += written_in_this_loop;
496
497        // exit conditions below
498
499        if written_in_this_loop == 0 {
500            /*
501            in case we read less than `buffer_window_size` in last `read`
502            we need to shrink result because otherwise we'll return those
503            un-read 0s
504            */
505            if written_total < result.len() {
506                result.resize(written_total, 0);
507            }
508            return Ok(());
509        }
510
511        // TODO: It would be good to detect compression bombs, but
512        // that would require access to the internal stream, which
513        // would in turn require some tricky programming.  For now, we
514        // use the maximum length here to prevent an attacker from
515        // filling our RAM.
516        if written_total > maxlen {
517            result.resize(maxlen, 0);
518            return Err(RequestError::ResponseTooLong(written_total));
519        }
520    }
521}
522
523/// Retire a directory circuit because of an error we've encountered on it.
524fn retire_circ<R>(circ_mgr: &Arc<CircMgr<R>>, id: &tor_proto::circuit::UniqId, error: &str)
525where
526    R: Runtime,
527{
528    info!(
529        "{}: Retiring circuit because of directory failure: {}",
530        &id, &error
531    );
532    circ_mgr.retire_circ(id);
533}
534
535/// Memory limit of the LZMA dictionary we are willing to allocate.
536///
537/// Without it, it may lead to an allocation of 4GiB, which is a bit overkill.
538///
539/// Right now, we use 16 MiB, as the spec limits the compression quality to
540/// "6", meaning 9 MiB of decompression memory, which rounds up to 16 MiB in
541/// terms of base 2.
542///
543/// From the xz(1) manual page:
544/// ```text
545/// Preset   DictSize   CompCPU   CompMem   DecMem
546///  -0e     256 KiB       8        4 MiB    1 MiB
547///  -1e       1 MiB       8       13 MiB    2 MiB
548///  -2e       2 MiB       8       25 MiB    3 MiB
549///  -3e       4 MiB       7       48 MiB    5 MiB
550///  -4e       4 MiB       8       48 MiB    5 MiB
551///  -5e       8 MiB       7       94 MiB    9 MiB
552///  -6e       8 MiB       8       94 MiB    9 MiB
553///  -7e      16 MiB       8      186 MiB   17 MiB
554///  -8e      32 MiB       8      370 MiB   33 MiB
555///  -9e      64 MiB       8      674 MiB   65 MiB
556/// ```
557const LZMA_DICT_MEM_LIMIT: u64 = 1 << 24; // 16 MiB
558
559/// Helper: Return a boxed decoder object that wraps the stream  $s.
560macro_rules! decoder {
561    (XzDecoder, $s:expr) => {{
562        let mut decoder = XzDecoder::with_mem_limit($s, LZMA_DICT_MEM_LIMIT);
563        decoder.multiple_members(true);
564        Ok(Box::new(decoder))
565    }};
566    ($dec:ident, $s:expr) => {{
567        let mut decoder = $dec::new($s);
568        decoder.multiple_members(true);
569        Ok(Box::new(decoder))
570    }};
571}
572
573/// Wrap `stream` in an appropriate type to undo the content encoding
574/// as described in `encoding`.
575fn get_decoder<'a, S: AsyncBufRead + Unpin + Send + 'a>(
576    stream: S,
577    encoding: Option<&str>,
578    anonymized: AnonymizedRequest,
579) -> RequestResult<Box<dyn AsyncRead + Unpin + Send + 'a>> {
580    use AnonymizedRequest::Direct;
581    match (encoding, anonymized) {
582        (None | Some("identity"), _) => Ok(Box::new(stream)),
583        (Some("deflate"), _) => decoder!(ZlibDecoder, stream),
584        // We only admit to supporting these on a direct connection; otherwise,
585        // a hostile directory could send them back even though we hadn't
586        // requested them.
587        #[cfg(feature = "xz")]
588        (Some("x-tor-lzma"), Direct) => decoder!(XzDecoder, stream),
589        #[cfg(feature = "zstd")]
590        (Some("x-zstd"), Direct) => decoder!(ZstdDecoder, stream),
591        (Some(other), _) => Err(RequestError::ContentEncoding(other.into())),
592    }
593}
594
595#[cfg(test)]
596mod test {
597    // @@ begin test lint list maintained by maint/add_warning @@
598    #![allow(clippy::bool_assert_comparison)]
599    #![allow(clippy::clone_on_copy)]
600    #![allow(clippy::dbg_macro)]
601    #![allow(clippy::mixed_attributes_style)]
602    #![allow(clippy::print_stderr)]
603    #![allow(clippy::print_stdout)]
604    #![allow(clippy::single_char_pattern)]
605    #![allow(clippy::unwrap_used)]
606    #![allow(clippy::unchecked_time_subtraction)]
607    #![allow(clippy::useless_vec)]
608    #![allow(clippy::needless_pass_by_value)]
609    #![allow(clippy::string_slice)] // See arti#2571
610    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
611    use super::*;
612    use tor_rtmock::io::stream_pair;
613
614    use tor_rtmock::simple_time::SimpleMockTimeProvider;
615    use web_time_compat::{SystemTime, SystemTimeExt};
616
617    use futures_await_test::async_test;
618
619    // Basic decompression wrapper.
620    async fn decomp_basic(
621        encoding: Option<&str>,
622        data: &[u8],
623        maxlen: usize,
624    ) -> (RequestResult<()>, Vec<u8>) {
625        // We don't need to do anything fancy here, since we aren't simulating
626        // a timeout.
627        #[allow(deprecated)] // TODO #1885
628        let mock_time = SimpleMockTimeProvider::from_wallclock(SystemTime::get());
629
630        let mut output = Vec::new();
631        let mut stream = match get_decoder(data, encoding, AnonymizedRequest::Direct) {
632            Ok(s) => s,
633            Err(e) => return (Err(e), output),
634        };
635
636        let r = read_and_decompress(&mock_time, &mut stream, maxlen, &mut output).await;
637
638        (r, output)
639    }
640
641    #[async_test]
642    async fn decompress_identity() -> RequestResult<()> {
643        let mut text = Vec::new();
644        for _ in 0..1000 {
645            text.extend(b"This is a string with a nontrivial length that we'll use to make sure that the loop is executed more than once.");
646        }
647
648        let limit = 10 << 20;
649        let (s, r) = decomp_basic(None, &text[..], limit).await;
650        s?;
651        assert_eq!(r, text);
652
653        let (s, r) = decomp_basic(Some("identity"), &text[..], limit).await;
654        s?;
655        assert_eq!(r, text);
656
657        // Try truncated result
658        let limit = 100;
659        let (s, r) = decomp_basic(Some("identity"), &text[..], limit).await;
660        assert!(s.is_err());
661        assert_eq!(r, &text[..100]);
662
663        Ok(())
664    }
665
666    #[async_test]
667    async fn decomp_zlib() -> RequestResult<()> {
668        let compressed =
669            hex::decode("789cf3cf4b5548cb2cce500829cf8730825253200ca79c52881c00e5970c88").unwrap();
670
671        let limit = 10 << 20;
672        let (s, r) = decomp_basic(Some("deflate"), &compressed, limit).await;
673        s?;
674        assert_eq!(r, b"One fish Two fish Red fish Blue fish");
675
676        Ok(())
677    }
678
679    #[cfg(feature = "zstd")]
680    #[async_test]
681    async fn decomp_zstd() -> RequestResult<()> {
682        let compressed = hex::decode("28b52ffd24250d0100c84f6e6520666973682054776f526564426c756520666973680a0200600c0e2509478352cb").unwrap();
683        let limit = 10 << 20;
684        let (s, r) = decomp_basic(Some("x-zstd"), &compressed, limit).await;
685        s?;
686        assert_eq!(r, b"One fish Two fish Red fish Blue fish\n");
687
688        Ok(())
689    }
690
691    #[cfg(feature = "xz")]
692    #[async_test]
693    async fn decomp_xz() -> RequestResult<()> {
694        // echo "One fish Two fish Red fish Blue fish" | xz -6 | xxd -p
695        let compressed = hex::decode("fd377a585a000004e6d6b44604c02525210116000000000000000000bfdbca33e00024001d5d00279b88a202ca8612cfb3c19c87c34248a570451e4851d3323d34ab8000000000000901af64854c91f600014125281fe0821fb6f37d010000000004595a").unwrap();
696        let limit = LZMA_DICT_MEM_LIMIT as usize;
697        let (s, r) = decomp_basic(Some("x-tor-lzma"), &compressed, limit).await;
698        s?;
699        assert_eq!(r, b"One fish Two fish Red fish Blue fish\n");
700
701        Ok(())
702    }
703
704    #[cfg(feature = "xz")]
705    #[async_test]
706    async fn decomp_xz_bad() -> RequestResult<()> {
707        // Not so good at tiny files...
708        let compressed = hex::decode("fd377a585a000004e6d6b446020021011c00000010cf58cce00024001d5d00279b88a202ca8612cfb3c19c87c34248a570451e4851d3323d34ab8000000000000901af64854c91f600013925d6ec06651fb6f37d010000000004595a").unwrap();
709        let limit = 10 << 20;
710        let (s, _) = decomp_basic(Some("x-tor-lzma"), &compressed, limit).await;
711        assert!(matches!(s, Err(RequestError::IoError(_))));
712
713        Ok(())
714    }
715
716    #[async_test]
717    async fn decomp_unknown() {
718        let compressed = hex::decode("28b52ffd24250d0100c84f6e6520666973682054776f526564426c756520666973680a0200600c0e2509478352cb").unwrap();
719        let limit = 10 << 20;
720        let (s, _r) = decomp_basic(Some("x-proprietary-rle"), &compressed, limit).await;
721
722        assert!(matches!(s, Err(RequestError::ContentEncoding(_))));
723    }
724
725    #[async_test]
726    async fn decomp_bad_data() {
727        let compressed = b"This is not good zlib data";
728        let limit = 10 << 20;
729        let (s, _r) = decomp_basic(Some("deflate"), compressed, limit).await;
730
731        // This should possibly be a different type in the future.
732        assert!(matches!(s, Err(RequestError::IoError(_))));
733    }
734
735    #[test]
736    fn headers_ok() -> RequestResult<()> {
737        let text = b"HTTP/1.0 200 OK\r\nDate: ignored\r\nContent-Encoding: Waffles\r\n\r\n";
738
739        let mut s = &text[..];
740        let h = tor_rtcompat::test_with_one_runtime!(async |rt| read_headers(&rt, &mut s).await)?;
741
742        assert_eq!(h.status, Some(200));
743        assert_eq!(h.encoding.as_deref(), Some("Waffles"));
744
745        // now try truncated
746        let mut s = &text[..15];
747        let h = tor_rtcompat::test_with_one_runtime!(async |rt| read_headers(&rt, &mut s).await);
748        assert!(matches!(h, Err(RequestError::TruncatedHeaders)));
749
750        // now try with no encoding.
751        let text = b"HTTP/1.0 404 Not found\r\n\r\n";
752        let mut s = &text[..];
753        let h = tor_rtcompat::test_with_one_runtime!(async |rt| read_headers(&rt, &mut s).await)?;
754
755        assert_eq!(h.status, Some(404));
756        assert!(h.encoding.is_none());
757
758        Ok(())
759    }
760
761    #[test]
762    fn headers_bogus() {
763        let text = b"HTTP/999.0 WHAT EVEN\r\n\r\n";
764        let mut s = &text[..];
765        let h = tor_rtcompat::test_with_one_runtime!(async |rt| read_headers(&rt, &mut s).await);
766
767        assert!(h.is_err());
768        assert!(matches!(h, Err(RequestError::HttparseError(_))));
769    }
770
771    /// Run a trivial download example with a response provided as a binary
772    /// string.
773    ///
774    /// Return the directory response (if any) and the request as encoded (if
775    /// any.)
776    fn run_download_test<Req: request::Requestable>(
777        req: Req,
778        response: &[u8],
779    ) -> (Result<DirResponse>, RequestResult<Vec<u8>>) {
780        let (mut s1, s2) = stream_pair();
781        let (mut s2_r, mut s2_w) = s2.split();
782
783        tor_rtcompat::test_with_one_runtime!(|rt| async move {
784            let rt2 = rt.clone();
785            let (v1, v2, v3): (
786                Result<DirResponse>,
787                RequestResult<Vec<u8>>,
788                RequestResult<()>,
789            ) = futures::join!(
790                async {
791                    // Run the download function.
792                    let r = send_request(&rt, &req, &mut s1, None).await;
793                    s1.close().await.map_err(|error| {
794                        Error::RequestFailed(RequestFailedError {
795                            source: None,
796                            error: error.into(),
797                        })
798                    })?;
799                    r
800                },
801                async {
802                    // Take the request from the client, and return it in "v2"
803                    let mut v = Vec::new();
804                    s2_r.read_to_end(&mut v).await?;
805                    Ok(v)
806                },
807                async {
808                    // Send back a response.
809                    s2_w.write_all(response).await?;
810                    // We wait a moment to give the other side time to notice it
811                    // has data.
812                    //
813                    // (Tentative diagnosis: The `async-compress` crate seems to
814                    // be behave differently depending on whether the "close"
815                    // comes right after the incomplete data or whether it comes
816                    // after a delay.  If there's a delay, it notices the
817                    // truncated data and tells us about it. But when there's
818                    // _no_delay, it treats the data as an error and doesn't
819                    // tell our code.)
820
821                    // TODO: sleeping in tests is not great.
822                    rt2.sleep(Duration::from_millis(50)).await;
823                    s2_w.close().await?;
824                    Ok(())
825                }
826            );
827
828            assert!(v3.is_ok());
829
830            (v1, v2)
831        })
832    }
833
834    #[test]
835    fn test_send_request() -> RequestResult<()> {
836        let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
837
838        let (response, request) = run_download_test(
839            req,
840            b"HTTP/1.0 200 OK\r\n\r\nThis is where the descs would go.",
841        );
842
843        let request = request?;
844        assert!(request[..].starts_with(
845            b"GET /tor/micro/d/CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk HTTP/1.0\r\n"
846        ));
847
848        let response = response.unwrap();
849        assert_eq!(response.status_code(), 200);
850        assert!(!response.is_partial());
851        assert!(response.error().is_none());
852        assert!(response.source().is_none());
853        let out_ref = response.output_unchecked();
854        assert_eq!(out_ref, b"This is where the descs would go.");
855        let out = response.into_output_unchecked();
856        assert_eq!(&out, b"This is where the descs would go.");
857
858        Ok(())
859    }
860
861    #[test]
862    fn test_download_truncated() {
863        // Request only one md, so "partial ok" will not be set.
864        let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
865        let mut response_text: Vec<u8> =
866            (*b"HTTP/1.0 200 OK\r\nContent-Encoding: deflate\r\n\r\n").into();
867        // "One fish two fish" as above twice, but truncated the second time
868        response_text.extend(
869            hex::decode("789cf3cf4b5548cb2cce500829cf8730825253200ca79c52881c00e5970c88").unwrap(),
870        );
871        response_text.extend(
872            hex::decode("789cf3cf4b5548cb2cce500829cf8730825253200ca79c52881c00e5").unwrap(),
873        );
874        let (response, request) = run_download_test(req, &response_text);
875        assert!(request.is_ok());
876        assert!(response.is_err()); // The whole download should fail, since partial_ok wasn't set.
877
878        // request two microdescs, so "partial_ok" will be set.
879        let req: request::MicrodescRequest = vec![[9; 32]; 2].into_iter().collect();
880
881        let (response, request) = run_download_test(req, &response_text);
882        assert!(request.is_ok());
883
884        let response = response.unwrap();
885        assert_eq!(response.status_code(), 200);
886        assert!(response.error().is_some());
887        assert!(response.is_partial());
888        assert!(response.output_unchecked().len() < 37 * 2);
889        assert!(response.output_unchecked().starts_with(b"One fish"));
890    }
891
892    #[test]
893    fn test_404() {
894        let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
895        let response_text = b"HTTP/1.0 418 I'm a teapot\r\n\r\n";
896        let (response, _request) = run_download_test(req, response_text);
897
898        assert_eq!(response.unwrap().status_code(), 418);
899    }
900
901    #[test]
902    fn test_headers_truncated() {
903        let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
904        let response_text = b"HTTP/1.0 404 truncation happens here\r\n";
905        let (response, _request) = run_download_test(req, response_text);
906
907        assert!(matches!(
908            response,
909            Err(Error::RequestFailed(RequestFailedError {
910                error: RequestError::TruncatedHeaders,
911                ..
912            }))
913        ));
914
915        // Try a completely empty response.
916        let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
917        let response_text = b"";
918        let (response, _request) = run_download_test(req, response_text);
919
920        assert!(matches!(
921            response,
922            Err(Error::RequestFailed(RequestFailedError {
923                error: RequestError::TruncatedHeaders,
924                ..
925            }))
926        ));
927    }
928
929    #[test]
930    fn test_headers_too_long() {
931        let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
932        let mut response_text: Vec<u8> = (*b"HTTP/1.0 418 I'm a teapot\r\nX-Too-Many-As: ").into();
933        response_text.resize(16384, b'A');
934        let (response, _request) = run_download_test(req, &response_text);
935
936        assert!(response.as_ref().unwrap_err().should_retire_circ());
937        assert!(matches!(
938            response,
939            Err(Error::RequestFailed(RequestFailedError {
940                error: RequestError::HeadersTooLong(_),
941                ..
942            }))
943        ));
944    }
945
946    #[test]
947    fn test_bad_utf8() {
948        let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
949        let faulty_utf8 = vec![0, 159, 146, 150];
950
951        let mut response_text: Vec<u8> = b"HTTP/1.0 200 OK\r\n\r\n".into();
952        response_text.extend(faulty_utf8);
953
954        let (response, _request) = run_download_test(req, &response_text);
955
956        assert!(matches!(
957            response.unwrap().into_output_string().unwrap_err(),
958            RequestFailedError {
959                error: RequestError::Utf8Encoding(_),
960                ..
961            }
962        ));
963    }
964}