yacme_protocol/
response.rs

1//! HTTP responses which adhere to RFC 8885
2//!
3//! [RFC 8885][] does not constrain HTTP responses from the ACME service
4//! strongly, except that they should contain a [nonce][crate::jose::Nonce].
5//!
6//! The response type here also implements [`crate::fmt::AcmeFormat`] so that
7//! it can be displayed in a form similar to those in [RFC 8885][] while
8//! debugging.
9//!
10//! [RFC 8885]: https://datatracker.ietf.org/doc/html/rfc8555
11
12use std::fmt::Write;
13
14use chrono::{DateTime, Utc};
15use http::HeaderMap;
16use serde::de::DeserializeOwned;
17
18use crate::fmt::{self, HttpCase};
19use crate::jose::Nonce;
20use crate::request::Encode;
21use crate::AcmeError;
22use crate::Url;
23
24/// Helper trait for any type which can be decoded from a
25/// response from an ACME server.
26///
27/// This trait is blanket-implemetned for [`serde::de::DeserializeOwned`]
28/// so most types should implement or derive [`serde::Deserialize`]
29/// rather than implementing this type.
30pub trait Decode: Sized {
31    /// Decode an ACME response from a byte slice.
32    fn decode(data: &[u8]) -> Result<Self, AcmeError>;
33}
34
35impl<T> Decode for T
36where
37    T: DeserializeOwned,
38{
39    fn decode(data: &[u8]) -> Result<Self, AcmeError> {
40        serde_json::from_slice(data).map_err(AcmeError::de)
41    }
42}
43
44/// A HTTP response from an ACME service
45#[derive(Debug, Clone)]
46pub struct Response<T> {
47    url: Url,
48    status: http::StatusCode,
49    headers: http::HeaderMap,
50    payload: T,
51}
52
53impl<T> Response<T>
54where
55    T: Decode,
56{
57    pub(crate) async fn from_decoded_response(
58        response: reqwest::Response,
59    ) -> Result<Self, AcmeError> {
60        let url = response.url().clone().into();
61        let status = response.status();
62        let headers = response.headers().clone();
63        let body = response.bytes().await?;
64        let payload: T = T::decode(&body)?;
65
66        Ok(Response {
67            url,
68            status,
69            headers,
70            payload,
71        })
72    }
73}
74
75impl<T> Response<T> {
76    /// Response [`http::StatusCode`]
77    pub fn status(&self) -> http::StatusCode {
78        self.status
79    }
80
81    /// Destination URL from the original request.
82    pub fn url(&self) -> &Url {
83        &self.url
84    }
85
86    /// The headers returned with this response
87    pub fn headers(&self) -> &HeaderMap {
88        &self.headers
89    }
90
91    /// The seconds to wait for a retry, from now.
92    pub fn retry_after(&self) -> Option<std::time::Duration> {
93        self.headers()
94            .get(http::header::RETRY_AFTER)
95            .and_then(|v| v.to_str().ok())
96            .and_then(|v| {
97                if v.contains("GMT") {
98                    DateTime::parse_from_rfc2822(v)
99                        .map(|ts| ts.signed_duration_since(Utc::now()))
100                        .ok()
101                        .and_then(|d| d.to_std().ok())
102                } else {
103                    v.parse::<u64>().ok().map(std::time::Duration::from_secs)
104                }
105            })
106    }
107
108    /// Get the [`Nonce`] from this response.
109    ///
110    /// Normally, this is unnecessay, as [`crate::Client`] will automatically handle
111    /// and track [`Nonce`] values.
112    pub fn nonce(&self) -> Option<Nonce> {
113        crate::client::extract_nonce(&self.headers).ok()
114    }
115
116    /// The URL from the `Location` HTTP header.
117    pub fn location(&self) -> Option<Url> {
118        self.headers.get(http::header::LOCATION).map(|value| {
119            value
120                .to_str()
121                .unwrap_or_else(|_| {
122                    panic!("valid text encoding in {} header", http::header::LOCATION)
123                })
124                .parse()
125                .unwrap_or_else(|_| panic!("valid URL in {} header", http::header::LOCATION))
126        })
127    }
128
129    /// The [`mime::Mime`] from the `Content-Type` header.
130    pub fn content_type(&self) -> Option<mime::Mime> {
131        self.headers.get(http::header::CONTENT_TYPE).map(|v| {
132            v.to_str()
133                .unwrap_or_else(|_| {
134                    panic!(
135                        "valid text encoding in {} header",
136                        http::header::CONTENT_TYPE
137                    )
138                })
139                .parse()
140                .unwrap_or_else(|_| {
141                    panic!("valid MIME type in {} header", http::header::CONTENT_TYPE)
142                })
143        })
144    }
145
146    /// The response payload.
147    pub fn payload(&self) -> &T {
148        &self.payload
149    }
150
151    /// Extract just the response payload.
152    pub fn into_inner(self) -> T {
153        self.payload
154    }
155}
156
157impl<T> fmt::AcmeFormat for Response<T>
158where
159    T: Encode,
160{
161    fn fmt<W: fmt::Write>(&self, f: &mut fmt::IndentWriter<'_, W>) -> fmt::Result {
162        writeln!(
163            f,
164            "HTTP/1.1 {} {}",
165            self.status.as_u16(),
166            self.status.canonical_reason().unwrap_or("")
167        )?;
168        for (header, value) in self.headers.iter() {
169            writeln!(f, "{}: {}", header.titlecase(), value.to_str().unwrap())?;
170        }
171
172        writeln!(f)?;
173
174        write!(f, "{}", self.payload.encode().unwrap())
175    }
176}