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}