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