routinator/http/
response.rs

1//! Building responses.
2
3use std::fmt;
4use std::convert::Infallible;
5use chrono::{DateTime, Utc};
6use futures::stream::{Stream, StreamExt};
7use http_body_util::{BodyExt, Empty, Full, StreamBody};
8use http_body_util::combinators::BoxBody;
9use hyper::body::{Body, Bytes, Frame};
10use hyper::StatusCode;
11use hyper::http::response::Builder;
12use crate::utils::date::{parse_http_date, format_http_date};
13use crate::utils::json::JsonBuilder;
14use super::request::Request;
15
16
17//------------ ResponseBody --------------------------------------------------
18
19type ResponseBody = BoxBody<Bytes, Infallible>;
20
21
22//------------ Response ------------------------------------------------------
23
24pub struct Response(hyper::Response<ResponseBody>);
25
26impl Response {
27    /// Creates a response indicating initial validation.
28    pub fn initial_validation(api: bool) -> Self {
29        Self::error(
30            api,
31            StatusCode::SERVICE_UNAVAILABLE,
32            "Initial validation ongoing. Please wait."
33        )
34    }
35
36    /// Returns a Bad Request response.
37    pub fn bad_request(api: bool, message: impl fmt::Display) -> Self {
38        Self::error(api, StatusCode::BAD_REQUEST, message)
39    }
40
41    /// Returns a Not Modified response.
42    pub fn not_found(api: bool) -> Self {
43        Self::error(api, StatusCode::NOT_FOUND, "resource not found")
44    }
45
46    /// Returns a Not Modified response.
47    pub fn not_modified(etag: &str, done: DateTime<Utc>) -> Self {
48        ResponseBuilder::new(
49            StatusCode::NOT_MODIFIED
50        ).etag(etag).last_modified(done).empty()
51    }
52
53    /// Returns a Method Not Allowed response.
54    pub fn method_not_allowed(api: bool) -> Self {
55        Self::error(
56            api, StatusCode::METHOD_NOT_ALLOWED,
57            "method not allowed"
58        )
59    }
60
61    // Returns a Unsupported Media Type response.
62    pub fn unsupported_media_type(api: bool, message: impl fmt::Display) -> Self {
63        Self::error(api, StatusCode::UNSUPPORTED_MEDIA_TYPE, message)
64    }
65
66    // Returns a Internal Server Error.
67    pub fn internal_server_error(api: bool) -> Self {
68        Self::error(api, StatusCode::INTERNAL_SERVER_ERROR, 
69        "internal server error")
70    }
71
72    /// Creates an error response.
73    ///
74    /// If `api` is `true`, the reponse will havea JSON body, otherwise a
75    /// plain text body is used.
76    ///
77    /// The status code of the response is taken from `status` and the
78    /// error message included in the body from `message`.
79    pub fn error(
80        api: bool,
81        status: StatusCode,
82        message: impl fmt::Display
83    ) -> Self {
84        if api {
85            ResponseBuilder::new(
86                status
87            ).content_type(
88                ContentType::JSON
89            ).body(
90                JsonBuilder::build(|json| {
91                    json.member_str("error", message);
92                })
93            )
94        }
95        else {
96            ResponseBuilder::new(
97                status
98            ).content_type(
99                ContentType::TEXT
100            ).body(message.to_string())
101        }
102    }
103
104    /// Returns a Moved Permanently response pointing to the given location.
105    #[allow(dead_code)]
106    pub fn moved_permanently(location: &str) -> Self {
107        ResponseBuilder::new(StatusCode::MOVED_PERMANENTLY)
108            .content_type(ContentType::TEXT)
109            .location(location)
110            .body(format!("Moved permanently to {location}"))
111    }
112
113    /// Returns a 304 Not Modified response if appropriate.
114    ///
115    /// If either the etag or the completion time are referred to by the
116    /// request, returns the reponse. If a new response needs to be generated,
117    /// returns `None`.
118    pub fn maybe_not_modified(
119        req: &Request,
120        etag: &str,
121        done: DateTime<Utc>,
122    ) -> Option<Response> {
123        // First, check If-None-Match.
124        for value in req.headers().get_all("If-None-Match").iter() {
125            // Skip ill-formatted values. By being lazy here we may falsely
126            // return a full response, so this should be fine.
127            let value = match value.to_str() {
128                Ok(value) => value,
129                Err(_) => continue
130            };
131            let value = value.trim();
132            if value == "*" {
133                return Some(Self::not_modified(etag, done))
134            }
135            for tag in EtagsIter(value) {
136                if tag.trim() == etag {
137                    return Some(Self::not_modified(etag, done))
138                }
139            }
140        }
141
142        // Now, the If-Modified-Since header.
143        if let Some(value) = req.headers().get("If-Modified-Since") {
144            if let Some(date) = parse_http_date(value.to_str().ok()?) {
145                if date >= done {
146                    return Some(Self::not_modified(etag, done))
147                }
148            }
149        }
150
151        None
152    }
153
154    /// Converts the response into a hyper response.
155    pub fn into_hyper(
156        self
157    ) -> Result<hyper::Response<ResponseBody>, Infallible> {
158        Ok(self.0)
159    }
160}
161
162
163//------------ ResponseBuilder ----------------------------------------------
164
165#[derive(Debug)]
166pub struct ResponseBuilder {
167    builder: Builder,
168}
169
170impl ResponseBuilder {
171    /// Creates a new builder with the given status.
172    pub fn new(status: StatusCode) -> Self {
173        ResponseBuilder {
174            builder:  Builder::new().status(status).header(
175                "Access-Control-Allow-Origin", "*"
176            )
177        }
178    }
179
180    /// Creates a new builder for a 200 OK response.
181    pub fn ok() -> Self {
182        Self::new(StatusCode::OK)
183    }
184
185    /// Adds the content type header.
186    pub fn content_type(self, content_type: ContentType) -> Self {
187        ResponseBuilder {
188            builder: self.builder.header("Content-Type", content_type.0)
189        }
190    }
191
192    /// Adds the ETag header.
193    pub fn etag(self, etag: &str) -> Self {
194        ResponseBuilder {
195            builder: self.builder.header("ETag", etag)
196        }
197    }
198
199    /// Adds the Last-Modified header.
200    pub fn last_modified(self, last_modified: DateTime<Utc>) -> Self {
201        ResponseBuilder {
202            builder: self.builder.header(
203                "Last-Modified",
204                format_http_date(last_modified)
205            )
206        }
207    }
208
209    /// Adds the Location header.
210    #[allow(dead_code)]
211    pub fn location(self, location: &str) -> Self {
212        ResponseBuilder {
213            builder: self.builder.header(
214                "Location",
215                location
216            )
217        }
218    }
219
220    fn finalize<B>(self, body: B) -> Response
221    where
222        B: Body<Data = Bytes, Error = Infallible> + Send + Sync + 'static
223    {
224        Response(
225            self.builder.body(
226                body.boxed()
227            ).expect("broken HTTP response builder")
228        )
229    }
230
231    /// Finalizes the response by adding a body.
232    pub fn body(self, body: impl Into<Bytes>) -> Response {
233        self.finalize(Full::new(body.into()))
234    }
235
236    /// Finalies the response by adding an empty body.
237    pub fn empty(self) -> Response {
238        self.finalize(Empty::new())
239    }
240
241    pub fn stream<S>(self, body: S) -> Response
242    where
243        S: Stream<Item = Bytes> + Send + Sync + 'static
244    {
245        self.finalize(
246            StreamBody::new(body.map(|item| {
247                Ok(Frame::data(item))
248            }))
249        )
250    }
251}
252
253
254//------------ ContentType ---------------------------------------------------
255
256#[derive(Clone, Debug)]
257pub struct ContentType(&'static [u8]);
258
259impl ContentType {
260    pub const CSV: ContentType = ContentType(
261        b"text/csv;charset=utf-8;header=present"
262    );
263    pub const JSON: ContentType = ContentType(b"application/json");
264    pub const TEXT: ContentType = ContentType(b"text/plain;charset=utf-8");
265    pub const PROMETHEUS: ContentType = ContentType(
266        b"text/plain; version=0.0.4"
267    );
268
269    pub fn external(value: &'static [u8]) -> Self {
270        ContentType(value)
271    }
272}
273
274
275//------------ Parsing Etags -------------------------------------------------
276
277/// An iterator over the etags in an If-Not-Match header value.
278///
279/// This does not handle the "*" value.
280///
281/// One caveat: The iterator stops when it encounters bad formatting which
282/// makes this indistinguishable from reaching the end of a correctly
283/// formatted value. As a consequence, we will 304 a request that has the
284/// right tag followed by garbage.
285struct EtagsIter<'a>(&'a str);
286
287impl<'a> Iterator for EtagsIter<'a> {
288    type Item = &'a str;
289
290    fn next(&mut self) -> Option<Self::Item> {
291        // Skip white space and check if we are done.
292        self.0 = self.0.trim_start();
293        if self.0.is_empty() {
294            return None
295        }
296
297        // We either have to have a lone DQUOTE or one prefixed by W/
298        let prefix_len = if self.0.starts_with('"') {
299            1
300        }
301        else if self.0.starts_with("W/\"") {
302            3
303        }
304        else {
305            return None
306        };
307
308        // Find the end of the tag which is after the next DQUOTE.
309        let end = match self.0[prefix_len..].find('"') {
310            Some(index) => index + prefix_len + 1,
311            None => return None
312        };
313
314        let res = &self.0[0..end];
315
316        // Move past the second DQUOTE and any space.
317        self.0 = self.0[end..].trim_start();
318
319        // If we have a comma, skip over that and any space.
320        if self.0.starts_with(',') {
321            self.0 = self.0[1..].trim_start();
322        }
323
324        Some(res)
325    }
326}
327
328
329//============ Tests =========================================================
330
331#[cfg(test)]
332mod test {
333    use super::*;
334
335    #[test]
336    fn etags_iter() {
337        assert_eq!(
338            EtagsIter("\"foo\", \"bar\", \"ba,zz\"").collect::<Vec<_>>(),
339            ["\"foo\"", "\"bar\"", "\"ba,zz\""]
340        );
341        assert_eq!(
342            EtagsIter("\"foo\", W/\"bar\" , \"ba,zz\", ").collect::<Vec<_>>(),
343            ["\"foo\"", "W/\"bar\"", "\"ba,zz\""]
344        );
345    }
346}
347