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` in case the `tracing`
259 /// crate feature is enabled.
260 ///
261 /// If you want to manually control how the error response is generated you can make a new
262 /// service that wraps a `ServeDir` and calls `try_call` instead of `call`.
263 ///
264 /// # Example
265 ///
266 /// ```
267 /// use tower_http::services::ServeDir;
268 /// use std::{io, convert::Infallible};
269 /// use http::{Request, Response, StatusCode};
270 /// use http_body::Body as _;
271 /// use http_body_util::{Full, BodyExt, combinators::UnsyncBoxBody};
272 /// use bytes::Bytes;
273 /// use tower::{service_fn, ServiceExt, BoxError};
274 ///
275 /// async fn serve_dir(
276 /// request: Request<Full<Bytes>>
277 /// ) -> Result<Response<UnsyncBoxBody<Bytes, BoxError>>, Infallible> {
278 /// let mut service = ServeDir::new("assets");
279 ///
280 /// // You only need to worry about backpressure, and thus call `ServiceExt::ready`, if
281 /// // your adding a fallback to `ServeDir` that cares about backpressure.
282 /// //
283 /// // Its shown here for demonstration but you can do `service.try_call(request)`
284 /// // otherwise
285 /// let ready_service = match ServiceExt::<Request<Full<Bytes>>>::ready(&mut service).await {
286 /// Ok(ready_service) => ready_service,
287 /// Err(infallible) => match infallible {},
288 /// };
289 ///
290 /// match ready_service.try_call(request).await {
291 /// Ok(response) => {
292 /// Ok(response.map(|body| body.map_err(Into::into).boxed_unsync()))
293 /// }
294 /// Err(err) => {
295 /// let body = Full::from("Something went wrong...")
296 /// .map_err(Into::into)
297 /// .boxed_unsync();
298 /// let response = Response::builder()
299 /// .status(StatusCode::INTERNAL_SERVER_ERROR)
300 /// .body(body)
301 /// .unwrap();
302 /// Ok(response)
303 /// }
304 /// }
305 /// }
306 /// ```
307 pub fn try_call<ReqBody, FResBody>(
308 &mut self,
309 req: Request<ReqBody>,
310 ) -> ResponseFuture<ReqBody, F>
311 where
312 F: Service<Request<ReqBody>, Response = Response<FResBody>, Error = Infallible> + Clone,
313 F::Future: Send + 'static,
314 FResBody: http_body::Body<Data = Bytes> + Send + 'static,
315 FResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
316 {
317 if req.method() != Method::GET && req.method() != Method::HEAD {
318 if self.call_fallback_on_method_not_allowed {
319 if let Some(fallback) = &mut self.fallback {
320 return ResponseFuture {
321 inner: future::call_fallback(fallback, req),
322 };
323 }
324 }
325
326 return ResponseFuture::method_not_allowed();
327 }
328
329 // `ServeDir` doesn't care about the request body but the fallback might. So move out the
330 // body and pass it to the fallback, leaving an empty body in its place
331 //
332 // this is necessary because we cannot clone bodies
333 let (mut parts, body) = req.into_parts();
334 // same goes for extensions
335 let extensions = std::mem::take(&mut parts.extensions);
336 let req = Request::from_parts(parts, Empty::<Bytes>::new());
337
338 let fallback_and_request = self.fallback.as_mut().map(|fallback| {
339 let mut fallback_req = Request::new(body);
340 *fallback_req.method_mut() = req.method().clone();
341 *fallback_req.uri_mut() = req.uri().clone();
342 *fallback_req.headers_mut() = req.headers().clone();
343 *fallback_req.extensions_mut() = extensions;
344
345 // get the ready fallback and leave a non-ready clone in its place
346 let clone = fallback.clone();
347 let fallback = std::mem::replace(fallback, clone);
348
349 (fallback, fallback_req)
350 });
351
352 let path_to_file = match self
353 .variant
354 .build_and_validate_path(&self.base, req.uri().path())
355 {
356 Some(path_to_file) => path_to_file,
357 None => {
358 return ResponseFuture::invalid_path(fallback_and_request);
359 }
360 };
361
362 let buf_chunk_size = self.buf_chunk_size;
363 let range_header = req
364 .headers()
365 .get(header::RANGE)
366 .and_then(|value| value.to_str().ok())
367 .map(|s| s.to_owned());
368
369 let negotiated_encodings: Vec<_> = encodings(
370 req.headers(),
371 self.precompressed_variants.unwrap_or_default(),
372 )
373 .collect();
374
375 let variant = self.variant.clone();
376
377 let open_file_future = Box::pin(open_file::open_file(
378 variant,
379 path_to_file,
380 req,
381 negotiated_encodings,
382 range_header,
383 buf_chunk_size,
384 ));
385
386 ResponseFuture::open_file_future(open_file_future, fallback_and_request)
387 }
388}
389
390impl<ReqBody, F, FResBody> Service<Request<ReqBody>> for ServeDir<F>
391where
392 F: Service<Request<ReqBody>, Response = Response<FResBody>, Error = Infallible> + Clone,
393 F::Future: Send + 'static,
394 FResBody: http_body::Body<Data = Bytes> + Send + 'static,
395 FResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
396{
397 type Response = Response<ResponseBody>;
398 type Error = Infallible;
399 type Future = InfallibleResponseFuture<ReqBody, F>;
400
401 #[inline]
402 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
403 if let Some(fallback) = &mut self.fallback {
404 fallback.poll_ready(cx)
405 } else {
406 Poll::Ready(Ok(()))
407 }
408 }
409
410 fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
411 let future = self
412 .try_call(req)
413 .map(|result: Result<_, _>| -> Result<_, Infallible> {
414 let response = result.unwrap_or_else(|_err| {
415 #[cfg(feature = "tracing")]
416 tracing::error!(error = %_err, "Failed to read file");
417
418 let body = ResponseBody::new(UnsyncBoxBody::new(
419 Empty::new().map_err(|err| match err {}).boxed_unsync(),
420 ));
421 Response::builder()
422 .status(StatusCode::INTERNAL_SERVER_ERROR)
423 .body(body)
424 .unwrap()
425 });
426 Ok(response)
427 } as _);
428
429 InfallibleResponseFuture::new(future)
430 }
431}
432
433opaque_future! {
434 /// Response future of [`ServeDir`].
435 pub type InfallibleResponseFuture<ReqBody, F> =
436 futures_util::future::Map<
437 ResponseFuture<ReqBody, F>,
438 fn(Result<Response<ResponseBody>, io::Error>) -> Result<Response<ResponseBody>, Infallible>,
439 >;
440}
441
442// Allow the ServeDir service to be used in the ServeFile service
443// with almost no overhead
444#[derive(Clone, Debug)]
445enum ServeVariant {
446 Directory {
447 append_index_html_on_directories: bool,
448 },
449 SingleFile {
450 mime: HeaderValue,
451 },
452}
453
454impl ServeVariant {
455 fn build_and_validate_path(&self, base_path: &Path, requested_path: &str) -> Option<PathBuf> {
456 match self {
457 ServeVariant::Directory {
458 append_index_html_on_directories: _,
459 } => {
460 let path = requested_path.trim_start_matches('/');
461
462 let path_decoded = percent_decode(path.as_ref()).decode_utf8().ok()?;
463 let path_decoded = Path::new(&*path_decoded);
464
465 let mut path_to_file = base_path.to_path_buf();
466 for component in path_decoded.components() {
467 match component {
468 Component::Normal(comp) => {
469 // protect against paths like `/foo/c:/bar/baz` (#204)
470 if Path::new(&comp)
471 .components()
472 .all(|c| matches!(c, Component::Normal(_)))
473 {
474 #[cfg(windows)]
475 {
476 use std::os::windows::ffi::OsStrExt;
477 if is_reserved_dos_name(|| comp.encode_wide()) {
478 return None;
479 }
480 }
481
482 path_to_file.push(comp)
483 } else {
484 return None;
485 }
486 }
487 Component::CurDir => {}
488 Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
489 return None;
490 }
491 }
492 }
493 Some(path_to_file)
494 }
495 ServeVariant::SingleFile { mime: _ } => Some(base_path.to_path_buf()),
496 }
497 }
498}
499
500/// Check whether a component name matches a reserved Windows DOS device name.
501/// See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
502///
503/// We explicitly check for Unicode superscript characters `¹` (0x00B9), `²` (0x00B2),
504/// and `³` (0x00B3) because older character tables (ISO/IEC 8859-1) define these values,
505/// which legacy Win32 file parsing resolves natively as valid port numbers (0..9).
506///
507/// This uses an iterator and stack array to avoid allocating. A closure is used because it
508/// iterates the characters twice. The closure must return the same iterator each time it is
509/// called.
510#[cfg(any(windows, test))]
511fn is_reserved_dos_name<F, I>(mut get_iter: F) -> bool
512where
513 F: FnMut() -> I,
514 I: Iterator<Item = u16>,
515{
516 const CON: [u16; 3] = [b'C' as u16, b'O' as u16, b'N' as u16];
517 const PRN: [u16; 3] = [b'P' as u16, b'R' as u16, b'N' as u16];
518 const AUX: [u16; 3] = [b'A' as u16, b'U' as u16, b'X' as u16];
519 const NUL: [u16; 3] = [b'N' as u16, b'U' as u16, b'L' as u16];
520 const CONIN: [u16; 6] = [
521 b'C' as u16,
522 b'O' as u16,
523 b'N' as u16,
524 b'I' as u16,
525 b'N' as u16,
526 b'$' as u16,
527 ];
528 const CONOUT: [u16; 7] = [
529 b'C' as u16,
530 b'O' as u16,
531 b'N' as u16,
532 b'O' as u16,
533 b'U' as u16,
534 b'T' as u16,
535 b'$' as u16,
536 ];
537
538 const COM: [u16; 3] = [b'C' as u16, b'O' as u16, b'M' as u16];
539 const LPT: [u16; 3] = [b'L' as u16, b'P' as u16, b'T' as u16];
540
541 const ZERO: u16 = b'0' as u16;
542 const NINE: u16 = b'9' as u16;
543 const SUPERSCRIPT_ONE: u16 = 0x00B9;
544 const SUPERSCRIPT_TWO: u16 = 0x00B2;
545 const SUPERSCRIPT_THREE: u16 = 0x00B3;
546
547 fn is_whitespace(c: u16) -> bool {
548 c <= 0x7F && ((c as u8).is_ascii_whitespace() || c == 0x000B)
549 }
550
551 // In a first pass over the string, obtain the length of the basename.
552 let trimmed_len = get_iter()
553 .enumerate()
554 // We want the base name, so stop at '.' or ':' characters.
555 .take_while(|&(_idx, c)| c != b'.' as u16 && c != b':' as u16)
556 // We want to trim whitespace from the end, so ignore whitespace chars.
557 .filter(|&(_idx, c)| !is_whitespace(c))
558 // Get the last non-whitespace char before the first '.'/':' character.
559 .last()
560 // Convert index of that char into length of string.
561 .map(|(idx, _)| idx + 1)
562 .unwrap_or(0);
563
564 // If the trimmed base name is longer than 7, it cannot be a reserved name.
565 if trimmed_len > 7 {
566 return false;
567 }
568
569 // At this point, we can store the string in an array, which is more convenient to work with.
570 let mut buf = [0u16; 7];
571 get_iter()
572 .take(trimmed_len)
573 .enumerate()
574 .for_each(|(i, c)| buf[i] = c);
575
576 for b in &mut buf {
577 if *b <= 0x7F {
578 *b = (*b as u8).to_ascii_uppercase() as u16;
579 }
580 if *b == SUPERSCRIPT_ONE {
581 *b = b'1' as u16;
582 }
583 if *b == SUPERSCRIPT_TWO {
584 *b = b'2' as u16;
585 }
586 if *b == SUPERSCRIPT_THREE {
587 *b = b'3' as u16;
588 }
589 }
590 let name = &buf[..trimmed_len];
591
592 // Check basic fixed-length strings
593 if name == CON || name == PRN || name == AUX || name == NUL || name == CONIN || name == CONOUT {
594 return true;
595 }
596
597 // COMx / LPTx
598 if name.len() == 4 {
599 let prefix = &name[..3];
600 let suffix = name[3];
601
602 if (prefix == COM || prefix == LPT) && matches!(suffix, ZERO..=NINE) {
603 return true;
604 }
605 }
606
607 false
608}
609
610opaque_body! {
611 /// Response body for [`ServeDir`] and [`ServeFile`][super::ServeFile].
612 #[derive(Default)]
613 pub type ResponseBody = UnsyncBoxBody<Bytes, io::Error>;
614}
615
616/// The default fallback service used with [`ServeDir`].
617#[derive(Debug, Clone, Copy)]
618pub struct DefaultServeDirFallback(Infallible);
619
620impl<ReqBody> Service<Request<ReqBody>> for DefaultServeDirFallback
621where
622 ReqBody: Send + 'static,
623{
624 type Response = Response<ResponseBody>;
625 type Error = Infallible;
626 type Future = InfallibleResponseFuture<ReqBody, Self>;
627
628 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
629 match self.0 {}
630 }
631
632 fn call(&mut self, _req: Request<ReqBody>) -> Self::Future {
633 match self.0 {}
634 }
635}
636
637#[derive(Clone, Copy, Debug, Default)]
638struct PrecompressedVariants {
639 gzip: bool,
640 deflate: bool,
641 br: bool,
642 zstd: bool,
643}
644
645impl SupportedEncodings for PrecompressedVariants {
646 fn gzip(&self) -> bool {
647 self.gzip
648 }
649
650 fn deflate(&self) -> bool {
651 self.deflate
652 }
653
654 fn br(&self) -> bool {
655 self.br
656 }
657
658 fn zstd(&self) -> bool {
659 self.zstd
660 }
661}