summaryrefslogtreecommitdiff
path: root/crates/tor-dirpublish/src/err.rs
blob: c224447bf50452de6f8b6ea01c88f10877f72a94 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
//! Error types from [`Uploader`](super::Uploader)s
//! and [`Publisher`](super::Publisher)s.
use std::{sync::Arc, time::Duration};
use tor_error::{Bug, HasKind, internal};

/// Trait to represent the kinds of errors that we generally return in Arti.
//
// TODO: Move this to tor-error?
// NOTE: Adding clone makes this no longer dyn-safe.
pub trait TorError:
    HasKind + std::fmt::Debug + std::fmt::Display + std::error::Error + Send + Sync + 'static
{
}
impl<T> TorError for T where
    T: HasKind + std::fmt::Debug + std::fmt::Display + std::error::Error + Send + Sync + 'static
{
}

/// A failure to upload a document to a single target.
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum UploadError {
    /// The target refused the document, and we should not retry.
    #[error("Directory rejected document, saying {0}")]
    Rejected(Rejection),

    /// We were unable to make a direct connection to a directory.
    #[error("Unable to connect to directory")]
    Connect(#[source] Arc<std::io::Error>),

    /// The response we received appears to violate some part of the protocol.
    #[error("Invalid response: {0}")]
    InvalidResponse(#[source] Arc<dyn TorError>),

    /// Our attempt to upload the document took too long.
    #[error("Upload attempt took too long")]
    Timeout,

    /// The target told us to come back later.
    ///
    /// If a duration is provided, we should not come back
    /// before the provided duration.
    #[error("Directory told us to come back later.")]
    Deferred {
        /// The status code we received
        status_code: u16,

        /// The HTTP message we received
        message: String,

        /// The amount of time we were told to wait (or decided to wait)
        how_long: Option<Duration>,
    },

    /// An error from tor_dirclient that did not fall into any other category.
    ///
    /// (Don't construct this directly; instead, use From.)
    #[error("Unable to upload document")]
    SendRequest(#[source] Box<tor_dirclient::RequestError>),

    /// The upload failed for some other non-retriable reason.
    #[error("Document upload failed; cannot retry at this target")]
    DocumentFailedPermanently(#[source] Arc<dyn TorError>),

    /// The upload failed for some other retriable reason.
    ///
    /// (This variant is here so we can construct [`UploadError`]s from anywhere in Arti.)
    #[error("Directory upload failed")]
    Other(#[source] Arc<dyn TorError>),

    /// A bug occurred.
    #[error("Bug occurred while uploading")]
    Bug(#[source] Bug),
}

impl UploadError {
    /// If this error has a suggested delay before retrying, return that delay.
    pub(crate) fn suggested_delay(&self) -> Option<Duration> {
        match self {
            UploadError::Deferred { how_long, .. } => *how_long,
            _ => None,
        }
    }
}

/// A record of a rejection from an upload target.
#[derive(Clone, Debug)]
pub struct Rejection {
    /// The provided message when the document was rejected.
    message: String,
}

impl Rejection {
    /// Create a new rejection from a message returned by an upload target.
    pub fn from_message(message: String) -> Self {
        Rejection { message }
    }

    /// Return the message returned by the upload target.
    pub fn message(&self) -> &str {
        self.message.as_str()
    }
}

impl std::fmt::Display for Rejection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self.message)
    }
}

impl From<tor_dirclient::RequestError> for UploadError {
    fn from(err: tor_dirclient::RequestError) -> Self {
        use UploadError as E;
        use tor_dirclient::RequestError as RE;
        match err {
            RE::DirTimeout => E::Timeout,
            RE::ResponseTooLong(_)
            | RE::HeadersTooLong(_)
            | RE::Utf8Encoding(_)
            | RE::HttpError(_)
            | RE::HttparseError(_) => E::InvalidResponse(Arc::new(err)),
            RE::HttpStatus(status_code, message) => {
                match Self::from_http_response(status_code, Some(message)) {
                    Some(e) => e,
                    None => E::Bug(internal!(
                        "Unexpected successful status code {status_code:?}"
                    )),
                }
            }
            RE::IoError(_)
            | RE::TruncatedHeaders
            | RE::Proto(_)
            | RE::Tunnel(_)
            | RE::TooMuchClockSkew
            | RE::EmptyRequest
            | RE::EmptyResponse
            | RE::ContentEncoding(_) => E::SendRequest(Box::new(err)),
            // We need this catch-all since the tor_dirclient::RequestError is non-exhaustive.
            other => E::SendRequest(Box::new(other)),
        }
    }
}

impl From<tor_dirclient::Error> for UploadError {
    fn from(err: tor_dirclient::Error) -> Self {
        use tor_dirclient::Error as DE;
        match err {
            DE::CircMgr(error) => Self::Other(Arc::new(error) as _),
            DE::RequestFailed(request_failed_error) => Self::from(request_failed_error.error),
            DE::Bug(bug) => Self::Bug(bug),
            other => Self::Other(Arc::new(other) as _),
        }
    }
}

/// Error type for an invalid http status code.
#[derive(Clone, Debug, thiserror::Error)]
#[error("Invalid http response code {0:?}")]
struct BadStatusCode(u16);

impl tor_error::HasKind for BadStatusCode {
    fn kind(&self) -> tor_error::ErrorKind {
        tor_error::ErrorKind::TorProtocolViolation
    }
}

impl UploadError {
    /// Construct a `Result<(), UploadError>` from a [`tor_dirclient::DirResponse`].
    pub(crate) fn from_directory_response(
        response: Result<tor_dirclient::DirResponse, tor_dirclient::Error>,
    ) -> Result<(), UploadError> {
        let response = response.map_err(Self::from)?;
        if let Some(e) = response.error() {
            return Err(Self::from(e.clone()));
        }

        match Self::from_http_response(
            response.status_code(),
            response.status_message().map(String::from),
        ) {
            Some(error) => Err(error),
            None => Ok(()),
        }
    }

    /// If the status code was 503, and no delay was provided,
    /// set the delay to `duration`
    pub(crate) fn set_503_delay(mut self, duration: Duration) -> Self {
        match &mut self {
            Self::Deferred {
                status_code: 503,
                how_long,
                ..
            } if how_long.is_none() => {
                *how_long = Some(duration);
            }
            _ => {}
        }
        self
    }

    /// Return true if we can retry after this error.
    pub(crate) fn is_retriable(&self) -> bool {
        match self {
            UploadError::Rejected(_) => false,
            UploadError::DocumentFailedPermanently(_) => false,

            UploadError::Connect(_) => true,
            UploadError::InvalidResponse(_) => true,
            UploadError::Timeout => true,
            UploadError::Deferred { .. } => true,
            UploadError::SendRequest(_) => true,
            UploadError::Other(_) => true,
            UploadError::Bug(_) => true,
        }
    }

    /// Construct an [`UploadError`] from a status code and message.
    ///
    /// Return None if the status code indicates success.
    fn from_http_response(status_code: u16, message: Option<String>) -> Option<Self> {
        use UploadError as E;
        Some(match status_code {
            200 => return None,
            400..=499 => E::Rejected(Rejection::from_message(
                message.unwrap_or_else(|| "Refused".into()),
            )),
            500..=599 => E::Deferred {
                status_code,
                message: message.unwrap_or_else(|| "Server Error".into()),
                how_long: None,
            },
            // TODO: We don't historically handle 3xx or 1xx or any 2xx except 200 while uploading,
            // but probably we should.
            100..=399 => return None,
            // Anything else is invalid HTTP.
            _ => E::InvalidResponse(Arc::new(BadStatusCode(status_code)) as _),
        })
    }
}

impl tor_error::HasKind for UploadError {
    fn kind(&self) -> tor_error::ErrorKind {
        use UploadError as E;
        use tor_error::ErrorKind as EK;
        match self {
            E::Rejected(_) => EK::TorDocumentRejected,
            E::Connect(_) => EK::LocalNetworkError,
            E::InvalidResponse(_) => EK::TorProtocolViolation,
            E::Timeout => EK::TorNetworkTimeout,
            E::Deferred { .. } => EK::RelayTooBusy,
            E::SendRequest(e) => e.kind(),
            E::DocumentFailedPermanently(e) => e.kind(),
            E::Other(e) => e.kind(),
            E::Bug(e) => e.kind(),
        }
    }
}