rama_http/service/fs/serve_dir/
mod.rs

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