1use std::cmp;
4use std::convert::Infallible;
5use std::fs::Metadata;
6use std::future::Future;
7use std::io;
8use std::path::{Path, PathBuf};
9use std::pin::Pin;
10use std::sync::Arc;
11use std::task::Poll;
12
13use bytes::{Bytes, BytesMut};
14use futures_util::future::Either;
15use futures_util::{future, ready, stream, FutureExt, Stream, StreamExt, TryFutureExt};
16use headers::{
17 AcceptRanges, ContentLength, ContentRange, ContentType, HeaderMapExt, IfModifiedSince, IfRange,
18 IfUnmodifiedSince, LastModified, Range,
19};
20use http::StatusCode;
21use hyper::Body;
22use percent_encoding::percent_decode_str;
23use tokio::fs::File as TkFile;
24use tokio::io::AsyncSeekExt;
25use tokio_util::io::poll_read_buf;
26
27use crate::filter::{Filter, FilterClone, One};
28use crate::reject::{self, Rejection};
29use crate::reply::{Reply, Response};
30
31pub fn file(path: impl Into<PathBuf>) -> impl FilterClone<Extract = One<File>, Error = Rejection> {
48 let path = Arc::new(path.into());
49 crate::any()
50 .map(move || {
51 tracing::trace!("file: {:?}", path);
52 ArcPath(path.clone())
53 })
54 .and(conditionals())
55 .and_then(file_reply)
56}
57
58pub fn dir(path: impl Into<PathBuf>) -> impl FilterClone<Extract = One<File>, Error = Rejection> {
81 let base = Arc::new(path.into());
82 crate::get()
83 .or(crate::head())
84 .unify()
85 .and(path_from_tail(base))
86 .and(conditionals())
87 .and_then(file_reply)
88}
89
90fn path_from_tail(
91 base: Arc<PathBuf>,
92) -> impl FilterClone<Extract = One<ArcPath>, Error = Rejection> {
93 crate::path::tail().and_then(move |tail: crate::path::Tail| {
94 future::ready(sanitize_path(base.as_ref(), tail.as_str())).and_then(|mut buf| async {
95 let is_dir = tokio::fs::metadata(buf.clone())
96 .await
97 .map(|m| m.is_dir())
98 .unwrap_or(false);
99
100 if is_dir {
101 tracing::debug!("dir: appending index.html to directory path");
102 buf.push("index.html");
103 }
104 tracing::trace!("dir: {:?}", buf);
105 Ok(ArcPath(Arc::new(buf)))
106 })
107 })
108}
109
110fn sanitize_path(base: impl AsRef<Path>, tail: &str) -> Result<PathBuf, Rejection> {
111 let mut buf = PathBuf::from(base.as_ref());
112 let p = match percent_decode_str(tail).decode_utf8() {
113 Ok(p) => p,
114 Err(err) => {
115 tracing::debug!("dir: failed to decode route={:?}: {:?}", tail, err);
116 return Err(reject::not_found());
117 }
118 };
119 tracing::trace!("dir? base={:?}, route={:?}", base.as_ref(), p);
120 for seg in p.split('/') {
121 if seg.starts_with("..") {
122 tracing::warn!("dir: rejecting segment starting with '..'");
123 return Err(reject::not_found());
124 } else if seg.contains('\\') {
125 tracing::warn!("dir: rejecting segment containing backslash (\\)");
126 return Err(reject::not_found());
127 } else if cfg!(windows) && seg.contains(':') {
128 tracing::warn!("dir: rejecting segment containing colon (:)");
129 return Err(reject::not_found());
130 } else {
131 buf.push(seg);
132 }
133 }
134 Ok(buf)
135}
136
137#[derive(Debug)]
138struct Conditionals {
139 if_modified_since: Option<IfModifiedSince>,
140 if_unmodified_since: Option<IfUnmodifiedSince>,
141 if_range: Option<IfRange>,
142 range: Option<Range>,
143}
144
145enum Cond {
146 NoBody(Response),
147 WithBody(Option<Range>),
148}
149
150impl Conditionals {
151 fn check(self, last_modified: Option<LastModified>) -> Cond {
152 if let Some(since) = self.if_unmodified_since {
153 let precondition = last_modified
154 .map(|time| since.precondition_passes(time.into()))
155 .unwrap_or(false);
156
157 tracing::trace!(
158 "if-unmodified-since? {:?} vs {:?} = {}",
159 since,
160 last_modified,
161 precondition
162 );
163 if !precondition {
164 let mut res = Response::new(Body::empty());
165 *res.status_mut() = StatusCode::PRECONDITION_FAILED;
166 return Cond::NoBody(res);
167 }
168 }
169
170 if let Some(since) = self.if_modified_since {
171 tracing::trace!(
172 "if-modified-since? header = {:?}, file = {:?}",
173 since,
174 last_modified
175 );
176 let unmodified = last_modified
177 .map(|time| !since.is_modified(time.into()))
178 .unwrap_or(false);
180 if unmodified {
181 let mut res = Response::new(Body::empty());
182 *res.status_mut() = StatusCode::NOT_MODIFIED;
183 return Cond::NoBody(res);
184 }
185 }
186
187 if let Some(if_range) = self.if_range {
188 tracing::trace!("if-range? {:?} vs {:?}", if_range, last_modified);
189 let can_range = !if_range.is_modified(None, last_modified.as_ref());
190
191 if !can_range {
192 return Cond::WithBody(None);
193 }
194 }
195
196 Cond::WithBody(self.range)
197 }
198}
199
200fn conditionals() -> impl Filter<Extract = One<Conditionals>, Error = Infallible> + Copy {
201 crate::header::optional2()
202 .and(crate::header::optional2())
203 .and(crate::header::optional2())
204 .and(crate::header::optional2())
205 .map(
206 |if_modified_since, if_unmodified_since, if_range, range| Conditionals {
207 if_modified_since,
208 if_unmodified_since,
209 if_range,
210 range,
211 },
212 )
213}
214
215#[derive(Debug)]
217pub struct File {
218 resp: Response,
219 path: ArcPath,
220}
221
222impl File {
223 pub fn path(&self) -> &Path {
243 self.path.as_ref()
244 }
245}
246
247#[derive(Clone, Debug)]
249struct ArcPath(Arc<PathBuf>);
250
251impl AsRef<Path> for ArcPath {
252 fn as_ref(&self) -> &Path {
253 (*self.0).as_ref()
254 }
255}
256
257impl Reply for File {
258 fn into_response(self) -> Response {
259 self.resp
260 }
261}
262
263fn file_reply(
264 path: ArcPath,
265 conditionals: Conditionals,
266) -> impl Future<Output = Result<File, Rejection>> + Send {
267 TkFile::open(path.clone()).then(move |res| match res {
268 Ok(f) => Either::Left(file_conditional(f, path, conditionals)),
269 Err(err) => {
270 let rej = match err.kind() {
271 io::ErrorKind::NotFound => {
272 tracing::debug!("file not found: {:?}", path.as_ref().display());
273 reject::not_found()
274 }
275 io::ErrorKind::PermissionDenied => {
276 tracing::warn!("file permission denied: {:?}", path.as_ref().display());
277 reject::known(FilePermissionError { _p: () })
278 }
279 _ => {
280 tracing::error!(
281 "file open error (path={:?}): {} ",
282 path.as_ref().display(),
283 err
284 );
285 reject::known(FileOpenError { _p: () })
286 }
287 };
288 Either::Right(future::err(rej))
289 }
290 })
291}
292
293async fn file_metadata(f: TkFile) -> Result<(TkFile, Metadata), Rejection> {
294 match f.metadata().await {
295 Ok(meta) => Ok((f, meta)),
296 Err(err) => {
297 tracing::debug!("file metadata error: {}", err);
298 Err(reject::not_found())
299 }
300 }
301}
302
303fn file_conditional(
304 f: TkFile,
305 path: ArcPath,
306 conditionals: Conditionals,
307) -> impl Future<Output = Result<File, Rejection>> + Send {
308 file_metadata(f).map_ok(move |(file, meta)| {
309 let mut len = meta.len();
310 let modified = meta.modified().ok().map(LastModified::from);
311
312 let resp = match conditionals.check(modified) {
313 Cond::NoBody(resp) => resp,
314 Cond::WithBody(range) => {
315 bytes_range(range, len)
316 .map(|(start, end)| {
317 let sub_len = end - start;
318 let buf_size = optimal_buf_size(&meta);
319 let stream = file_stream(file, buf_size, (start, end));
320 let body = Body::wrap_stream(stream);
321
322 let mut resp = Response::new(body);
323
324 if sub_len != len {
325 *resp.status_mut() = StatusCode::PARTIAL_CONTENT;
326 resp.headers_mut().typed_insert(
327 ContentRange::bytes(start..end, len).expect("valid ContentRange"),
328 );
329
330 len = sub_len;
331 }
332
333 let mime = mime_guess::from_path(path.as_ref()).first_or_octet_stream();
334
335 resp.headers_mut().typed_insert(ContentLength(len));
336 resp.headers_mut().typed_insert(ContentType::from(mime));
337 resp.headers_mut().typed_insert(AcceptRanges::bytes());
338
339 if let Some(last_modified) = modified {
340 resp.headers_mut().typed_insert(last_modified);
341 }
342
343 resp
344 })
345 .unwrap_or_else(|BadRange| {
346 let mut resp = Response::new(Body::empty());
348 *resp.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
349 resp.headers_mut()
350 .typed_insert(ContentRange::unsatisfied_bytes(len));
351 resp
352 })
353 }
354 };
355
356 File { resp, path }
357 })
358}
359
360struct BadRange;
361
362fn bytes_range(range: Option<Range>, max_len: u64) -> Result<(u64, u64), BadRange> {
363 use std::ops::Bound;
364
365 let range = if let Some(range) = range {
366 range
367 } else {
368 return Ok((0, max_len));
369 };
370
371 let ret = range
372 .iter()
373 .map(|(start, end)| {
374 let start = match start {
375 Bound::Unbounded => 0,
376 Bound::Included(s) => s,
377 Bound::Excluded(s) => s + 1,
378 };
379
380 let end = match end {
381 Bound::Unbounded => max_len,
382 Bound::Included(s) => {
383 if s == max_len {
385 s
386 } else {
387 s + 1
388 }
389 }
390 Bound::Excluded(s) => s,
391 };
392
393 if start < end && end <= max_len {
394 Ok((start, end))
395 } else {
396 tracing::trace!("unsatisfiable byte range: {}-{}/{}", start, end, max_len);
397 Err(BadRange)
398 }
399 })
400 .next()
401 .unwrap_or(Ok((0, max_len)));
402 ret
403}
404
405fn file_stream(
406 mut file: TkFile,
407 buf_size: usize,
408 (start, end): (u64, u64),
409) -> impl Stream<Item = Result<Bytes, io::Error>> + Send {
410 use std::io::SeekFrom;
411
412 let seek = async move {
413 if start != 0 {
414 file.seek(SeekFrom::Start(start)).await?;
415 }
416 Ok(file)
417 };
418
419 seek.into_stream()
420 .map(move |result| {
421 let mut buf = BytesMut::new();
422 let mut len = end - start;
423 let mut f = match result {
424 Ok(f) => f,
425 Err(f) => return Either::Left(stream::once(future::err(f))),
426 };
427
428 Either::Right(stream::poll_fn(move |cx| {
429 if len == 0 {
430 return Poll::Ready(None);
431 }
432 reserve_at_least(&mut buf, buf_size);
433
434 let n = match ready!(poll_read_buf(Pin::new(&mut f), cx, &mut buf)) {
435 Ok(n) => n as u64,
436 Err(err) => {
437 tracing::debug!("file read error: {}", err);
438 return Poll::Ready(Some(Err(err)));
439 }
440 };
441
442 if n == 0 {
443 tracing::debug!("file read found EOF before expected length");
444 return Poll::Ready(None);
445 }
446
447 let mut chunk = buf.split().freeze();
448 if n > len {
449 chunk = chunk.split_to(len as usize);
450 len = 0;
451 } else {
452 len -= n;
453 }
454
455 Poll::Ready(Some(Ok(chunk)))
456 }))
457 })
458 .flatten()
459}
460
461fn reserve_at_least(buf: &mut BytesMut, cap: usize) {
462 if buf.capacity() - buf.len() < cap {
463 buf.reserve(cap);
464 }
465}
466
467const DEFAULT_READ_BUF_SIZE: usize = 8_192;
468
469fn optimal_buf_size(metadata: &Metadata) -> usize {
470 let block_size = get_block_size(metadata);
471
472 cmp::min(block_size as u64, metadata.len()) as usize
475}
476
477#[cfg(unix)]
478fn get_block_size(metadata: &Metadata) -> usize {
479 use std::os::unix::fs::MetadataExt;
480 cmp::max(metadata.blksize() as usize, DEFAULT_READ_BUF_SIZE)
485}
486
487#[cfg(not(unix))]
488fn get_block_size(_metadata: &Metadata) -> usize {
489 DEFAULT_READ_BUF_SIZE
490}
491
492unit_error! {
495 pub(crate) FileOpenError: "file open error"
496}
497
498unit_error! {
499 pub(crate) FilePermissionError: "file perimission error"
500}
501
502#[cfg(test)]
503mod tests {
504 use super::sanitize_path;
505 use bytes::BytesMut;
506
507 #[test]
508 fn test_sanitize_path() {
509 let base = "/var/www";
510
511 fn p(s: &str) -> &::std::path::Path {
512 s.as_ref()
513 }
514
515 assert_eq!(
516 sanitize_path(base, "/foo.html").unwrap(),
517 p("/var/www/foo.html")
518 );
519
520 sanitize_path(base, "/../foo.html").expect_err("dot dot");
522
523 sanitize_path(base, "/C:\\/foo.html").expect_err("C:\\");
524 }
525
526 #[test]
527 fn test_reserve_at_least() {
528 let mut buf = BytesMut::new();
529 let cap = 8_192;
530
531 assert_eq!(buf.len(), 0);
532 assert_eq!(buf.capacity(), 0);
533
534 super::reserve_at_least(&mut buf, cap);
535 assert_eq!(buf.len(), 0);
536 assert_eq!(buf.capacity(), cap);
537 }
538}