use std::str;
use http::Method;
use tor_linkspec::{LoggedChanTarget, OwnedChanTarget};
use tor_proto::circuit::UniqId;
use crate::{RequestError, RequestFailedError};
#[derive(Debug, Clone)]
#[must_use = "You need to check whether the response was successful."]
pub struct DirResponse {
method: Method,
status: u16,
status_message: Option<String>,
output: Vec<u8>,
error: Option<RequestError>,
source: Option<SourceInfo>,
}
#[derive(Debug, Clone, derive_more::Display)]
#[display("{} via {}", cache_id, tunnel)]
pub struct SourceInfo {
tunnel: UniqId,
cache_id: LoggedChanTarget,
}
impl DirResponse {
pub(crate) fn new(
method: Method,
status: u16,
status_message: Option<String>,
error: Option<RequestError>,
output: Vec<u8>,
source: Option<SourceInfo>,
) -> Self {
DirResponse {
method,
status,
status_message,
output,
error,
source,
}
}
pub fn from_get_body(body: impl AsRef<[u8]>) -> Self {
Self::new(Method::GET, 200, None, None, body.as_ref().to_vec(), None)
}
pub fn status_code(&self) -> u16 {
self.status
}
pub fn is_partial(&self) -> bool {
self.error.is_some()
}
pub fn error(&self) -> Option<&RequestError> {
self.error.as_ref()
}
pub fn output_unchecked(&self) -> &[u8] {
&self.output
}
pub fn output(&self) -> Result<&[u8], RequestFailedError> {
self.check_ok()?;
Ok(self.output_unchecked())
}
pub fn output_string(&self) -> Result<&str, RequestFailedError> {
let output = self.output()?;
let s = str::from_utf8(output).map_err(|_| RequestFailedError {
error: String::from_utf8(output.to_owned())
.expect_err("was bad, now good")
.into(),
source: self.source.clone(),
})?;
Ok(s)
}
pub fn into_output_unchecked(self) -> Vec<u8> {
self.output
}
pub fn into_output(self) -> Result<Vec<u8>, RequestFailedError> {
self.check_ok()?;
Ok(self.into_output_unchecked())
}
pub fn into_output_string(self) -> Result<String, RequestFailedError> {
self.check_ok()?;
let s = String::from_utf8(self.output).map_err(|error| RequestFailedError {
error: error.into(),
source: self.source.clone(),
})?;
Ok(s)
}
pub fn source(&self) -> Option<&SourceInfo> {
self.source.as_ref()
}
fn check_ok(&self) -> Result<(), RequestFailedError> {
let wrap_err = |error| {
Err(RequestFailedError {
error,
source: self.source.clone(),
})
};
if let Some(error) = &self.error {
return wrap_err(error.clone());
}
assert!(!self.is_partial(), "partial but no error?");
if self.status_code() != 200 {
let msg = match &self.status_message {
Some(m) => m.clone(),
None => "".to_owned(),
};
return wrap_err(RequestError::HttpStatus(self.status_code(), msg));
}
if self.output.is_empty() && self.method == Method::GET {
return wrap_err(RequestError::EmptyResponse);
}
Ok(())
}
}
impl SourceInfo {
pub fn from_tunnel(
tunnel: impl AsRef<tor_proto::ClientTunnel>,
) -> tor_proto::Result<Option<Self>> {
let tunnel = tunnel.as_ref();
match tunnel.last_hop_info()? {
None => Ok(None),
Some(last_hop) => Ok(Some(SourceInfo {
tunnel: tunnel.unique_id(),
cache_id: last_hop.into(),
})),
}
}
pub fn unique_circ_id(&self) -> &UniqId {
&self.tunnel
}
pub fn cache_id(&self) -> &OwnedChanTarget {
self.cache_id.as_inner()
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
#[test]
fn errors() {
let mut response = DirResponse::new(Method::GET, 200, None, None, vec![b'Y'], None);
assert_eq!(response.output().unwrap(), b"Y");
assert_eq!(response.clone().into_output().unwrap(), b"Y");
let expect_error = |response: &DirResponse, error: RequestError| {
let error = RequestFailedError {
error,
source: None,
};
let error = format!("{:?}", error);
assert_eq!(error, format!("{:?}", response.output().unwrap_err()));
assert_eq!(
error,
format!("{:?}", response.clone().into_output().unwrap_err())
);
};
let with_error = |response: &DirResponse| {
let mut response = response.clone();
response.error = Some(RequestError::DirTimeout);
expect_error(&response, RequestError::DirTimeout);
};
with_error(&response);
response.output = vec![];
expect_error(&response, RequestError::EmptyResponse);
response.status = 404;
response.status_message = Some("Not found".into());
expect_error(&response, RequestError::HttpStatus(404, "Not found".into()));
with_error(&response);
}
}