hyper_staticfile_jsutf8/util/
file_response_builder.rs

1use super::{FileBytesStream, FileBytesStreamMultiRange, FileBytesStreamRange};
2use http::response::Builder as ResponseBuilder;
3use http::{header, HeaderMap, Method, Request, Response, Result, StatusCode};
4use http_range::HttpRange;
5use http_range::HttpRangeParseError;
6use hyper::Body;
7use rand::prelude::{thread_rng, SliceRandom};
8use std::fs::Metadata;
9use std::time::{Duration, SystemTime, UNIX_EPOCH};
10use tokio::fs::File;
11
12/// Minimum duration since Unix epoch we accept for file modification time.
13///
14/// This is intended to discard invalid times, specifically:
15///  - Zero values on any Unix system.
16///  - 'Epoch + 1' on NixOS.
17const MIN_VALID_MTIME: Duration = Duration::from_secs(2);
18
19const BOUNDARY_LENGTH: usize = 60;
20const BOUNDARY_CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
21
22/// Utility to build responses for serving a `tokio::fs::File`.
23///
24/// This struct allows direct access to its fields, but these fields are typically initialized by
25/// the accessors, using the builder pattern. The fields are basically a bunch of settings that
26/// determine the response details.
27#[derive(Clone, Debug, Default)]
28pub struct FileResponseBuilder {
29    /// Whether to send cache headers, and what lifespan to indicate.
30    pub cache_headers: Option<u32>,
31    /// Whether this is a `HEAD` request, with no response body.
32    pub is_head: bool,
33    /// The parsed value of the `If-Modified-Since` request header.
34    pub if_modified_since: Option<SystemTime>,
35    /// The file ranges to read, if any, otherwise we read from the beginning.
36    pub range: Option<String>,
37    /// The unparsed value of the `If-Range` request header. May match etag or last-modified.
38    pub if_range: Option<String>,
39}
40
41impl FileResponseBuilder {
42    /// Create a new builder with a default configuration.
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Apply parameters based on a request.
48    pub fn request<B>(&mut self, req: &Request<B>) -> &mut Self {
49        self.request_parts(req.method(), req.headers())
50    }
51
52    /// Apply parameters based on request parts.
53    pub fn request_parts(&mut self, method: &Method, headers: &HeaderMap) -> &mut Self {
54        self.request_method(method);
55        self.request_headers(headers);
56        self
57    }
58
59    /// Apply parameters based on a request method.
60    pub fn request_method(&mut self, method: &Method) -> &mut Self {
61        self.is_head = *method == Method::HEAD;
62        self
63    }
64
65    /// Apply parameters based on request headers.
66    pub fn request_headers(&mut self, headers: &HeaderMap) -> &mut Self {
67        self.if_modified_since_header(headers.get(header::IF_MODIFIED_SINCE));
68        self.range_header(headers.get(header::RANGE));
69        self.if_range(headers.get(header::IF_RANGE));
70        self
71    }
72
73    /// Add cache headers to responses for the given lifespan.
74    pub fn cache_headers(&mut self, value: Option<u32>) -> &mut Self {
75        self.cache_headers = value;
76        self
77    }
78
79    /// Set whether this is a `HEAD` request, with no response body.
80    pub fn is_head(&mut self, value: bool) -> &mut Self {
81        self.is_head = value;
82        self
83    }
84
85    /// Build responses for the given `If-Modified-Since` date-time.
86    pub fn if_modified_since(&mut self, value: Option<SystemTime>) -> &mut Self {
87        self.if_modified_since = value;
88        self
89    }
90
91    /// Build responses for the given `If-Modified-Since` request header value.
92    pub fn if_modified_since_header(&mut self, value: Option<&header::HeaderValue>) -> &mut Self {
93        self.if_modified_since = value
94            .and_then(|v| v.to_str().ok())
95            .and_then(|v| httpdate::parse_http_date(v).ok());
96        self
97    }
98
99    /// Build responses for the given `If-Range` request header value.
100    pub fn if_range(&mut self, value: Option<&header::HeaderValue>) -> &mut Self {
101        if let Some(s) = value.and_then(|s| s.to_str().ok()) {
102            self.if_range = Some(s.to_string());
103        }
104        self
105    }
106
107    /// Build responses for the given `Range` request header value.
108    pub fn range_header(&mut self, value: Option<&header::HeaderValue>) -> &mut Self {
109        self.range = value.and_then(|v| v.to_str().ok()).map(|v| v.to_string());
110        self
111    }
112
113    /// Build a response for the given file and metadata.
114    pub fn build(
115        &self,
116        file: File,
117        metadata: Metadata,
118        content_type: String,
119    ) -> Result<Response<Body>> {
120        let mut res = ResponseBuilder::new();
121
122        // Set `Last-Modified` and check `If-Modified-Since`.
123        let modified = metadata.modified().ok().filter(|v| {
124            v.duration_since(UNIX_EPOCH)
125                .ok()
126                .filter(|v| v >= &MIN_VALID_MTIME)
127                .is_some()
128        });
129
130        // default to false when specified, either the etag or last_modified will set
131        // it to true later.
132        let mut range_cond_ok = self.if_range.is_none();
133        if let Some(modified) = modified {
134            if let Ok(modified_unix) = modified.duration_since(UNIX_EPOCH) {
135                // Compare whole seconds only, because the HTTP date-time
136                // format also does not contain a fractional part.
137                if let Some(Ok(ims_unix)) =
138                    self.if_modified_since.map(|v| v.duration_since(UNIX_EPOCH))
139                {
140                    if modified_unix.as_secs() <= ims_unix.as_secs() {
141                        return ResponseBuilder::new()
142                            .status(StatusCode::NOT_MODIFIED)
143                            .body(Body::empty());
144                    }
145                }
146
147                let etag = format!(
148                    "W/\"{0:x}-{1:x}.{2:x}\"",
149                    metadata.len(),
150                    modified_unix.as_secs(),
151                    modified_unix.subsec_nanos()
152                );
153                if let Some(ref v) = self.if_range {
154                    if *v == etag {
155                        range_cond_ok = true;
156                    }
157                }
158
159                res = res.header(header::ETAG, etag);
160            }
161
162            let last_modified_formatted = httpdate::fmt_http_date(modified);
163            if let Some(ref v) = self.if_range {
164                if *v == last_modified_formatted {
165                    range_cond_ok = true;
166                }
167            }
168
169            res = res
170                .header(header::LAST_MODIFIED, last_modified_formatted)
171                .header(header::ACCEPT_RANGES, "bytes");
172        }
173
174        // Build remaining headers.
175        if let Some(seconds) = self.cache_headers {
176            res = res.header(
177                header::CACHE_CONTROL,
178                format!("public, max-age={}", seconds),
179            );
180        }
181
182        if self.is_head {
183            res = res.header(header::CONTENT_LENGTH, format!("{}", metadata.len()));
184            return res.status(StatusCode::OK).body(Body::empty());
185        }
186
187        let ranges = self.range.as_ref().filter(|_| range_cond_ok).and_then(|r| {
188            match HttpRange::parse(r, metadata.len()) {
189                Ok(r) => Some(Ok(r)),
190                Err(HttpRangeParseError::NoOverlap) => Some(Err(())),
191                Err(HttpRangeParseError::InvalidRange) => None,
192            }
193        });
194
195        if let Some(ranges) = ranges {
196            let ranges = match ranges {
197                Ok(r) => r,
198                Err(()) => {
199                    return res
200                        .status(StatusCode::RANGE_NOT_SATISFIABLE)
201                        .body(Body::empty());
202                }
203            };
204
205            if ranges.len() == 1 {
206                let single_span = ranges[0];
207                res = res
208                    .header(
209                        header::CONTENT_RANGE,
210                        content_range_header(&single_span, metadata.len()),
211                    )
212                    .header(header::CONTENT_LENGTH, format!("{}", single_span.length));
213
214                let body_stream = FileBytesStreamRange::new(file, single_span);
215                return res
216                    .status(StatusCode::PARTIAL_CONTENT)
217                    .body(body_stream.into_body());
218            } else if ranges.len() > 1 {
219                let mut boundary_tmp = [0u8; BOUNDARY_LENGTH];
220
221                let mut rng = thread_rng();
222                for v in boundary_tmp.iter_mut() {
223                    // won't panic since BOUNDARY_CHARS is non-empty
224                    *v = *BOUNDARY_CHARS.choose(&mut rng).unwrap();
225                }
226
227                // won't panic because boundary_tmp is guaranteed to be all ASCII
228                let boundary = std::str::from_utf8(&boundary_tmp[..]).unwrap().to_string();
229
230                res = res.header(
231                    hyper::header::CONTENT_TYPE,
232                    format!("multipart/byteranges; boundary={}", boundary),
233                );
234
235                let mut body_stream =
236                    FileBytesStreamMultiRange::new(file, ranges, boundary, metadata.len());
237                if !content_type.is_empty() {
238                    body_stream.set_content_type(&content_type);
239                }
240
241                res = res.header(
242                    hyper::header::CONTENT_LENGTH,
243                    format!("{}", body_stream.compute_length()),
244                );
245
246                return res
247                    .status(StatusCode::PARTIAL_CONTENT)
248                    .body(body_stream.into_body());
249            }
250        }
251
252        res = res.header(header::CONTENT_LENGTH, format!("{}", metadata.len()));
253        if !content_type.is_empty() {
254            res = res.header(header::CONTENT_TYPE, content_type);
255        }
256
257        // Stream the body.
258        res.status(StatusCode::OK)
259            .body(FileBytesStream::new_with_limit(file, metadata.len()).into_body())
260    }
261}
262
263fn content_range_header(r: &HttpRange, total_length: u64) -> String {
264    format!(
265        "bytes {}-{}/{}",
266        r.start,
267        r.start + r.length - 1,
268        total_length
269    )
270}