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