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