1
//! Define a response type for directory requests.
2

            
3
use std::str;
4

            
5
use http::Method;
6
use tor_linkspec::{LoggedChanTarget, OwnedChanTarget};
7
use tor_proto::circuit::UniqId;
8

            
9
use crate::{RequestError, RequestFailedError};
10

            
11
/// A successful (or at any rate, well-formed) response to a directory
12
/// request.
13
#[derive(Debug, Clone)]
14
#[must_use = "You need to check whether the response was successful."]
15
pub struct DirResponse {
16
    /// The HTTP method of the request.
17
    method: Method,
18
    /// An HTTP status code.
19
    status: u16,
20
    /// The message associated with the status code.
21
    status_message: Option<String>,
22
    /// The decompressed output that we got from the directory cache.
23
    output: Vec<u8>,
24
    /// The error, if any, that caused us to stop getting this response early.
25
    error: Option<RequestError>,
26
    /// Information about the directory cache we used.
27
    source: Option<SourceInfo>,
28
}
29

            
30
/// Information about the source of a directory response.
31
///
32
/// We use this to remember when a request has failed, so we can
33
/// abandon the circuit.
34
#[derive(Debug, Clone, derive_more::Display)]
35
#[display("{} via {}", cache_id, tunnel)]
36
pub struct SourceInfo {
37
    /// Unique identifier for the circuit we're using
38
    tunnel: UniqId,
39
    /// Identity of the directory cache that provided us this information.
40
    cache_id: LoggedChanTarget,
41
}
42

            
43
impl DirResponse {
44
    /// Construct a new DirResponse from its parts
45
3452
    pub(crate) fn new(
46
3452
        method: Method,
47
3452
        status: u16,
48
3452
        status_message: Option<String>,
49
3452
        error: Option<RequestError>,
50
3452
        output: Vec<u8>,
51
3452
        source: Option<SourceInfo>,
52
3452
    ) -> Self {
53
3452
        DirResponse {
54
3452
            method,
55
3452
            status,
56
3452
            status_message,
57
3452
            output,
58
3452
            error,
59
3452
            source,
60
3452
        }
61
3452
    }
62

            
63
    /// Construct a new successful DirResponse to a GET request from its body.
64
2
    pub fn from_get_body(body: impl AsRef<[u8]>) -> Self {
65
2
        Self::new(Method::GET, 200, None, None, body.as_ref().to_vec(), None)
66
2
    }
67

            
68
    /// Return the HTTP status code for this response.
69
4368
    pub fn status_code(&self) -> u16 {
70
4368
        self.status
71
4368
    }
72

            
73
    /// Return true if this is in incomplete response.
74
3419
    pub fn is_partial(&self) -> bool {
75
3419
        self.error.is_some()
76
3419
    }
77

            
78
    /// Return the error from this response, if any.
79
4
    pub fn error(&self) -> Option<&RequestError> {
80
4
        self.error.as_ref()
81
4
    }
82

            
83
    /// Return the output from this response.
84
    ///
85
    /// Returns some output, even if the response indicates truncation or an error.
86
8
    pub fn output_unchecked(&self) -> &[u8] {
87
8
        &self.output
88
8
    }
89

            
90
    /// Return the output from this response, if it was successful and complete.
91
10
    pub fn output(&self) -> Result<&[u8], RequestFailedError> {
92
10
        self.check_ok()?;
93
2
        Ok(self.output_unchecked())
94
10
    }
95

            
96
    /// Return this the output from this response, as a string,
97
    /// if it was successful and complete and valid UTF-8.
98
    pub fn output_string(&self) -> Result<&str, RequestFailedError> {
99
        let output = self.output()?;
100
        let s = str::from_utf8(output).map_err(|_| RequestFailedError {
101
            // For RequestError::Utf8Encoding We need a `String::FromUtf8Error`
102
            // (which contains an owned copy of the bytes).
103
            error: String::from_utf8(output.to_owned())
104
                .expect_err("was bad, now good")
105
                .into(),
106
            source: self.source.clone(),
107
        })?;
108
        Ok(s)
109
    }
110

            
111
    /// Consume this DirResponse and return the output in it.
112
    ///
113
    /// Returns some output, even if the response indicates truncation or an error.
114
168
    pub fn into_output_unchecked(self) -> Vec<u8> {
115
168
        self.output
116
168
    }
117

            
118
    /// Consume this DirResponse and return the output, if it was successful and complete.
119
420
    pub fn into_output(self) -> Result<Vec<u8>, RequestFailedError> {
120
420
        self.check_ok()?;
121
125
        Ok(self.into_output_unchecked())
122
420
    }
123

            
124
    /// Consume this DirResponse and return the output, as a string,
125
    /// if it was successful and complete and valid UTF-8.
126
2993
    pub fn into_output_string(self) -> Result<String, RequestFailedError> {
127
2993
        self.check_ok()?;
128
2337
        let s = String::from_utf8(self.output).map_err(|error| RequestFailedError {
129
            error: error.into(),
130
            source: self.source.clone(),
131
        })?;
132
2337
        Ok(s)
133
2993
    }
134

            
135
    /// Return the source information about this response.
136
43
    pub fn source(&self) -> Option<&SourceInfo> {
137
43
        self.source.as_ref()
138
43
    }
139

            
140
    /// Check if this request was successful and complete.
141
3423
    fn check_ok(&self) -> Result<(), RequestFailedError> {
142
3454
        let wrap_err = |error| {
143
959
            Err(RequestFailedError {
144
959
                error,
145
959
                source: self.source.clone(),
146
959
            })
147
959
        };
148
3423
        if let Some(error) = &self.error {
149
8
            return wrap_err(error.clone());
150
3415
        }
151
3415
        assert!(!self.is_partial(), "partial but no error?");
152
3415
        if self.status_code() != 200 {
153
947
            let msg = match &self.status_message {
154
947
                Some(m) => m.clone(),
155
                None => "".to_owned(),
156
            };
157
947
            return wrap_err(RequestError::HttpStatus(self.status_code(), msg));
158
2468
        }
159

            
160
        // We do not allow a successful response with an empty body on GET.
161
2468
        if self.output.is_empty() && self.method == Method::GET {
162
4
            return wrap_err(RequestError::EmptyResponse);
163
2464
        }
164

            
165
2464
        Ok(())
166
3423
    }
167
}
168

            
169
impl SourceInfo {
170
    /// Try to construct a new SourceInfo representing the last hop of a given tunnel.
171
    ///
172
    /// Return an error if the tunnel is closed;
173
    /// return `Ok(None)` if the circuit's last hop is virtual.
174
    pub fn from_tunnel(
175
        tunnel: impl AsRef<tor_proto::ClientTunnel>,
176
    ) -> tor_proto::Result<Option<Self>> {
177
        let tunnel = tunnel.as_ref();
178
        match tunnel.last_hop_info()? {
179
            None => Ok(None),
180
            Some(last_hop) => Ok(Some(SourceInfo {
181
                tunnel: tunnel.unique_id(),
182
                cache_id: last_hop.into(),
183
            })),
184
        }
185
    }
186

            
187
    /// Return the unique circuit identifier for the circuit on which
188
    /// we received this info.
189
    pub fn unique_circ_id(&self) -> &UniqId {
190
        &self.tunnel
191
    }
192

            
193
    /// Return information about the peer from which we received this info.
194
    pub fn cache_id(&self) -> &OwnedChanTarget {
195
        self.cache_id.as_inner()
196
    }
197
}
198

            
199
#[cfg(test)]
200
mod test {
201
    // @@ begin test lint list maintained by maint/add_warning @@
202
    #![allow(clippy::bool_assert_comparison)]
203
    #![allow(clippy::clone_on_copy)]
204
    #![allow(clippy::dbg_macro)]
205
    #![allow(clippy::mixed_attributes_style)]
206
    #![allow(clippy::print_stderr)]
207
    #![allow(clippy::print_stdout)]
208
    #![allow(clippy::single_char_pattern)]
209
    #![allow(clippy::unwrap_used)]
210
    #![allow(clippy::unchecked_time_subtraction)]
211
    #![allow(clippy::useless_vec)]
212
    #![allow(clippy::needless_pass_by_value)]
213
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
214
    use super::*;
215

            
216
    #[test]
217
    fn errors() {
218
        let mut response = DirResponse::new(Method::GET, 200, None, None, vec![b'Y'], None);
219

            
220
        assert_eq!(response.output().unwrap(), b"Y");
221
        assert_eq!(response.clone().into_output().unwrap(), b"Y");
222

            
223
        let expect_error = |response: &DirResponse, error: RequestError| {
224
            let error = RequestFailedError {
225
                error,
226
                source: None,
227
            };
228
            let error = format!("{:?}", error);
229

            
230
            assert_eq!(error, format!("{:?}", response.output().unwrap_err()));
231
            assert_eq!(
232
                error,
233
                format!("{:?}", response.clone().into_output().unwrap_err())
234
            );
235
        };
236

            
237
        let with_error = |response: &DirResponse| {
238
            let mut response = response.clone();
239
            response.error = Some(RequestError::DirTimeout);
240
            expect_error(&response, RequestError::DirTimeout);
241
        };
242

            
243
        with_error(&response);
244

            
245
        response.output = vec![];
246
        expect_error(&response, RequestError::EmptyResponse);
247

            
248
        response.status = 404;
249
        response.status_message = Some("Not found".into());
250
        expect_error(&response, RequestError::HttpStatus(404, "Not found".into()));
251

            
252
        with_error(&response);
253
    }
254
}