hyper_static/
serve.rs

1use crate::streamer::{Empty, File as FileStreamer};
2use crate::Streamed;
3use bmart_derive::EnumStr;
4use hyper::{http, Response, StatusCode};
5use std::io::SeekFrom;
6use std::path::Path;
7use tokio::fs::File;
8use tokio::io::AsyncSeekExt;
9
10pub static DEFAULT_MIME_TYPE: &str = "application/octet-stream";
11
12const TIME_STR: &str = "%a, %d %b %Y %T %Z";
13
14#[derive(Debug, EnumStr, Copy, Clone, Eq, PartialEq)]
15pub enum ErrorKind {
16    Internal,
17    Forbidden,
18    NotFound,
19    BadRequest,
20}
21
22#[derive(Debug)]
23pub struct Error {
24    kind: ErrorKind,
25    source: Option<Box<dyn std::error::Error + 'static>>,
26}
27
28impl Error {
29    #[inline]
30    pub fn kind(&self) -> ErrorKind {
31        self.kind
32    }
33    #[inline]
34    pub fn bad_req() -> Self {
35        Self {
36            kind: ErrorKind::BadRequest,
37            source: None,
38        }
39    }
40    #[inline]
41    pub fn forbidden() -> Self {
42        Self {
43            kind: ErrorKind::Forbidden,
44            source: None,
45        }
46    }
47    #[inline]
48    pub fn internal(source: impl std::error::Error + 'static) -> Self {
49        Self {
50            kind: ErrorKind::Forbidden,
51            source: Some(Box::new(source)),
52        }
53    }
54}
55
56impl From<Error> for Result<Streamed, http::Error> {
57    fn from(err: Error) -> Self {
58        let code = match err.kind() {
59            ErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
60            ErrorKind::Forbidden => StatusCode::FORBIDDEN,
61            ErrorKind::NotFound => StatusCode::NOT_FOUND,
62            ErrorKind::BadRequest => StatusCode::BAD_REQUEST,
63        };
64        Response::builder()
65            .status(code)
66            .body(Box::pin(Empty::new()))
67    }
68}
69
70impl std::fmt::Display for Error {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "parse error")
73    }
74}
75
76impl std::error::Error for Error {
77    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
78        self.source.as_ref().map(AsRef::as_ref)
79    }
80}
81
82struct Range {
83    start: u64,
84    end: Option<u64>,
85}
86
87#[inline]
88fn parse_range(range_hdr: &hyper::header::HeaderValue) -> Result<Range, Error> {
89    let hdr = range_hdr.to_str().map_err(|_| Error::bad_req())?;
90    let mut sp = hdr.splitn(2, '=');
91    let units = sp.next().unwrap();
92    if units == "bytes" {
93        let range = sp.next().ok_or_else(Error::bad_req)?;
94        let mut sp_range = range.splitn(2, '-');
95        let start: u64 = sp_range
96            .next()
97            .unwrap()
98            .parse()
99            .map_err(|_| Error::bad_req())?;
100        let end: Option<u64> = if let Some(end) = sp_range.next() {
101            if end.is_empty() {
102                None
103            } else {
104                Some(end.parse().map_err(|_| Error::bad_req())?)
105            }
106        } else {
107            None
108        };
109        Ok(Range { start, end })
110    } else {
111        Err(Error::bad_req())
112    }
113}
114
115#[inline]
116fn etag_match(inm_hdr: &hyper::header::HeaderValue, etag: &str) -> Result<bool, Error> {
117    let hdr = inm_hdr.to_str().map_err(|_| Error::bad_req())?;
118    for t in hdr.split(',') {
119        if t.trim() == etag {
120            return Ok(true);
121        }
122    }
123    Ok(false)
124}
125
126macro_rules! resp {
127    ($code: expr, $lm: expr, $et: expr, $mt: expr) => {
128        Response::builder()
129            .status($code)
130            .header(hyper::header::ACCEPT_RANGES, "bytes")
131            .header(
132                hyper::header::LAST_MODIFIED,
133                $lm.with_timezone(&chrono_tz::GMT)
134                    .format(TIME_STR)
135                    .to_string(),
136            )
137            .header("ETag", $et)
138            .header(
139                hyper::header::CONTENT_TYPE,
140                $mt.unwrap_or(DEFAULT_MIME_TYPE),
141            )
142    };
143}
144
145#[allow(clippy::too_many_lines)]
146pub async fn static_file<'a>(
147    file_path: &Path,
148    mime_type: Option<&str>,
149    headers: &hyper::header::HeaderMap,
150    buf_size: usize,
151) -> Result<Result<Streamed, http::Error>, Error> {
152    macro_rules! forbidden {
153        () => {
154            return Err(Error::forbidden())
155        };
156    }
157    macro_rules! int_error {
158        ($err: expr) => {
159            return Err(Error::internal($err))
160        };
161    }
162    macro_rules! not_modified {
163        () => {
164            return Ok(Response::builder()
165                .status(StatusCode::NOT_MODIFIED)
166                .body(Box::pin(Empty::new())));
167        };
168    }
169    let range = if let Some(range_hdr) = headers.get(hyper::header::RANGE) {
170        Some(parse_range(range_hdr)?)
171    } else {
172        None
173    };
174    let (mut f, size, last_modified, etag) = match File::open(file_path).await {
175        Ok(v) => {
176            let (size, lmt) = match v.metadata().await {
177                Ok(m) => {
178                    if m.is_dir() {
179                        forbidden!();
180                    }
181                    let last_modified = match m.modified() {
182                        Ok(v) => v,
183                        Err(e) => {
184                            int_error!(e);
185                        }
186                    };
187                    (m.len(), last_modified)
188                }
189                Err(e) => {
190                    int_error!(e);
191                }
192            };
193            let last_modified: chrono::DateTime<chrono::Utc> = lmt.into();
194            let mut hasher = hashing::Sha256::new();
195            hasher.update(file_path.to_string_lossy().as_bytes());
196            hasher.update(&last_modified.timestamp().to_le_bytes());
197            hasher.update(&last_modified.timestamp_subsec_nanos().to_le_bytes());
198            (
199                v,
200                size,
201                last_modified,
202                format!(r#""{}""#, hex::encode(hasher.finalize())),
203            )
204        }
205        Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
206            forbidden!();
207        }
208        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
209            return Err(Error {
210                kind: ErrorKind::NotFound,
211                source: None,
212            });
213        }
214        Err(e) => {
215            int_error!(e);
216        }
217    };
218    if let Some(h) = headers.get(hyper::header::IF_NONE_MATCH) {
219        if etag_match(h, &etag)? {
220            not_modified!();
221        }
222    } else if let Some(h) = headers.get(hyper::header::IF_MODIFIED_SINCE) {
223        let hdr = h.to_str().map_err(|_| Error::bad_req())?;
224        let dt = chrono::DateTime::parse_from_rfc2822(hdr).map_err(|_| Error::bad_req())?;
225        if last_modified.timestamp() == dt.timestamp() {
226            not_modified!();
227        }
228    }
229    Ok(if let Some(rn) = range {
230        if rn.end.map_or_else(|| rn.start < size, |v| v >= rn.start)
231            && f.seek(SeekFrom::Start(rn.start)).await.is_ok()
232        {
233            let part_size = rn
234                .end
235                .map_or_else(|| size - rn.start, |end| end - rn.start + 1);
236            let reader = FileStreamer::new(f, buf_size);
237            resp!(StatusCode::PARTIAL_CONTENT, last_modified, etag, mime_type)
238                .header(
239                    hyper::header::CONTENT_RANGE,
240                    format!("bytes {}-{}/{}", rn.start, rn.end.unwrap_or(size - 1), size),
241                )
242                .header(hyper::header::CONTENT_LENGTH, part_size)
243                .body(Box::pin(http_body_util::StreamBody::new(
244                    reader.into_stream_sized(part_size),
245                )))
246        } else {
247            Response::builder()
248                .status(StatusCode::RANGE_NOT_SATISFIABLE)
249                .header(hyper::header::ACCEPT_RANGES, "bytes")
250                .header(hyper::header::CONTENT_RANGE, format!("*/{}", size))
251                .body(Box::pin(Empty::new()))
252        }
253    } else {
254        let reader = FileStreamer::new(f, buf_size);
255        resp!(StatusCode::OK, last_modified, etag, mime_type)
256            .header(hyper::header::CONTENT_LENGTH, size)
257            .body(Box::pin(http_body_util::StreamBody::new(
258                reader.into_stream(),
259            )))
260    })
261}
262
263mod hashing {
264    #[cfg(feature = "hashing-openssl")]
265    #[repr(transparent)]
266    pub struct Sha256(openssl::sha::Sha256);
267
268    #[cfg(feature = "hashing-openssl")]
269    impl Sha256 {
270        #[inline]
271        pub fn new() -> Self {
272            Self(openssl::sha::Sha256::new())
273        }
274
275        #[inline]
276        pub fn update(&mut self, bytes: &[u8]) {
277            self.0.update(bytes);
278        }
279
280        #[inline]
281        pub fn finalize(self) -> impl AsRef<[u8]> {
282            self.0.finish()
283        }
284    }
285
286    #[cfg(all(not(feature = "hashing-openssl"), feature = "hashing-sha2"))]
287    #[repr(transparent)]
288    pub struct Sha256(sha2::Sha256);
289
290    #[cfg(all(not(feature = "hashing-openssl"), feature = "hashing-sha2"))]
291    impl Sha256 {
292        #[inline]
293        pub fn new() -> Self {
294            use sha2::Digest;
295            Self(sha2::Sha256::new())
296        }
297
298        #[inline]
299        pub fn update(&mut self, bytes: &[u8]) {
300            use sha2::Digest;
301            self.0.update(bytes);
302        }
303
304        #[inline]
305        pub fn finalize(self) -> impl AsRef<[u8]> {
306            use sha2::Digest;
307            self.0.finalize()
308        }
309    }
310
311    #[cfg(not(any(feature = "hashing-openssl", feature = "hashing-sha2")))]
312    pub struct Sha256;
313
314    #[cfg(not(any(feature = "hashing-openssl", feature = "hashing-sha2")))]
315    impl Sha256 {
316        compile_error!(
317            "some hashing implementation should be specified via one of \"hashing-\" features"
318        );
319
320        pub fn new() -> Self {
321            unimplemented!();
322        }
323
324        #[inline]
325        pub fn update(&mut self, _bytes: &[u8]) {
326            unimplemented!();
327        }
328
329        #[inline]
330        pub fn finalize(self) -> [u8; 32] {
331            unimplemented!();
332        }
333    }
334}