1use crate::client::{Client, RequestError};
10use crate::sig::{Scheme, SenderKey, rfc9421, signature};
11use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
12use std::fmt;
13use std::time::Duration;
14use url::Url;
15
16const ACTIVITY_JSON: &str = "application/activity+json";
17
18#[derive(Debug)]
20pub enum DeliveryError {
21 Request(RequestError),
23 Signing(String),
25 Unavailable(String),
28 Held {
31 retry_after: Duration,
33 },
34 Status {
36 status: u16,
37 retry_after: Option<Duration>,
39 body: String,
41 },
42}
43
44impl DeliveryError {
45 #[must_use]
51 pub fn is_permanent(&self) -> bool {
52 self.is_permanent_by(permanent_status)
53 }
54
55 #[must_use]
58 pub fn is_permanent_by(&self, permanent: fn(u16) -> bool) -> bool {
59 match self {
60 Self::Status { status, .. } => permanent(*status),
61 Self::Request(RequestError::Refused(_) | RequestError::InvalidUrl(_))
62 | Self::Signing(_) => true,
63 Self::Request(RequestError::TooLarge | RequestError::Network(_))
64 | Self::Unavailable(_)
65 | Self::Held { .. } => false,
66 }
67 }
68
69 #[must_use]
71 pub fn status(&self) -> Option<u16> {
72 match self {
73 Self::Status { status, .. } => Some(*status),
74 _ => None,
75 }
76 }
77
78 #[must_use]
81 pub fn retry_after(&self) -> Option<Duration> {
82 match self {
83 Self::Status { retry_after, .. } => *retry_after,
84 Self::Held { retry_after } => Some(*retry_after),
85 _ => None,
86 }
87 }
88}
89
90#[must_use]
93pub fn permanent_status(status: u16) -> bool {
94 (400..500).contains(&status) && !matches!(status, 408 | 429)
95}
96
97impl fmt::Display for DeliveryError {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 Self::Request(error) => error.fmt(f),
101 Self::Signing(error) => write!(f, "signing: {error}"),
102 Self::Unavailable(error) => write!(f, "unavailable: {error}"),
103 Self::Held { retry_after } => write!(
104 f,
105 "held: its destination keeps failing; let through in {}s",
106 retry_after.as_secs()
107 ),
108 Self::Status { status, body, .. } => write!(f, "HTTP {status}: {body}"),
109 }
110 }
111}
112
113impl std::error::Error for DeliveryError {}
114
115pub async fn deliver(
122 client: &Client,
123 inbox: &Url,
124 body: &[u8],
125 key: &SenderKey,
126 first: Scheme,
127) -> Result<Scheme, DeliveryError> {
128 deliver_with_headers(client, inbox, body, key, first, &[]).await
129}
130
131pub async fn deliver_with_headers(
138 client: &Client,
139 inbox: &Url,
140 body: &[u8],
141 key: &SenderKey,
142 first: Scheme,
143 extra: &[(String, String)],
144) -> Result<Scheme, DeliveryError> {
145 match attempt(client, inbox, body, key, first, extra).await {
146 Ok(()) => Ok(first),
147 Err(DeliveryError::Status {
148 status: 400 | 401, ..
149 }) => {
150 let second = first.other();
151 attempt(client, inbox, body, key, second, extra).await?;
152 Ok(second)
153 }
154 Err(error) => Err(error),
155 }
156}
157
158async fn attempt(
159 client: &Client,
160 inbox: &Url,
161 body: &[u8],
162 key: &SenderKey,
163 scheme: Scheme,
164 extra: &[(String, String)],
165) -> Result<(), DeliveryError> {
166 let mut headers = HeaderMap::new();
167 insert(&mut headers, "content-type", ACTIVITY_JSON)?;
168 insert(&mut headers, "accept", ACTIVITY_JSON)?;
169 for (name, value) in extra {
170 let header = HeaderName::try_from(name.as_str())
171 .map_err(|error| DeliveryError::Signing(format!("{name}: {error}")))?;
172 let value = HeaderValue::from_str(value)
173 .map_err(|error| DeliveryError::Signing(format!("{name}: {error}")))?;
174 headers.insert(header, value);
175 }
176 match scheme {
177 Scheme::DraftCavage => {
178 let signed = signature::sign_request_with_key(
180 "post",
181 inbox.as_str(),
182 body,
183 &key.key_id,
184 &key.private_key,
185 &[("content-type", ACTIVITY_JSON)],
186 chrono::Utc::now().timestamp(),
187 )
188 .map_err(|error| DeliveryError::Signing(error.to_string()))?;
189 insert(&mut headers, "date", &signed.date)?;
190 insert(&mut headers, "digest", &signed.digest)?;
191 insert(&mut headers, "signature", &signed.signature)?;
192 }
193 Scheme::Rfc9421 => {
194 let signed = rfc9421::sign_request(
195 "post",
196 inbox.as_str(),
197 Some(body),
198 &key.key_id,
199 &rfc9421::SigningKey::Rsa(&key.private_key),
200 chrono::Utc::now().timestamp(),
201 )
202 .map_err(|error| DeliveryError::Signing(error.to_string()))?;
203 insert(&mut headers, "signature-input", &signed.signature_input)?;
204 insert(&mut headers, "signature", &signed.signature)?;
205 if let Some(digest) = &signed.content_digest {
206 insert(&mut headers, "content-digest", digest)?;
208 }
209 }
210 }
211
212 let response = client
213 .post(inbox, headers, body.to_vec())
214 .await
215 .map_err(DeliveryError::Request)?;
216 if (200..300).contains(&response.status) {
217 return Ok(());
218 }
219 let retry_after = response
220 .headers
221 .get("retry-after")
222 .and_then(|value| value.to_str().ok())
223 .and_then(|value| value.trim().parse::<u64>().ok())
224 .map(Duration::from_secs);
225 let body: String = String::from_utf8_lossy(&response.body)
226 .chars()
227 .take(200)
228 .collect();
229 Err(DeliveryError::Status {
230 status: response.status,
231 retry_after,
232 body,
233 })
234}
235
236fn insert(headers: &mut HeaderMap, name: &'static str, value: &str) -> Result<(), DeliveryError> {
237 let value = HeaderValue::from_str(value)
238 .map_err(|error| DeliveryError::Signing(format!("{name}: {error}")))?;
239 headers.insert(HeaderName::from_static(name), value);
240 Ok(())
241}