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
3116
    pub(crate) fn new(
46
3116
        method: Method,
47
3116
        status: u16,
48
3116
        status_message: Option<String>,
49
3116
        error: Option<RequestError>,
50
3116
        output: Vec<u8>,
51
3116
        source: Option<SourceInfo>,
52
3116
    ) -> Self {
53
3116
        DirResponse {
54
3116
            method,
55
3116
            status,
56
3116
            status_message,
57
3116
            output,
58
3116
            error,
59
3116
            source,
60
3116
        }
61
3116
    }
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
3760
    pub fn status_code(&self) -> u16 {
70
3760
        self.status
71
3760
    }
72

            
73
    /// Return the HTTP status message for this response.
74
    pub fn status_message(&self) -> Option<&str> {
75
        self.status_message.as_deref()
76
    }
77

            
78
    /// Return true if this is in incomplete response.
79
3082
    pub fn is_partial(&self) -> bool {
80
3082
        self.error.is_some()
81
3082
    }
82

            
83
    /// Return the error from this response, if any.
84
4
    pub fn error(&self) -> Option<&RequestError> {
85
4
        self.error.as_ref()
86
4
    }
87

            
88
    /// Return the output from this response.
89
    ///
90
    /// Returns some output, even if the response indicates truncation or an error.
91
92
    pub fn output_unchecked(&self) -> &[u8] {
92
92
        &self.output
93
92
    }
94

            
95
    /// Return the output from this response, if it was successful and complete.
96
94
    pub fn output(&self) -> Result<&[u8], RequestFailedError> {
97
94
        self.check_ok()?;
98
86
        Ok(self.output_unchecked())
99
94
    }
100

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

            
116
    /// Consume this DirResponse and return the output in it.
117
    ///
118
    /// Returns some output, even if the response indicates truncation or an error.
119
46
    pub fn into_output_unchecked(self) -> Vec<u8> {
120
46
        self.output
121
46
    }
122

            
123
    /// Consume this DirResponse and return the output, if it was successful and complete.
124
10
    pub fn into_output(self) -> Result<Vec<u8>, RequestFailedError> {
125
10
        self.check_ok()?;
126
2
        Ok(self.into_output_unchecked())
127
10
    }
128

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

            
140
    /// Return the source information about this response.
141
44
    pub fn source(&self) -> Option<&SourceInfo> {
142
44
        self.source.as_ref()
143
44
    }
144

            
145
    /// Check if this request was successful and complete.
146
3086
    fn check_ok(&self) -> Result<(), RequestFailedError> {
147
3110
        let wrap_err = |error| {
148
688
            Err(RequestFailedError {
149
688
                error,
150
688
                source: self.source.clone(),
151
688
            })
152
688
        };
153
3086
        if let Some(error) = &self.error {
154
8
            return wrap_err(error.clone());
155
3078
        }
156
3078
        assert!(!self.is_partial(), "partial but no error?");
157
3078
        if self.status_code() != 200 {
158
676
            let msg = match &self.status_message {
159
676
                Some(m) => m.clone(),
160
                None => "".to_owned(),
161
            };
162
676
            return wrap_err(RequestError::HttpStatus(self.status_code(), msg));
163
2402
        }
164

            
165
        // We do not allow a successful response with an empty body on GET.
166
2402
        if self.output.is_empty() && self.method == Method::GET {
167
4
            return wrap_err(RequestError::EmptyResponse);
168
2398
        }
169

            
170
2398
        Ok(())
171
3086
    }
172
}
173

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

            
192
    /// Return the unique circuit identifier for the circuit on which
193
    /// we received this info.
194
    pub fn unique_circ_id(&self) -> &UniqId {
195
        &self.tunnel
196
    }
197

            
198
    /// Return information about the peer from which we received this info.
199
    pub fn cache_id(&self) -> &OwnedChanTarget {
200
        self.cache_id.as_inner()
201
    }
202
}
203

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

            
221
    #[test]
222
    fn errors() {
223
        let mut response = DirResponse::new(Method::GET, 200, None, None, vec![b'Y'], None);
224

            
225
        assert_eq!(response.output().unwrap(), b"Y");
226
        assert_eq!(response.clone().into_output().unwrap(), b"Y");
227

            
228
        let expect_error = |response: &DirResponse, error: RequestError| {
229
            let error = RequestFailedError {
230
                error,
231
                source: None,
232
            };
233
            let error = format!("{:?}", error);
234

            
235
            assert_eq!(error, format!("{:?}", response.output().unwrap_err()));
236
            assert_eq!(
237
                error,
238
                format!("{:?}", response.clone().into_output().unwrap_err())
239
            );
240
        };
241

            
242
        let with_error = |response: &DirResponse| {
243
            let mut response = response.clone();
244
            response.error = Some(RequestError::DirTimeout);
245
            expect_error(&response, RequestError::DirTimeout);
246
        };
247

            
248
        with_error(&response);
249

            
250
        response.output = vec![];
251
        expect_error(&response, RequestError::EmptyResponse);
252

            
253
        response.status = 404;
254
        response.status_message = Some("Not found".into());
255
        expect_error(&response, RequestError::HttpStatus(404, "Not found".into()));
256

            
257
        with_error(&response);
258
    }
259
}