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}