tor_dirclient/
response.rs

1//! Define a response type for directory requests.
2
3use std::str;
4
5use tor_linkspec::{LoggedChanTarget, OwnedChanTarget};
6use tor_proto::circuit::UniqId;
7
8use crate::{RequestError, RequestFailedError};
9
10/// A successful (or at any rate, well-formed) response to a directory
11/// request.
12#[derive(Debug, Clone)]
13#[must_use = "You need to check whether the response was successful."]
14pub struct DirResponse {
15    /// An HTTP status code.
16    status: u16,
17    /// The message associated with the status code.
18    status_message: Option<String>,
19    /// The decompressed output that we got from the directory cache.
20    output: Vec<u8>,
21    /// The error, if any, that caused us to stop getting this response early.
22    error: Option<RequestError>,
23    /// Information about the directory cache we used.
24    source: Option<SourceInfo>,
25}
26
27/// Information about the source of a directory response.
28///
29/// We use this to remember when a request has failed, so we can
30/// abandon the circuit.
31#[derive(Debug, Clone, derive_more::Display)]
32#[display("{} via {}", cache_id, tunnel)]
33pub struct SourceInfo {
34    /// Unique identifier for the circuit we're using
35    tunnel: UniqId,
36    /// Identity of the directory cache that provided us this information.
37    cache_id: LoggedChanTarget,
38}
39
40impl DirResponse {
41    /// Construct a new DirResponse from its parts
42    pub(crate) fn new(
43        status: u16,
44        status_message: Option<String>,
45        error: Option<RequestError>,
46        output: Vec<u8>,
47        source: Option<SourceInfo>,
48    ) -> Self {
49        DirResponse {
50            status,
51            status_message,
52            output,
53            error,
54            source,
55        }
56    }
57
58    /// Construct a new successful DirResponse from its body.
59    pub fn from_body(body: impl AsRef<[u8]>) -> Self {
60        Self::new(200, None, None, body.as_ref().to_vec(), None)
61    }
62
63    /// Return the HTTP status code for this response.
64    pub fn status_code(&self) -> u16 {
65        self.status
66    }
67
68    /// Return true if this is in incomplete response.
69    pub fn is_partial(&self) -> bool {
70        self.error.is_some()
71    }
72
73    /// Return the error from this response, if any.
74    pub fn error(&self) -> Option<&RequestError> {
75        self.error.as_ref()
76    }
77
78    /// Return the output from this response.
79    ///
80    /// Returns some output, even if the response indicates truncation or an error.
81    pub fn output_unchecked(&self) -> &[u8] {
82        &self.output
83    }
84
85    /// Return the output from this response, if it was successful and complete.
86    pub fn output(&self) -> Result<&[u8], RequestFailedError> {
87        self.check_ok()?;
88        Ok(self.output_unchecked())
89    }
90
91    /// Return this the output from this response, as a string,
92    /// if it was successful and complete and valid UTF-8.
93    pub fn output_string(&self) -> Result<&str, RequestFailedError> {
94        let output = self.output()?;
95        let s = str::from_utf8(output).map_err(|_| RequestFailedError {
96            // For RequestError::Utf8Encoding We need a `String::FromUtf8Error`
97            // (which contains an owned copy of the bytes).
98            error: String::from_utf8(output.to_owned())
99                .expect_err("was bad, now good")
100                .into(),
101            source: self.source.clone(),
102        })?;
103        Ok(s)
104    }
105
106    /// Consume this DirResponse and return the output in it.
107    ///
108    /// Returns some output, even if the response indicates truncation or an error.
109    pub fn into_output_unchecked(self) -> Vec<u8> {
110        self.output
111    }
112
113    /// Consume this DirResponse and return the output, if it was successful and complete.
114    pub fn into_output(self) -> Result<Vec<u8>, RequestFailedError> {
115        self.check_ok()?;
116        Ok(self.into_output_unchecked())
117    }
118
119    /// Consume this DirResponse and return the output, as a string,
120    /// if it was successful and complete and valid UTF-8.
121    pub fn into_output_string(self) -> Result<String, RequestFailedError> {
122        self.check_ok()?;
123        let s = String::from_utf8(self.output).map_err(|error| RequestFailedError {
124            error: error.into(),
125            source: self.source.clone(),
126        })?;
127        Ok(s)
128    }
129
130    /// Return the source information about this response.
131    pub fn source(&self) -> Option<&SourceInfo> {
132        self.source.as_ref()
133    }
134
135    /// Check if this request was successful and complete.
136    fn check_ok(&self) -> Result<(), RequestFailedError> {
137        let wrap_err = |error| {
138            Err(RequestFailedError {
139                error,
140                source: self.source.clone(),
141            })
142        };
143        if let Some(error) = &self.error {
144            return wrap_err(error.clone());
145        }
146        assert!(!self.is_partial(), "partial but no error?");
147        if self.status_code() != 200 {
148            let msg = match &self.status_message {
149                Some(m) => m.clone(),
150                None => "".to_owned(),
151            };
152            return wrap_err(RequestError::HttpStatus(self.status_code(), msg));
153        }
154        Ok(())
155    }
156}
157
158impl SourceInfo {
159    /// Try to construct a new SourceInfo representing the last hop of a given tunnel.
160    ///
161    /// Return an error if the tunnel is closed;
162    /// return `Ok(None)` if the circuit's last hop is virtual.
163    pub fn from_tunnel(
164        tunnel: impl AsRef<tor_proto::ClientTunnel>,
165    ) -> tor_proto::Result<Option<Self>> {
166        let tunnel = tunnel.as_ref();
167        match tunnel.last_hop_info()? {
168            None => Ok(None),
169            Some(last_hop) => Ok(Some(SourceInfo {
170                tunnel: tunnel.unique_id(),
171                cache_id: last_hop.into(),
172            })),
173        }
174    }
175
176    /// Return the unique circuit identifier for the circuit on which
177    /// we received this info.
178    pub fn unique_circ_id(&self) -> &UniqId {
179        &self.tunnel
180    }
181
182    /// Return information about the peer from which we received this info.
183    pub fn cache_id(&self) -> &OwnedChanTarget {
184        self.cache_id.as_inner()
185    }
186}
187
188#[cfg(test)]
189mod test {
190    // @@ begin test lint list maintained by maint/add_warning @@
191    #![allow(clippy::bool_assert_comparison)]
192    #![allow(clippy::clone_on_copy)]
193    #![allow(clippy::dbg_macro)]
194    #![allow(clippy::mixed_attributes_style)]
195    #![allow(clippy::print_stderr)]
196    #![allow(clippy::print_stdout)]
197    #![allow(clippy::single_char_pattern)]
198    #![allow(clippy::unwrap_used)]
199    #![allow(clippy::unchecked_duration_subtraction)]
200    #![allow(clippy::useless_vec)]
201    #![allow(clippy::needless_pass_by_value)]
202    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
203    use super::*;
204
205    #[test]
206    fn errors() {
207        let mut response = DirResponse::new(200, None, None, vec![b'Y'], None);
208
209        assert_eq!(response.output().unwrap(), b"Y");
210        assert_eq!(response.clone().into_output().unwrap(), b"Y");
211
212        let expect_error = |response: &DirResponse, error: RequestError| {
213            let error = RequestFailedError {
214                error,
215                source: None,
216            };
217            let error = format!("{:?}", error);
218
219            assert_eq!(error, format!("{:?}", response.output().unwrap_err()));
220            assert_eq!(
221                error,
222                format!("{:?}", response.clone().into_output().unwrap_err())
223            );
224        };
225
226        let with_error = |response: &DirResponse| {
227            let mut response = response.clone();
228            response.error = Some(RequestError::DirTimeout);
229            expect_error(&response, RequestError::DirTimeout);
230        };
231
232        with_error(&response);
233
234        response.status = 404;
235        response.status_message = Some("Not found".into());
236        expect_error(&response, RequestError::HttpStatus(404, "Not found".into()));
237
238        with_error(&response);
239    }
240}