tower_http/services/fs/serve_dir/
mod.rs

1use self::future::ResponseFuture;
2use crate::{
3    body::UnsyncBoxBody,
4    content_encoding::{encodings, SupportedEncodings},
5    set_status::SetStatus,
6};
7use bytes::Bytes;
8use futures_util::FutureExt;
9use http::{header, HeaderValue, Method, Request, Response, StatusCode};
10use http_body_util::{BodyExt, Empty};
11use percent_encoding::percent_decode;
12use std::{
13    convert::Infallible,
14    io,
15    path::{Component, Path, PathBuf},
16    task::{Context, Poll},
17};
18use tower_service::Service;
19
20pub(crate) mod future;
21mod headers;
22mod open_file;
23
24#[cfg(test)]
25mod tests;
26
27// default capacity 64KiB
28const DEFAULT_CAPACITY: usize = 65536;
29
30/// Service that serves files from a given directory and all its sub directories.
31///
32/// The `Content-Type` will be guessed from the file extension.
33///
34/// An empty response with status `404 Not Found` will be returned if:
35///
36/// - The file doesn't exist
37/// - Any segment of the path contains `..`
38/// - Any segment of the path contains a backslash
39/// - On unix, any segment of the path referenced as directory is actually an
40///   existing file (`/file.html/something`)
41/// - We don't have necessary permissions to read the file
42///
43/// # Example
44///
45/// ```
46/// use tower_http::services::ServeDir;
47///
48/// // This will serve files in the "assets" directory and
49/// // its subdirectories
50/// let service = ServeDir::new("assets");
51/// ```
52#[derive(Clone, Debug)]
53pub struct ServeDir<F = DefaultServeDirFallback> {
54    base: PathBuf,
55    buf_chunk_size: usize,
56    precompressed_variants: Option<PrecompressedVariants>,
57    // This is used to specialise implementation for
58    // single files
59    variant: ServeVariant,
60    fallback: Option<F>,
61    call_fallback_on_method_not_allowed: bool,
62}
63
64impl ServeDir<DefaultServeDirFallback> {
65    /// Create a new [`ServeDir`].
66    pub fn new<P>(path: P) -> Self
67    where
68        P: AsRef<Path>,
69    {
70        let mut base = PathBuf::from(".");
71        base.push(path.as_ref());
72
73        Self {
74            base,
75            buf_chunk_size: DEFAULT_CAPACITY,
76            precompressed_variants: None,
77            variant: ServeVariant::Directory {
78                append_index_html_on_directories: true,
79            },
80            fallback: None,
81            call_fallback_on_method_not_allowed: false,
82        }
83    }
84
85    pub(crate) fn new_single_file<P>(path: P, mime: HeaderValue) -> Self
86    where
87        P: AsRef<Path>,
88    {
89        Self {
90            base: path.as_ref().to_owned(),
91            buf_chunk_size: DEFAULT_CAPACITY,
92            precompressed_variants: None,
93            variant: ServeVariant::SingleFile { mime },
94            fallback: None,
95            call_fallback_on_method_not_allowed: false,
96        }
97    }
98}
99
100impl<F> ServeDir<F> {
101    /// If the requested path is a directory append `index.html`.
102    ///
103    /// This is useful for static sites.
104    ///
105    /// Defaults to `true`.
106    pub fn append_index_html_on_directories(mut self, append: bool) -> Self {
107        match &mut self.variant {
108            ServeVariant::Directory {
109                append_index_html_on_directories,
110            } => {
111                *append_index_html_on_directories = append;
112                self
113            }
114            ServeVariant::SingleFile { mime: _ } => self,
115        }
116    }
117
118    /// Set a specific read buffer chunk size.
119    ///
120    /// The default capacity is 64kb.
121    pub fn with_buf_chunk_size(mut self, chunk_size: usize) -> Self {
122        self.buf_chunk_size = chunk_size;
123        self
124    }
125
126    /// Informs the service that it should also look for a precompressed gzip
127    /// version of _any_ file in the directory.
128    ///
129    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
130    /// a client with an `Accept-Encoding` header that allows the gzip encoding
131    /// will receive the file `dir/foo.txt.gz` instead of `dir/foo.txt`.
132    /// If the precompressed file is not available, or the client doesn't support it,
133    /// the uncompressed version will be served instead.
134    /// Both the precompressed version and the uncompressed version are expected
135    /// to be present in the directory. Different precompressed variants can be combined.
136    pub fn precompressed_gzip(mut self) -> Self {
137        self.precompressed_variants
138            .get_or_insert(Default::default())
139            .gzip = true;
140        self
141    }
142
143    /// Informs the service that it should also look for a precompressed brotli
144    /// version of _any_ file in the directory.
145    ///
146    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
147    /// a client with an `Accept-Encoding` header that allows the brotli encoding
148    /// will receive the file `dir/foo.txt.br` instead of `dir/foo.txt`.
149    /// If the precompressed file is not available, or the client doesn't support it,
150    /// the uncompressed version will be served instead.
151    /// Both the precompressed version and the uncompressed version are expected
152    /// to be present in the directory. Different precompressed variants can be combined.
153    pub fn precompressed_br(mut self) -> Self {
154        self.precompressed_variants
155            .get_or_insert(Default::default())
156            .br = true;
157        self
158    }
159
160    /// Informs the service that it should also look for a precompressed deflate
161    /// version of _any_ file in the directory.
162    ///
163    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
164    /// a client with an `Accept-Encoding` header that allows the deflate encoding
165    /// will receive the file `dir/foo.txt.zz` instead of `dir/foo.txt`.
166    /// If the precompressed file is not available, or the client doesn't support it,
167    /// the uncompressed version will be served instead.
168    /// Both the precompressed version and the uncompressed version are expected
169    /// to be present in the directory. Different precompressed variants can be combined.
170    pub fn precompressed_deflate(mut self) -> Self {
171        self.precompressed_variants
172            .get_or_insert(Default::default())
173            .deflate = true;
174        self
175    }
176
177    /// Informs the service that it should also look for a precompressed zstd
178    /// version of _any_ file in the directory.
179    ///
180    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
181    /// a client with an `Accept-Encoding` header that allows the zstd encoding
182    /// will receive the file `dir/foo.txt.zst` instead of `dir/foo.txt`.
183    /// If the precompressed file is not available, or the client doesn't support it,
184    /// the uncompressed version will be served instead.
185    /// Both the precompressed version and the uncompressed version are expected
186    /// to be present in the directory. Different precompressed variants can be combined.
187    pub fn precompressed_zstd(mut self) -> Self {
188        self.precompressed_variants
189            .get_or_insert(Default::default())
190            .zstd = true;
191        self
192    }
193
194    /// Set the fallback service.
195    ///
196    /// This service will be called if there is no file at the path of the request.
197    ///
198    /// The status code returned by the fallback will not be altered. Use
199    /// [`ServeDir::not_found_service`] to set a fallback and always respond with `404 Not Found`.
200    ///
201    /// # Example
202    ///
203    /// This can be used to respond with a different file:
204    ///
205    /// ```rust
206    /// use tower_http::services::{ServeDir, ServeFile};
207    ///
208    /// let service = ServeDir::new("assets")
209    ///     // respond with `not_found.html` for missing files
210    ///     .fallback(ServeFile::new("assets/not_found.html"));
211    /// ```
212    pub fn fallback<F2>(self, new_fallback: F2) -> ServeDir<F2> {
213        ServeDir {
214            base: self.base,
215            buf_chunk_size: self.buf_chunk_size,
216            precompressed_variants: self.precompressed_variants,
217            variant: self.variant,
218            fallback: Some(new_fallback),
219            call_fallback_on_method_not_allowed: self.call_fallback_on_method_not_allowed,
220        }
221    }
222
223    /// Set the fallback service and override the fallback's status code to `404 Not Found`.
224    ///
225    /// This service will be called if there is no file at the path of the request.
226    ///
227    /// # Example
228    ///
229    /// This can be used to respond with a different file:
230    ///
231    /// ```rust
232    /// use tower_http::services::{ServeDir, ServeFile};
233    ///
234    /// let service = ServeDir::new("assets")
235    ///     // respond with `404 Not Found` and the contents of `not_found.html` for missing files
236    ///     .not_found_service(ServeFile::new("assets/not_found.html"));
237    /// ```
238    ///
239    /// Setups like this are often found in single page applications.
240    pub fn not_found_service<F2>(self, new_fallback: F2) -> ServeDir<SetStatus<F2>> {
241        self.fallback(SetStatus::new(new_fallback, StatusCode::NOT_FOUND))
242    }
243
244    /// Customize whether or not to call the fallback for requests that aren't `GET` or `HEAD`.
245    ///
246    /// Defaults to not calling the fallback and instead returning `405 Method Not Allowed`.
247    pub fn call_fallback_on_method_not_allowed(mut self, call_fallback: bool) -> Self {
248        self.call_fallback_on_method_not_allowed = call_fallback;
249        self
250    }
251
252    /// Call the service and get a future that contains any `std::io::Error` that might have
253    /// happened.
254    ///
255    /// By default `<ServeDir as Service<_>>::call` will handle IO errors and convert them into
256    /// responses. It does that by converting [`std::io::ErrorKind::NotFound`] and
257    /// [`std::io::ErrorKind::PermissionDenied`] to `404 Not Found` and any other error to `500
258    /// Internal Server Error`. The error will also be logged with `tracing`.
259    ///
260    /// If you want to manually control how the error response is generated you can make a new
261    /// service that wraps a `ServeDir` and calls `try_call` instead of `call`.
262    ///
263    /// # Example
264    ///
265    /// ```
266    /// use tower_http::services::ServeDir;
267    /// use std::{io, convert::Infallible};
268    /// use http::{Request, Response, StatusCode};
269    /// use http_body::Body as _;
270    /// use http_body_util::{Full, BodyExt, combinators::UnsyncBoxBody};
271    /// use bytes::Bytes;
272    /// use tower::{service_fn, ServiceExt, BoxError};
273    ///
274    /// async fn serve_dir(
275    ///     request: Request<Full<Bytes>>
276    /// ) -> Result<Response<UnsyncBoxBody<Bytes, BoxError>>, Infallible> {
277    ///     let mut service = ServeDir::new("assets");
278    ///
279    ///     // You only need to worry about backpressure, and thus call `ServiceExt::ready`, if
280    ///     // your adding a fallback to `ServeDir` that cares about backpressure.
281    ///     //
282    ///     // Its shown here for demonstration but you can do `service.try_call(request)`
283    ///     // otherwise
284    ///     let ready_service = match ServiceExt::<Request<Full<Bytes>>>::ready(&mut service).await {
285    ///         Ok(ready_service) => ready_service,
286    ///         Err(infallible) => match infallible {},
287    ///     };
288    ///
289    ///     match ready_service.try_call(request).await {
290    ///         Ok(response) => {
291    ///             Ok(response.map(|body| body.map_err(Into::into).boxed_unsync()))
292    ///         }
293    ///         Err(err) => {
294    ///             let body = Full::from("Something went wrong...")
295    ///                 .map_err(Into::into)
296    ///                 .boxed_unsync();
297    ///             let response = Response::builder()
298    ///                 .status(StatusCode::INTERNAL_SERVER_ERROR)
299    ///                 .body(body)
300    ///                 .unwrap();
301    ///             Ok(response)
302    ///         }
303    ///     }
304    /// }
305    /// ```
306    pub fn try_call<ReqBody, FResBody>(
307        &mut self,
308        req: Request<ReqBody>,
309    ) -> ResponseFuture<ReqBody, F>
310    where
311        F: Service<Request<ReqBody>, Response = Response<FResBody>, Error = Infallible> + Clone,
312        F::Future: Send + 'static,
313        FResBody: http_body::Body<Data = Bytes> + Send + 'static,
314        FResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
315    {
316        if req.method() != Method::GET && req.method() != Method::HEAD {
317            if self.call_fallback_on_method_not_allowed {
318                if let Some(fallback) = &mut self.fallback {
319                    return ResponseFuture {
320                        inner: future::call_fallback(fallback, req),
321                    };
322                }
323            } else {
324                return ResponseFuture::method_not_allowed();
325            }
326        }
327
328        // `ServeDir` doesn't care about the request body but the fallback might. So move out the
329        // body and pass it to the fallback, leaving an empty body in its place
330        //
331        // this is necessary because we cannot clone bodies
332        let (mut parts, body) = req.into_parts();
333        // same goes for extensions
334        let extensions = std::mem::take(&mut parts.extensions);
335        let req = Request::from_parts(parts, Empty::<Bytes>::new());
336
337        let fallback_and_request = self.fallback.as_mut().map(|fallback| {
338            let mut fallback_req = Request::new(body);
339            *fallback_req.method_mut() = req.method().clone();
340            *fallback_req.uri_mut() = req.uri().clone();
341            *fallback_req.headers_mut() = req.headers().clone();
342            *fallback_req.extensions_mut() = extensions;
343
344            // get the ready fallback and leave a non-ready clone in its place
345            let clone = fallback.clone();
346            let fallback = std::mem::replace(fallback, clone);
347
348            (fallback, fallback_req)
349        });
350
351        let path_to_file = match self
352            .variant
353            .build_and_validate_path(&self.base, req.uri().path())
354        {
355            Some(path_to_file) => path_to_file,
356            None => {
357                return ResponseFuture::invalid_path(fallback_and_request);
358            }
359        };
360
361        let buf_chunk_size = self.buf_chunk_size;
362        let range_header = req
363            .headers()
364            .get(header::RANGE)
365            .and_then(|value| value.to_str().ok())
366            .map(|s| s.to_owned());
367
368        let negotiated_encodings: Vec<_> = encodings(
369            req.headers(),
370            self.precompressed_variants.unwrap_or_default(),
371        )
372        .collect();
373
374        let variant = self.variant.clone();
375
376        let open_file_future = Box::pin(open_file::open_file(
377            variant,
378            path_to_file,
379            req,
380            negotiated_encodings,
381            range_header,
382            buf_chunk_size,
383        ));
384
385        ResponseFuture::open_file_future(open_file_future, fallback_and_request)
386    }
387}
388
389impl<ReqBody, F, FResBody> Service<Request<ReqBody>> for ServeDir<F>
390where
391    F: Service<Request<ReqBody>, Response = Response<FResBody>, Error = Infallible> + Clone,
392    F::Future: Send + 'static,
393    FResBody: http_body::Body<Data = Bytes> + Send + 'static,
394    FResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
395{
396    type Response = Response<ResponseBody>;
397    type Error = Infallible;
398    type Future = InfallibleResponseFuture<ReqBody, F>;
399
400    #[inline]
401    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
402        if let Some(fallback) = &mut self.fallback {
403            fallback.poll_ready(cx)
404        } else {
405            Poll::Ready(Ok(()))
406        }
407    }
408
409    fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
410        let future = self
411            .try_call(req)
412            .map(|result: Result<_, _>| -> Result<_, Infallible> {
413                let response = result.unwrap_or_else(|err| {
414                    tracing::error!(error = %err, "Failed to read file");
415
416                    let body = ResponseBody::new(UnsyncBoxBody::new(
417                        Empty::new().map_err(|err| match err {}).boxed_unsync(),
418                    ));
419                    Response::builder()
420                        .status(StatusCode::INTERNAL_SERVER_ERROR)
421                        .body(body)
422                        .unwrap()
423                });
424                Ok(response)
425            } as _);
426
427        InfallibleResponseFuture::new(future)
428    }
429}
430
431opaque_future! {
432    /// Response future of [`ServeDir`].
433    pub type InfallibleResponseFuture<ReqBody, F> =
434        futures_util::future::Map<
435            ResponseFuture<ReqBody, F>,
436            fn(Result<Response<ResponseBody>, io::Error>) -> Result<Response<ResponseBody>, Infallible>,
437        >;
438}
439
440// Allow the ServeDir service to be used in the ServeFile service
441// with almost no overhead
442#[derive(Clone, Debug)]
443enum ServeVariant {
444    Directory {
445        append_index_html_on_directories: bool,
446    },
447    SingleFile {
448        mime: HeaderValue,
449    },
450}
451
452impl ServeVariant {
453    fn build_and_validate_path(&self, base_path: &Path, requested_path: &str) -> Option<PathBuf> {
454        match self {
455            ServeVariant::Directory {
456                append_index_html_on_directories: _,
457            } => {
458                let path = requested_path.trim_start_matches('/');
459
460                let path_decoded = percent_decode(path.as_ref()).decode_utf8().ok()?;
461                let path_decoded = Path::new(&*path_decoded);
462
463                let mut path_to_file = base_path.to_path_buf();
464                for component in path_decoded.components() {
465                    match component {
466                        Component::Normal(comp) => {
467                            // protect against paths like `/foo/c:/bar/baz` (#204)
468                            if Path::new(&comp)
469                                .components()
470                                .all(|c| matches!(c, Component::Normal(_)))
471                            {
472                                path_to_file.push(comp)
473                            } else {
474                                return None;
475                            }
476                        }
477                        Component::CurDir => {}
478                        Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
479                            return None;
480                        }
481                    }
482                }
483                Some(path_to_file)
484            }
485            ServeVariant::SingleFile { mime: _ } => Some(base_path.to_path_buf()),
486        }
487    }
488}
489
490opaque_body! {
491    /// Response body for [`ServeDir`] and [`ServeFile`][super::ServeFile].
492    #[derive(Default)]
493    pub type ResponseBody = UnsyncBoxBody<Bytes, io::Error>;
494}
495
496/// The default fallback service used with [`ServeDir`].
497#[derive(Debug, Clone, Copy)]
498pub struct DefaultServeDirFallback(Infallible);
499
500impl<ReqBody> Service<Request<ReqBody>> for DefaultServeDirFallback
501where
502    ReqBody: Send + 'static,
503{
504    type Response = Response<ResponseBody>;
505    type Error = Infallible;
506    type Future = InfallibleResponseFuture<ReqBody, Self>;
507
508    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
509        match self.0 {}
510    }
511
512    fn call(&mut self, _req: Request<ReqBody>) -> Self::Future {
513        match self.0 {}
514    }
515}
516
517#[derive(Clone, Copy, Debug, Default)]
518struct PrecompressedVariants {
519    gzip: bool,
520    deflate: bool,
521    br: bool,
522    zstd: bool,
523}
524
525impl SupportedEncodings for PrecompressedVariants {
526    fn gzip(&self) -> bool {
527        self.gzip
528    }
529
530    fn deflate(&self) -> bool {
531        self.deflate
532    }
533
534    fn br(&self) -> bool {
535        self.br
536    }
537
538    fn zstd(&self) -> bool {
539        self.zstd
540    }
541}