tower_async_http/services/fs/serve_dir/
mod.rs

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