routinator/http/
response.rs1use 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
17type ResponseBody = BoxBody<Bytes, Infallible>;
20
21
22pub struct Response(hyper::Response<ResponseBody>);
25
26impl Response {
27 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 pub fn bad_request(api: bool, message: impl fmt::Display) -> Self {
38 Self::error(api, StatusCode::BAD_REQUEST, message)
39 }
40
41 pub fn not_found(api: bool) -> Self {
43 Self::error(api, StatusCode::NOT_FOUND, "resource not found")
44 }
45
46 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 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 pub fn unsupported_media_type(api: bool, message: impl fmt::Display) -> Self {
63 Self::error(api, StatusCode::UNSUPPORTED_MEDIA_TYPE, message)
64 }
65
66 pub fn internal_server_error(api: bool) -> Self {
68 Self::error(api, StatusCode::INTERNAL_SERVER_ERROR,
69 "internal server error")
70 }
71
72 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 #[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 pub fn maybe_not_modified(
119 req: &Request,
120 etag: &str,
121 done: DateTime<Utc>,
122 ) -> Option<Response> {
123 for value in req.headers().get_all("If-None-Match").iter() {
125 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 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 pub fn into_hyper(
156 self
157 ) -> Result<hyper::Response<ResponseBody>, Infallible> {
158 Ok(self.0)
159 }
160}
161
162
163#[derive(Debug)]
166pub struct ResponseBuilder {
167 builder: Builder,
168}
169
170impl ResponseBuilder {
171 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 pub fn ok() -> Self {
182 Self::new(StatusCode::OK)
183 }
184
185 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 pub fn etag(self, etag: &str) -> Self {
194 ResponseBuilder {
195 builder: self.builder.header("ETag", etag)
196 }
197 }
198
199 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 #[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 pub fn body(self, body: impl Into<Bytes>) -> Response {
233 self.finalize(Full::new(body.into()))
234 }
235
236 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#[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
275struct 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 self.0 = self.0.trim_start();
293 if self.0.is_empty() {
294 return None
295 }
296
297 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 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 self.0 = self.0[end..].trim_start();
318
319 if self.0.starts_with(',') {
321 self.0 = self.0[1..].trim_start();
322 }
323
324 Some(res)
325 }
326}
327
328
329#[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