hyper_staticfile/util/
file_response_builder.rs

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