warp/filters/
fs.rs

1//! File System Filters
2
3use 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
31/// Creates a `Filter` that serves a File at the `path`.
32///
33/// Does not filter out based on any information of the request. Always serves
34/// the file at the exact `path` provided. Thus, this can be used to serve a
35/// single file with `GET`s, but could also be used in combination with other
36/// filters, such as after validating in `POST` request, wanting to return a
37/// specific file as the body.
38///
39/// For serving a directory, see [dir].
40///
41/// # Example
42///
43/// ```
44/// // Always serves this file from the file system.
45/// let route = warp::fs::file("/www/static/app.js");
46/// ```
47pub 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
58/// Creates a `Filter` that serves a directory at the base `path` joined
59/// by the request path.
60///
61/// This can be used to serve "static files" from a directory. By far the most
62/// common pattern of serving static files is for `GET` requests, so this
63/// filter automatically includes a `GET` check.
64///
65/// # Example
66///
67/// ```
68/// use warp::Filter;
69///
70/// // Matches requests that start with `/static`,
71/// // and then uses the rest of that path to lookup
72/// // and serve a file from `/www/static`.
73/// let route = warp::path("static")
74///     .and(warp::fs::dir("/www/static"));
75///
76/// // For example:
77/// // - `GET /static/app.js` would serve the file `/www/static/app.js`
78/// // - `GET /static/css/app.css` would serve the file `/www/static/css/app.css`
79/// ```
80pub 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                // no last_modified means its always modified
179                .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/// A file response.
216#[derive(Debug)]
217pub struct File {
218    resp: Response,
219    path: ArcPath,
220}
221
222impl File {
223    /// Extract the `&Path` of the file this `Response` delivers.
224    ///
225    /// # Example
226    ///
227    /// The example below changes the Content-Type response header for every file called `video.mp4`.
228    ///
229    /// ```
230    /// use warp::{Filter, reply::Reply};
231    ///
232    /// let route = warp::path("static")
233    ///     .and(warp::fs::dir("/www/static"))
234    ///     .map(|reply: warp::filters::fs::File| {
235    ///         if reply.path().ends_with("video.mp4") {
236    ///             warp::reply::with_header(reply, "Content-Type", "video/mp4").into_response()
237    ///         } else {
238    ///             reply.into_response()
239    ///         }
240    ///     });
241    /// ```
242    pub fn path(&self) -> &Path {
243        self.path.as_ref()
244    }
245}
246
247// Silly wrapper since Arc<PathBuf> doesn't implement AsRef<Path> ;_;
248#[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                        // bad byte range
347                        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                    // For the special case where s == the file size
384                    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    // If file length is smaller than block size, don't waste space
473    // reserving a bigger-than-needed buffer.
474    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    //TODO: blksize() returns u64, should handle bad cast...
481    //(really, a block size bigger than 4gb?)
482
483    // Use device blocksize unless it's really small.
484    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
492// ===== Rejections =====
493
494unit_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        // bad paths
521        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}