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}