1use crate::headers::encoding::{SupportedEncodings, parse_accept_encoding_headers};
2use crate::layer::set_status::SetStatus;
3use crate::mime::Mime;
4use crate::service::web::response::IntoResponse;
5use crate::{Body, Method, Request, Response, StatusCode, StreamingBody, header};
6use rama_core::Service;
7use rama_core::bytes::Bytes;
8use rama_core::error::BoxError;
9use rama_core::error::BoxErrorExt as _;
10use rama_core::telemetry::tracing;
11use rama_net::uri::util::percent_encoding::percent_decode;
12use rama_utils::include_dir::Dir;
13use std::fmt;
14use std::str::FromStr;
15use std::{
16 convert::Infallible,
17 path::{Path, PathBuf},
18};
19
20pub(crate) mod future;
21mod headers;
22mod open_file;
23
24#[cfg(test)]
25mod tests;
26
27#[derive(Clone, Debug)]
29enum DirSource {
30 Filesystem(PathBuf),
31 Embedded(Dir<'static>),
32}
33
34const DEFAULT_CAPACITY: usize = 65536;
36
37#[derive(Clone, Debug)]
50pub struct ServeDir<F = DefaultServeDirFallback> {
51 base: DirSource,
52 buf_chunk_size: usize,
53 symlink_policy: ServeDirSymlinkPolicy,
54 precompressed_variants: Option<PrecompressedVariants>,
55 variant: ServeVariant,
58 fallback: Option<F>,
59 call_fallback_on_method_not_allowed: bool,
60}
61
62impl ServeDir<DefaultServeDirFallback> {
63 pub fn new<P>(path: P) -> Self
65 where
66 P: AsRef<Path>,
67 {
68 let mut base = PathBuf::from(".");
69 base.push(path.as_ref());
70
71 Self::new_with_base(DirSource::Filesystem(base))
72 }
73
74 #[must_use]
76 pub fn new_embedded(path: Dir<'static>) -> Self {
77 Self::new_with_base(DirSource::Embedded(path))
78 }
79
80 fn new_with_base(base: DirSource) -> Self {
82 Self {
83 base,
84 buf_chunk_size: DEFAULT_CAPACITY,
85 symlink_policy: ServeDirSymlinkPolicy::default(),
86 precompressed_variants: None,
87 variant: ServeVariant::Directory {
88 serve_mode: Default::default(),
89 html_as_default_extension: false,
90 },
91 fallback: None,
92 call_fallback_on_method_not_allowed: false,
93 }
94 }
95
96 pub(crate) fn new_single_file<P>(path: P, mime: Mime) -> Self
98 where
99 P: AsRef<Path>,
100 {
101 Self {
102 base: DirSource::Filesystem(path.as_ref().to_path_buf()),
103 buf_chunk_size: DEFAULT_CAPACITY,
104 symlink_policy: ServeDirSymlinkPolicy::default(),
105 precompressed_variants: None,
106 variant: ServeVariant::SingleFile { mime },
107 fallback: None,
108 call_fallback_on_method_not_allowed: false,
109 }
110 }
111}
112
113impl<F> ServeDir<F> {
114 rama_utils::macros::generate_set_and_with! {
115 pub fn directory_serve_mode(mut self, mode: DirectoryServeMode) -> Self {
117 match &mut self.variant {
118 ServeVariant::Directory { serve_mode, .. } => {
119 *serve_mode = mode;
120 self
121 }
122 ServeVariant::SingleFile { mime: _ } => self,
123 }
124 }
125 }
126
127 rama_utils::macros::generate_set_and_with! {
128 pub fn html_as_default_extension(mut self, html_as_default_extension: bool) -> Self {
136 match &mut self.variant {
137 ServeVariant::Directory { html_as_default_extension: dst, .. } => {
138 *dst = html_as_default_extension;
139 self
140 }
141 ServeVariant::SingleFile { mime: _ } => self,
142 }
143 }
144 }
145
146 rama_utils::macros::generate_set_and_with! {
147 pub fn buf_chunk_size(mut self, chunk_size: usize) -> Self {
151 self.buf_chunk_size = chunk_size;
152 self
153 }
154 }
155
156 rama_utils::macros::generate_set_and_with! {
157 pub fn symlink_policy(mut self, policy: ServeDirSymlinkPolicy) -> Self {
163 self.symlink_policy = policy;
164 self
165 }
166 }
167
168 rama_utils::macros::generate_set_and_with! {
169 pub fn precompressed_gzip(mut self) -> Self {
180 self.precompressed_variants
181 .get_or_insert(Default::default())
182 .gzip = true;
183 self
184 }
185 }
186
187 rama_utils::macros::generate_set_and_with! {
188 pub fn precompressed_br(mut self) -> Self {
199 self.precompressed_variants
200 .get_or_insert_default()
201 .br = true;
202 self
203 }
204 }
205
206 rama_utils::macros::generate_set_and_with! {
207 pub fn precompressed_deflate(mut self) -> Self {
218 self.precompressed_variants
219 .get_or_insert_default()
220 .deflate = true;
221 self
222 }
223 }
224
225 rama_utils::macros::generate_set_and_with! {
226 pub fn precompressed_zstd(mut self) -> Self {
237 self.precompressed_variants
238 .get_or_insert_default()
239 .zstd = true;
240 self
241 }
242 }
243
244 pub fn fallback<F2>(self, new_fallback: F2) -> ServeDir<F2> {
251 ServeDir {
252 base: self.base,
253 buf_chunk_size: self.buf_chunk_size,
254 symlink_policy: self.symlink_policy,
255 precompressed_variants: self.precompressed_variants,
256 variant: self.variant,
257 fallback: Some(new_fallback),
258 call_fallback_on_method_not_allowed: self.call_fallback_on_method_not_allowed,
259 }
260 }
261
262 #[must_use]
266 pub fn not_found_service<F2>(self, new_fallback: F2) -> ServeDir<SetStatus<F2>> {
267 self.fallback(SetStatus::new(new_fallback, StatusCode::NOT_FOUND))
268 }
269
270 rama_utils::macros::generate_set_and_with! {
271 pub fn call_fallback_on_method_not_allowed(mut self, call_fallback: bool) -> Self {
275 self.call_fallback_on_method_not_allowed = call_fallback;
276 self
277 }
278 }
279
280 pub async fn try_call<ReqBody, FResBody>(
297 &self,
298 req: Request<ReqBody>,
299 ) -> Result<Response, std::io::Error>
300 where
301 F: Service<Request<ReqBody>, Output = Response<FResBody>, Error = Infallible> + Clone,
302 FResBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
303 {
304 if req.method() != Method::GET && req.method() != Method::HEAD {
305 if self.call_fallback_on_method_not_allowed
306 && let Some(fallback) = self.fallback.as_ref()
307 {
308 return future::serve_fallback(fallback, req).await;
309 }
310
311 return Ok(future::method_not_allowed());
312 }
313
314 let (mut parts, body) = req.into_parts();
319 let extensions = std::mem::take(&mut parts.extensions);
321 let req = Request::from_parts(parts, Body::empty());
322
323 let fallback_and_request = self.fallback.as_ref().map(|fallback| {
324 let mut fallback_req = Request::new(body);
325 *fallback_req.method_mut() = req.method().clone();
326 *fallback_req.uri_mut() = req.uri().clone();
327 *fallback_req.headers_mut() = req.headers().clone();
328 let (mut fallback_parts, fallback_body) = fallback_req.into_parts();
332 fallback_parts.extensions = extensions;
333 let fallback_req = Request::from_parts(fallback_parts, fallback_body);
334
335 (fallback, fallback_req)
336 });
337
338 let canonical_uri = req.uri().clone().canonicalize();
343
344 let requested_path = canonical_uri.path_or_root();
345 let Some(path_to_file) = self
346 .variant
347 .build_and_validate_path(&self.base, requested_path.as_ref())
348 else {
349 return if let Some((fallback, request)) = fallback_and_request {
350 future::serve_fallback(fallback, request).await
351 } else {
352 Ok(future::not_found())
353 };
354 };
355
356 let buf_chunk_size = self.buf_chunk_size;
357 let range_header = req
358 .headers()
359 .get(header::RANGE)
360 .and_then(|value| value.to_str().ok())
361 .map(|s| s.to_owned());
362
363 let precompression_configured = self.precompressed_variants.is_some();
364 let negotiated_encodings: Vec<_> = parse_accept_encoding_headers(
365 req.headers(),
366 self.precompressed_variants.unwrap_or_default(),
367 )
368 .collect();
369
370 let variant = self.variant.clone();
371 let open_file_result = open_file::open_file(
372 variant,
373 path_to_file,
374 req,
375 negotiated_encodings,
376 range_header.as_deref(),
377 buf_chunk_size,
378 &self.base,
379 precompression_configured,
380 self.symlink_policy,
381 )
382 .await;
383
384 future::consume_open_file_result(open_file_result, fallback_and_request).await
385 }
386}
387
388impl<ReqBody, F, FResBody> Service<Request<ReqBody>> for ServeDir<F>
389where
390 ReqBody: Send + 'static,
391 F: Service<Request<ReqBody>, Output = Response<FResBody>, Error = Infallible> + Clone,
392 FResBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
393{
394 type Output = Response;
395 type Error = Infallible;
396
397 async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
398 let result = self.try_call(req).await;
399 Ok(result.unwrap_or_else(|err| {
400 if future::should_return_not_found(&err) {
401 StatusCode::NOT_FOUND.into_response()
402 } else {
403 tracing::error!("Failed to read file: {err:?}");
404 StatusCode::INTERNAL_SERVER_ERROR.into_response()
405 }
406 }))
407 }
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
427pub enum ServeDirSymlinkPolicy {
428 #[default]
429 RejectAll,
431 AllowFinalComponent,
434 AllowAll,
436}
437
438impl fmt::Display for ServeDirSymlinkPolicy {
439 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440 write!(
441 f,
442 "{}",
443 match self {
444 Self::RejectAll => "reject-all",
445 Self::AllowFinalComponent => "allow-final-component",
446 Self::AllowAll => "allow-all",
447 }
448 )
449 }
450}
451
452impl FromStr for ServeDirSymlinkPolicy {
453 type Err = BoxError;
454
455 fn from_str(s: &str) -> Result<Self, Self::Err> {
456 rama_utils::macros::match_ignore_ascii_case_str! {
457 match(s) {
458 "reject-all" | "reject_all" => Ok(Self::RejectAll),
459 "allow-final-component" | "allow_final_component" => Ok(Self::AllowFinalComponent),
460 "allow-all" | "allow_all" => Ok(Self::AllowAll),
461 _ => Err(BoxError::from_static_str("invalid ServeDirSymlinkPolicy str")),
462 }
463 }
464 }
465}
466
467impl TryFrom<&str> for ServeDirSymlinkPolicy {
468 type Error = BoxError;
469
470 #[inline]
471 fn try_from(value: &str) -> Result<Self, Self::Error> {
472 value.parse()
473 }
474}
475
476#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
477pub enum DirectoryServeMode {
478 #[default]
479 AppendIndexHtml,
483 NotFound,
486 #[cfg(feature = "html")]
489 HtmlFileList,
490}
491
492impl fmt::Display for DirectoryServeMode {
493 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494 write!(
495 f,
496 "{}",
497 match self {
498 Self::AppendIndexHtml => "append-index",
499 Self::NotFound => "not-found",
500 #[cfg(feature = "html")]
501 Self::HtmlFileList => "html-file-list",
502 }
503 )
504 }
505}
506
507impl FromStr for DirectoryServeMode {
508 type Err = BoxError;
509
510 fn from_str(s: &str) -> Result<Self, Self::Err> {
511 rama_utils::macros::match_ignore_ascii_case_str! {
512 match(s) {
513 "append-index" | "append_index" => Ok(Self::AppendIndexHtml),
514 "not-found" | "not_found" => Ok(Self::NotFound),
515 "html-file-list" | "html_file_list" => html_file_list_from_str(),
516 _ => Err(BoxError::from_static_str("invalid DirectoryServeMode str")),
517 }
518 }
519 }
520}
521
522#[inline(always)]
523fn html_file_list_from_str() -> Result<DirectoryServeMode, BoxError> {
524 #[cfg(feature = "html")]
525 {
526 Ok(DirectoryServeMode::HtmlFileList)
527 }
528 #[cfg(not(feature = "html"))]
529 {
530 Err(BoxError::from_static_str(
531 "invalid DirectoryServeMode str: html file list requires html feature",
532 ))
533 }
534}
535
536impl TryFrom<&str> for DirectoryServeMode {
537 type Error = BoxError;
538
539 #[inline]
540 fn try_from(value: &str) -> Result<Self, Self::Error> {
541 value.parse()
542 }
543}
544
545#[derive(Clone, Debug)]
548enum ServeVariant {
549 Directory {
550 serve_mode: DirectoryServeMode,
551 html_as_default_extension: bool,
554 },
555 SingleFile {
556 mime: Mime,
557 },
558}
559
560impl ServeVariant {
561 fn build_and_validate_path(&self, source: &DirSource, requested_path: &str) -> Option<PathBuf> {
564 match self {
565 Self::Directory { .. } => {
566 let path = requested_path.trim_start_matches('/');
567
568 let path_decoded = percent_decode(path.as_ref()).decode_utf8().ok()?;
569
570 let relative = rama_utils::fs::sanitize_relative_path(&*path_decoded).ok()?;
575
576 let mut path_to_file = match source {
577 DirSource::Filesystem(base_path) => base_path.clone(),
578 DirSource::Embedded(_) => PathBuf::new(), };
580 path_to_file.push(relative);
581 Some(path_to_file)
582 }
583 Self::SingleFile { mime: _ } => match source {
584 DirSource::Filesystem(base_path) => Some(base_path.clone()),
585 DirSource::Embedded(_) => Some(PathBuf::new()), },
587 }
588 }
589}
590
591#[derive(Debug, Clone, Copy)]
593pub struct DefaultServeDirFallback(Infallible);
594
595impl<ReqBody> Service<Request<ReqBody>> for DefaultServeDirFallback
596where
597 ReqBody: Send + 'static,
598{
599 type Output = Response;
600 type Error = Infallible;
601
602 async fn serve(&self, _req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
603 match self.0 {}
604 }
605}
606
607#[derive(Clone, Copy, Debug, Default)]
608struct PrecompressedVariants {
609 gzip: bool,
610 deflate: bool,
611 br: bool,
612 zstd: bool,
613}
614
615impl SupportedEncodings for PrecompressedVariants {
616 fn gzip(&self) -> bool {
617 self.gzip
618 }
619
620 fn deflate(&self) -> bool {
621 self.deflate
622 }
623
624 fn br(&self) -> bool {
625 self.br
626 }
627
628 fn zstd(&self) -> bool {
629 self.zstd
630 }
631}