tor_dirclient/
response.rs1use std::str;
4
5use http::Method;
6use tor_linkspec::{LoggedChanTarget, OwnedChanTarget};
7use tor_proto::circuit::UniqId;
8
9use crate::{RequestError, RequestFailedError};
10
11#[derive(Debug, Clone)]
14#[must_use = "You need to check whether the response was successful."]
15pub struct DirResponse {
16 method: Method,
18 status: u16,
20 status_message: Option<String>,
22 output: Vec<u8>,
24 error: Option<RequestError>,
26 source: Option<SourceInfo>,
28}
29
30#[derive(Debug, Clone, derive_more::Display)]
35#[display("{} via {}", cache_id, tunnel)]
36pub struct SourceInfo {
37 tunnel: UniqId,
39 cache_id: LoggedChanTarget,
41}
42
43impl DirResponse {
44 pub(crate) fn new(
46 method: Method,
47 status: u16,
48 status_message: Option<String>,
49 error: Option<RequestError>,
50 output: Vec<u8>,
51 source: Option<SourceInfo>,
52 ) -> Self {
53 DirResponse {
54 method,
55 status,
56 status_message,
57 output,
58 error,
59 source,
60 }
61 }
62
63 pub fn from_get_body(body: impl AsRef<[u8]>) -> Self {
65 Self::new(Method::GET, 200, None, None, body.as_ref().to_vec(), None)
66 }
67
68 pub fn status_code(&self) -> u16 {
70 self.status
71 }
72
73 pub fn status_message(&self) -> Option<&str> {
75 self.status_message.as_deref()
76 }
77
78 pub fn is_partial(&self) -> bool {
80 self.error.is_some()
81 }
82
83 pub fn error(&self) -> Option<&RequestError> {
85 self.error.as_ref()
86 }
87
88 pub fn output_unchecked(&self) -> &[u8] {
92 &self.output
93 }
94
95 pub fn output(&self) -> Result<&[u8], RequestFailedError> {
97 self.check_ok()?;
98 Ok(self.output_unchecked())
99 }
100
101 pub fn output_string(&self) -> Result<&str, RequestFailedError> {
104 let output = self.output()?;
105 let s = str::from_utf8(output).map_err(|_| RequestFailedError {
106 error: String::from_utf8(output.to_owned())
109 .expect_err("was bad, now good")
110 .into(),
111 source: self.source.clone(),
112 })?;
113 Ok(s)
114 }
115
116 pub fn into_output_unchecked(self) -> Vec<u8> {
120 self.output
121 }
122
123 pub fn into_output(self) -> Result<Vec<u8>, RequestFailedError> {
125 self.check_ok()?;
126 Ok(self.into_output_unchecked())
127 }
128
129 pub fn into_output_string(self) -> Result<String, RequestFailedError> {
132 self.check_ok()?;
133 let s = String::from_utf8(self.output).map_err(|error| RequestFailedError {
134 error: error.into(),
135 source: self.source.clone(),
136 })?;
137 Ok(s)
138 }
139
140 pub fn source(&self) -> Option<&SourceInfo> {
142 self.source.as_ref()
143 }
144
145 fn check_ok(&self) -> Result<(), RequestFailedError> {
147 let wrap_err = |error| {
148 Err(RequestFailedError {
149 error,
150 source: self.source.clone(),
151 })
152 };
153 if let Some(error) = &self.error {
154 return wrap_err(error.clone());
155 }
156 assert!(!self.is_partial(), "partial but no error?");
157 if self.status_code() != 200 {
158 let msg = match &self.status_message {
159 Some(m) => m.clone(),
160 None => "".to_owned(),
161 };
162 return wrap_err(RequestError::HttpStatus(self.status_code(), msg));
163 }
164
165 if self.output.is_empty() && self.method == Method::GET {
167 return wrap_err(RequestError::EmptyResponse);
168 }
169
170 Ok(())
171 }
172}
173
174impl SourceInfo {
175 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 pub fn unique_circ_id(&self) -> &UniqId {
195 &self.tunnel
196 }
197
198 pub fn cache_id(&self) -> &OwnedChanTarget {
200 self.cache_id.as_inner()
201 }
202}
203
204#[cfg(test)]
205mod test {
206 #![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 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}