1 //! File System Filters
2 
3 use std::cmp;
4 use std::error::Error as StdError;
5 use std::fs::Metadata;
6 use std::io;
7 use std::path::{Path, PathBuf};
8 use std::sync::Arc;
9 
10 use bytes::{BufMut, BytesMut};
11 use futures::future::Either;
12 use futures::{future, stream, Future, Stream};
13 use headers::{
14     AcceptRanges, ContentLength, ContentRange, ContentType, HeaderMapExt, IfModifiedSince, IfRange,
15     IfUnmodifiedSince, LastModified, Range,
16 };
17 use http::StatusCode;
18 use hyper::{Body, Chunk};
19 use mime_guess;
20 use tokio::fs::File as TkFile;
21 use tokio::io::AsyncRead;
22 use tokio_threadpool;
23 use urlencoding::decode;
24 
25 use filter::{Filter, FilterClone, One};
26 use never::Never;
27 use reject::{self, Rejection};
28 use reply::{Reply, Response};
29 
30 /// Creates a `Filter` that serves a File at the `path`.
31 ///
32 /// Does not filter out based on any information of the request. Always serves
33 /// the file at the exact `path` provided. Thus, this can be used to serve a
34 /// single file with `GET`s, but could also be used in combination with other
35 /// filters, such as after validating in `POST` request, wanting to return a
36 /// specific file as the body.
37 ///
38 /// For serving a directory, see [dir](dir).
39 ///
40 /// # Example
41 ///
42 /// ```
43 /// // Always serves this file from the file system.
44 /// let route = warp::fs::file("/www/static/app.js");
45 /// ```
46 ///
47 /// # Note
48 ///
49 /// This filter uses `tokio-fs` to serve files, which requires the server
50 /// to be run in the threadpool runtime. This is only important to remember
51 /// if starting a runtime manually.
file(path: impl Into<PathBuf>) -> impl FilterClone<Extract = One<File>, Error = Rejection>52 pub fn file(path: impl Into<PathBuf>) -> impl FilterClone<Extract = One<File>, Error = Rejection> {
53     let path = Arc::new(path.into());
54     ::any()
55         .map(move || {
56             trace!("file: {:?}", path);
57             ArcPath(path.clone())
58         })
59         .and(conditionals())
60         .and_then(file_reply)
61 }
62 
63 /// Creates a `Filter` that serves a directory at the base `path` joined
64 /// by the request path.
65 ///
66 /// This can be used to serve "static files" from a directory. By far the most
67 /// common pattern of serving static files is for `GET` requests, so this
68 /// filter automatically includes a `GET` check.
69 ///
70 /// # Example
71 ///
72 /// ```
73 /// use warp::Filter;
74 ///
75 /// // Matches requests that start with `/static`,
76 /// // and then uses the rest of that path to lookup
77 /// // and serve a file from `/www/static`.
78 /// let route = warp::path("static")
79 ///     .and(warp::fs::dir("/www/static"));
80 ///
81 /// // For example:
82 /// // - `GET /static/app.js` would serve the file `/www/static/app.js`
83 /// // - `GET /static/css/app.css` would serve the file `/www/static/css/app.css`
84 /// ```
85 ///
86 /// # Note
87 ///
88 /// This filter uses `tokio-fs` to serve files, which requires the server
89 /// to be run in the threadpool runtime. This is only important to remember
90 /// if starting a runtime manually.
dir(path: impl Into<PathBuf>) -> impl FilterClone<Extract = One<File>, Error = Rejection>91 pub fn dir(path: impl Into<PathBuf>) -> impl FilterClone<Extract = One<File>, Error = Rejection> {
92     let base = Arc::new(path.into());
93     ::get2()
94         .and(path_from_tail(base))
95         .and(conditionals())
96         .and_then(file_reply)
97 }
98 
path_from_tail( base: Arc<PathBuf>, ) -> impl FilterClone<Extract = One<ArcPath>, Error = Rejection>99 fn path_from_tail(
100     base: Arc<PathBuf>,
101 ) -> impl FilterClone<Extract = One<ArcPath>, Error = Rejection> {
102     ::path::tail()
103         .and_then(move |tail: ::path::Tail| {
104             sanitize_path(base.as_ref(), tail.as_str())
105         })
106         .and_then(|buf: PathBuf| {
107             // Checking Path::is_dir can block since it has to read from disk,
108             // so put it in a blocking() future
109             let mut buf = Some(buf);
110             future::poll_fn(move || {
111                 let is_dir = try_ready!(tokio_threadpool::blocking(|| buf
112                     .as_ref()
113                     .unwrap()
114                     .is_dir()));
115                 let mut buf = buf.take().unwrap();
116                 if is_dir {
117                     debug!("dir: appending index.html to directory path");
118                     buf.push("index.html");
119                 }
120 
121                 trace!("dir: {:?}", buf);
122 
123                 Ok(ArcPath(Arc::new(buf)).into())
124             })
125             .map_err(|blocking_err: tokio_threadpool::BlockingError| {
126                 error!(
127                     "threadpool blocking error checking buf.is_dir(): {}",
128                     blocking_err,
129                 );
130                 reject::known(FsNeedsTokioThreadpool)
131             })
132         })
133 }
134 
sanitize_path(base: impl AsRef<Path>, tail: &str) -> Result<PathBuf, Rejection>135 fn sanitize_path(base: impl AsRef<Path>, tail: &str) -> Result<PathBuf, Rejection> {
136     let mut buf = PathBuf::from(base.as_ref());
137     let p = match decode(tail) {
138         Ok(p) => p,
139         Err(err) => {
140             debug!("dir: failed to decode route={:?}: {:?}", tail, err);
141             // FromUrlEncodingError doesn't implement StdError
142             return Err(reject::not_found());
143         }
144     };
145     trace!("dir? base={:?}, route={:?}", base.as_ref(), p);
146     for seg in p.split('/') {
147         if seg.starts_with("..") {
148             warn!("dir: rejecting segment starting with '..'");
149             return Err(reject::not_found());
150         } else if seg.contains('\\') {
151             warn!("dir: rejecting segment containing with backslash (\\)");
152             return Err(reject::not_found());
153         } else {
154             buf.push(seg);
155         }
156     }
157     Ok(buf)
158 }
159 
160 #[derive(Debug)]
161 struct Conditionals {
162     if_modified_since: Option<IfModifiedSince>,
163     if_unmodified_since: Option<IfUnmodifiedSince>,
164     if_range: Option<IfRange>,
165     range: Option<Range>,
166 }
167 
168 enum Cond {
169     NoBody(Response),
170     WithBody(Option<Range>),
171 }
172 
173 impl Conditionals {
check(self, last_modified: Option<LastModified>) -> Cond174     fn check(self, last_modified: Option<LastModified>) -> Cond {
175         if let Some(since) = self.if_unmodified_since {
176             let precondition = last_modified
177                 .map(|time| since.precondition_passes(time.into()))
178                 .unwrap_or(false);
179 
180             trace!(
181                 "if-unmodified-since? {:?} vs {:?} = {}",
182                 since,
183                 last_modified,
184                 precondition
185             );
186             if !precondition {
187                 let mut res = Response::new(Body::empty());
188                 *res.status_mut() = StatusCode::PRECONDITION_FAILED;
189                 return Cond::NoBody(res);
190             }
191         }
192 
193         if let Some(since) = self.if_modified_since {
194             trace!(
195                 "if-modified-since? header = {:?}, file = {:?}",
196                 since,
197                 last_modified
198             );
199             let unmodified = last_modified
200                 .map(|time| !since.is_modified(time.into()))
201                 // no last_modified means its always modified
202                 .unwrap_or(false);
203             if unmodified {
204                 let mut res = Response::new(Body::empty());
205                 *res.status_mut() = StatusCode::NOT_MODIFIED;
206                 return Cond::NoBody(res);
207             }
208         }
209 
210         if let Some(if_range) = self.if_range {
211             trace!("if-range? {:?} vs {:?}", if_range, last_modified);
212             let can_range = !if_range.is_modified(None, last_modified.as_ref());
213 
214             if !can_range {
215                 return Cond::WithBody(None);
216             }
217         }
218 
219         Cond::WithBody(self.range)
220     }
221 }
222 
conditionals() -> impl Filter<Extract = One<Conditionals>, Error = Never> + Copy223 fn conditionals() -> impl Filter<Extract = One<Conditionals>, Error = Never> + Copy {
224     ::header::optional2()
225         .and(::header::optional2())
226         .and(::header::optional2())
227         .and(::header::optional2())
228         .map(
229             |if_modified_since, if_unmodified_since, if_range, range| Conditionals {
230                 if_modified_since,
231                 if_unmodified_since,
232                 if_range,
233                 range,
234             },
235         )
236 }
237 
238 /// A file response.
239 #[derive(Debug)]
240 pub struct File {
241     resp: Response,
242 }
243 
244 // Silly wrapper since Arc<PathBuf> doesn't implement AsRef<Path> ;_;
245 #[derive(Clone, Debug)]
246 struct ArcPath(Arc<PathBuf>);
247 
248 impl AsRef<Path> for ArcPath {
as_ref(&self) -> &Path249     fn as_ref(&self) -> &Path {
250         (*self.0).as_ref()
251     }
252 }
253 
254 impl Reply for File {
into_response(self) -> Response255     fn into_response(self) -> Response {
256         self.resp
257     }
258 }
259 
file_reply( path: ArcPath, conditionals: Conditionals, ) -> impl Future<Item = File, Error = Rejection> + Send260 fn file_reply(
261     path: ArcPath,
262     conditionals: Conditionals,
263 ) -> impl Future<Item = File, Error = Rejection> + Send {
264     TkFile::open(path.clone()).then(move |res| match res {
265         Ok(f) => Either::A(file_conditional(f, path, conditionals)),
266         Err(err) => {
267             let rej = match err.kind() {
268                 io::ErrorKind::NotFound => {
269                     debug!("file not found: {:?}", path.as_ref().display());
270                     reject::not_found()
271                 }
272                 _ => {
273                     error!(
274                         "file open error (path={:?}): {} ",
275                         path.as_ref().display(),
276                         err
277                     );
278                     reject::not_found()
279                 }
280             };
281             Either::B(future::err(rej))
282         }
283     })
284 }
285 
file_metadata(f: TkFile) -> impl Future<Item = (TkFile, Metadata), Error = Rejection>286 fn file_metadata(f: TkFile) -> impl Future<Item = (TkFile, Metadata), Error = Rejection> {
287     let mut f = Some(f);
288     future::poll_fn(move || {
289         let meta = try_ready!(f.as_mut().unwrap().poll_metadata());
290         Ok((f.take().unwrap(), meta).into())
291     })
292     .map_err(|err: ::std::io::Error| {
293         debug!("file metadata error: {}", err);
294         reject::not_found()
295     })
296 }
297 
file_conditional( f: TkFile, path: ArcPath, conditionals: Conditionals, ) -> impl Future<Item = File, Error = Rejection> + Send298 fn file_conditional(
299     f: TkFile,
300     path: ArcPath,
301     conditionals: Conditionals,
302 ) -> impl Future<Item = File, Error = Rejection> + Send {
303     file_metadata(f).map(move |(file, meta)| {
304         let mut len = meta.len();
305         let modified = meta.modified().ok().map(LastModified::from);
306 
307         let resp = match conditionals.check(modified) {
308             Cond::NoBody(resp) => resp,
309             Cond::WithBody(range) => {
310                 bytes_range(range, len)
311                     .map(|(start, end)| {
312                         let sub_len = end - start;
313                         let buf_size = optimal_buf_size(&meta);
314                         let stream = file_stream(file, buf_size, (start, end));
315                         let body = Body::wrap_stream(stream);
316 
317                         let mut resp = Response::new(body);
318 
319                         if sub_len != len {
320                             *resp.status_mut() = StatusCode::PARTIAL_CONTENT;
321                             resp.headers_mut().typed_insert(
322                                 ContentRange::bytes(start..end, len).expect("valid ContentRange"),
323                             );
324 
325                             len = sub_len;
326                         }
327 
328                         let mime = mime_guess::from_path(path.as_ref()).first_or_octet_stream();
329 
330                         resp.headers_mut().typed_insert(ContentLength(len));
331                         resp.headers_mut().typed_insert(ContentType::from(mime));
332                         resp.headers_mut().typed_insert(AcceptRanges::bytes());
333 
334                         if let Some(last_modified) = modified {
335                             resp.headers_mut().typed_insert(last_modified);
336                         }
337 
338                         resp
339                     })
340                     .unwrap_or_else(|BadRange| {
341                         // bad byte range
342                         let mut resp = Response::new(Body::empty());
343                         *resp.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
344                         resp.headers_mut()
345                             .typed_insert(ContentRange::unsatisfied_bytes(len));
346                         resp
347                     })
348             }
349         };
350 
351         File { resp }
352     })
353 }
354 
355 struct BadRange;
356 
bytes_range(range: Option<Range>, max_len: u64) -> Result<(u64, u64), BadRange>357 fn bytes_range(range: Option<Range>, max_len: u64) -> Result<(u64, u64), BadRange> {
358     use std::ops::Bound;
359 
360     let range = if let Some(range) = range {
361         range
362     } else {
363         return Ok((0, max_len));
364     };
365 
366     let ret = range
367         .iter()
368         .map(|(start, end)| {
369             let start = match start {
370                 Bound::Unbounded => 0,
371                 Bound::Included(s) => s,
372                 Bound::Excluded(s) => s + 1,
373             };
374 
375             let end = match end {
376                 Bound::Unbounded => max_len,
377                 Bound::Included(s) => s + 1,
378                 Bound::Excluded(s) => s,
379             };
380 
381             if start < end && end <= max_len {
382                 Ok((start, end))
383             } else {
384                 trace!("unsatisfiable byte range: {}-{}/{}", start, end, max_len);
385                 Err(BadRange)
386             }
387         })
388         .next()
389         .unwrap_or(Ok((0, max_len)));
390     ret
391 }
392 
file_stream( file: TkFile, buf_size: usize, (start, end): (u64, u64), ) -> impl Stream<Item = Chunk, Error = io::Error> + Send393 fn file_stream(
394     file: TkFile,
395     buf_size: usize,
396     (start, end): (u64, u64),
397 ) -> impl Stream<Item = Chunk, Error = io::Error> + Send {
398     use std::io::SeekFrom;
399 
400     // seek
401     let seek = if start != 0 {
402         trace!("partial content; seeking ({}..{})", start, end);
403         Either::A(file.seek(SeekFrom::Start(start)).map(|(f, _pos)| f))
404     } else {
405         Either::B(future::ok(file))
406     };
407 
408     seek.into_stream()
409         .map(move |mut f| {
410             let mut buf = BytesMut::new();
411             let mut len = end - start;
412             stream::poll_fn(move || {
413                 if len == 0 {
414                     return Ok(None.into());
415                 }
416                 if buf.remaining_mut() < buf_size {
417                     buf.reserve(buf_size);
418                 }
419                 let n = try_ready!(f.read_buf(&mut buf).map_err(|err| {
420                     debug!("file read error: {}", err);
421                     err
422                 })) as u64;
423 
424                 if n == 0 {
425                     debug!("file read found EOF before expected length");
426                     return Ok(None.into());
427                 }
428 
429                 let mut chunk = buf.take().freeze();
430                 if n > len {
431                     chunk = chunk.split_to(len as usize);
432                     len = 0;
433                 } else {
434                     len -= n;
435                 }
436 
437                 Ok(Some(Chunk::from(chunk)).into())
438             })
439         })
440         .flatten()
441 }
442 
optimal_buf_size(metadata: &Metadata) -> usize443 fn optimal_buf_size(metadata: &Metadata) -> usize {
444     let block_size = get_block_size(metadata);
445 
446     // If file length is smaller than block size, don't waste space
447     // reserving a bigger-than-needed buffer.
448     cmp::min(block_size as u64, metadata.len()) as usize
449 }
450 
451 #[cfg(unix)]
get_block_size(metadata: &Metadata) -> usize452 fn get_block_size(metadata: &Metadata) -> usize {
453     use std::os::unix::fs::MetadataExt;
454     //TODO: blksize() returns u64, should handle bad cast...
455     //(really, a block size bigger than 4gb?)
456     metadata.blksize() as usize
457 }
458 
459 #[cfg(not(unix))]
get_block_size(_metadata: &Metadata) -> usize460 fn get_block_size(_metadata: &Metadata) -> usize {
461     8_192
462 }
463 
464 // ===== Rejections =====
465 
466 #[derive(Debug)]
467 pub(crate) struct FsNeedsTokioThreadpool;
468 
469 impl ::std::fmt::Display for FsNeedsTokioThreadpool {
fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result470     fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
471         f.write_str("File system operations require tokio threadpool runtime")
472     }
473 }
474 
475 impl StdError for FsNeedsTokioThreadpool {
description(&self) -> &str476     fn description(&self) -> &str {
477         "File system operations require tokio threadpool runtime"
478     }
479 }
480 
481 #[cfg(test)]
482 mod tests {
483     use super::sanitize_path;
484 
485     #[test]
test_sanitize_path()486     fn test_sanitize_path() {
487         let base = "/var/www";
488 
489         fn p(s: &str) -> &::std::path::Path {
490             s.as_ref()
491         }
492 
493         assert_eq!(sanitize_path(base, "/foo.html").unwrap(), p("/var/www/foo.html"));
494 
495         // bad paths
496         sanitize_path(base, "/../foo.html").expect_err("dot dot");
497 
498         sanitize_path(base, "/C:\\/foo.html").expect_err("C:\\");
499     }
500 }
501