use crate::headers::encoding::{SupportedEncodings, parse_accept_encoding_headers};
use crate::layer::set_status::SetStatus;
use crate::mime::Mime;
use crate::service::web::response::IntoResponse;
use crate::{Body, Method, Request, Response, StatusCode, StreamingBody, header};
use rama_core::Service;
use rama_core::bytes::Bytes;
use rama_core::error::BoxError;
use rama_core::error::BoxErrorExt as _;
use rama_core::telemetry::tracing;
use rama_net::uri::util::percent_encoding::percent_decode;
use rama_utils::include_dir::Dir;
use std::fmt;
use std::str::FromStr;
use std::{
convert::Infallible,
path::{Path, PathBuf},
};
pub(crate) mod future;
mod headers;
mod open_file;
#[cfg(test)]
mod tests;
#[derive(Clone, Debug)]
enum DirSource {
Filesystem(PathBuf),
Embedded(Dir<'static>),
}
const DEFAULT_CAPACITY: usize = 65536;
#[derive(Clone, Debug)]
pub struct ServeDir<F = DefaultServeDirFallback> {
base: DirSource,
buf_chunk_size: usize,
symlink_policy: ServeDirSymlinkPolicy,
precompressed_variants: Option<PrecompressedVariants>,
variant: ServeVariant,
fallback: Option<F>,
call_fallback_on_method_not_allowed: bool,
}
impl ServeDir<DefaultServeDirFallback> {
pub fn new<P>(path: P) -> Self
where
P: AsRef<Path>,
{
let mut base = PathBuf::from(".");
base.push(path.as_ref());
Self::new_with_base(DirSource::Filesystem(base))
}
#[must_use]
pub fn new_embedded(path: Dir<'static>) -> Self {
Self::new_with_base(DirSource::Embedded(path))
}
fn new_with_base(base: DirSource) -> Self {
Self {
base,
buf_chunk_size: DEFAULT_CAPACITY,
symlink_policy: ServeDirSymlinkPolicy::default(),
precompressed_variants: None,
variant: ServeVariant::Directory {
serve_mode: Default::default(),
html_as_default_extension: false,
},
fallback: None,
call_fallback_on_method_not_allowed: false,
}
}
pub(crate) fn new_single_file<P>(path: P, mime: Mime) -> Self
where
P: AsRef<Path>,
{
Self {
base: DirSource::Filesystem(path.as_ref().to_path_buf()),
buf_chunk_size: DEFAULT_CAPACITY,
symlink_policy: ServeDirSymlinkPolicy::default(),
precompressed_variants: None,
variant: ServeVariant::SingleFile { mime },
fallback: None,
call_fallback_on_method_not_allowed: false,
}
}
}
impl<F> ServeDir<F> {
rama_utils::macros::generate_set_and_with! {
pub fn directory_serve_mode(mut self, mode: DirectoryServeMode) -> Self {
match &mut self.variant {
ServeVariant::Directory { serve_mode, .. } => {
*serve_mode = mode;
self
}
ServeVariant::SingleFile { mime: _ } => self,
}
}
}
rama_utils::macros::generate_set_and_with! {
pub fn html_as_default_extension(mut self, html_as_default_extension: bool) -> Self {
match &mut self.variant {
ServeVariant::Directory { html_as_default_extension: dst, .. } => {
*dst = html_as_default_extension;
self
}
ServeVariant::SingleFile { mime: _ } => self,
}
}
}
rama_utils::macros::generate_set_and_with! {
pub fn buf_chunk_size(mut self, chunk_size: usize) -> Self {
self.buf_chunk_size = chunk_size;
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn symlink_policy(mut self, policy: ServeDirSymlinkPolicy) -> Self {
self.symlink_policy = policy;
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn precompressed_gzip(mut self) -> Self {
self.precompressed_variants
.get_or_insert(Default::default())
.gzip = true;
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn precompressed_br(mut self) -> Self {
self.precompressed_variants
.get_or_insert_default()
.br = true;
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn precompressed_deflate(mut self) -> Self {
self.precompressed_variants
.get_or_insert_default()
.deflate = true;
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn precompressed_zstd(mut self) -> Self {
self.precompressed_variants
.get_or_insert_default()
.zstd = true;
self
}
}
pub fn fallback<F2>(self, new_fallback: F2) -> ServeDir<F2> {
ServeDir {
base: self.base,
buf_chunk_size: self.buf_chunk_size,
symlink_policy: self.symlink_policy,
precompressed_variants: self.precompressed_variants,
variant: self.variant,
fallback: Some(new_fallback),
call_fallback_on_method_not_allowed: self.call_fallback_on_method_not_allowed,
}
}
#[must_use]
pub fn not_found_service<F2>(self, new_fallback: F2) -> ServeDir<SetStatus<F2>> {
self.fallback(SetStatus::new(new_fallback, StatusCode::NOT_FOUND))
}
rama_utils::macros::generate_set_and_with! {
pub fn call_fallback_on_method_not_allowed(mut self, call_fallback: bool) -> Self {
self.call_fallback_on_method_not_allowed = call_fallback;
self
}
}
pub async fn try_call<ReqBody, FResBody>(
&self,
req: Request<ReqBody>,
) -> Result<Response, std::io::Error>
where
F: Service<Request<ReqBody>, Output = Response<FResBody>, Error = Infallible> + Clone,
FResBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
{
if req.method() != Method::GET && req.method() != Method::HEAD {
if self.call_fallback_on_method_not_allowed
&& let Some(fallback) = self.fallback.as_ref()
{
return future::serve_fallback(fallback, req).await;
}
return Ok(future::method_not_allowed());
}
let (mut parts, body) = req.into_parts();
let extensions = std::mem::take(&mut parts.extensions);
let req = Request::from_parts(parts, Body::empty());
let fallback_and_request = self.fallback.as_ref().map(|fallback| {
let mut fallback_req = Request::new(body);
*fallback_req.method_mut() = req.method().clone();
*fallback_req.uri_mut() = req.uri().clone();
*fallback_req.headers_mut() = req.headers().clone();
let (mut fallback_parts, fallback_body) = fallback_req.into_parts();
fallback_parts.extensions = extensions;
let fallback_req = Request::from_parts(fallback_parts, fallback_body);
(fallback, fallback_req)
});
let canonical_uri = req.uri().clone().canonicalize();
let requested_path = canonical_uri.path_or_root();
let Some(path_to_file) = self
.variant
.build_and_validate_path(&self.base, requested_path.as_ref())
else {
return if let Some((fallback, request)) = fallback_and_request {
future::serve_fallback(fallback, request).await
} else {
Ok(future::not_found())
};
};
let buf_chunk_size = self.buf_chunk_size;
let range_header = req
.headers()
.get(header::RANGE)
.and_then(|value| value.to_str().ok())
.map(|s| s.to_owned());
let precompression_configured = self.precompressed_variants.is_some();
let negotiated_encodings: Vec<_> = parse_accept_encoding_headers(
req.headers(),
self.precompressed_variants.unwrap_or_default(),
)
.collect();
let variant = self.variant.clone();
let open_file_result = open_file::open_file(
variant,
path_to_file,
req,
negotiated_encodings,
range_header.as_deref(),
buf_chunk_size,
&self.base,
precompression_configured,
self.symlink_policy,
)
.await;
future::consume_open_file_result(open_file_result, fallback_and_request).await
}
}
impl<ReqBody, F, FResBody> Service<Request<ReqBody>> for ServeDir<F>
where
ReqBody: Send + 'static,
F: Service<Request<ReqBody>, Output = Response<FResBody>, Error = Infallible> + Clone,
FResBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
{
type Output = Response;
type Error = Infallible;
async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
let result = self.try_call(req).await;
Ok(result.unwrap_or_else(|err| {
tracing::error!("Failed to read file: {err:?}");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ServeDirSymlinkPolicy {
#[default]
RejectAll,
AllowFinalComponent,
AllowAll,
}
impl fmt::Display for ServeDirSymlinkPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::RejectAll => "reject-all",
Self::AllowFinalComponent => "allow-final-component",
Self::AllowAll => "allow-all",
}
)
}
}
impl FromStr for ServeDirSymlinkPolicy {
type Err = BoxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
rama_utils::macros::match_ignore_ascii_case_str! {
match(s) {
"reject-all" | "reject_all" => Ok(Self::RejectAll),
"allow-final-component" | "allow_final_component" => Ok(Self::AllowFinalComponent),
"allow-all" | "allow_all" => Ok(Self::AllowAll),
_ => Err(BoxError::from_static_str("invalid ServeDirSymlinkPolicy str")),
}
}
}
}
impl TryFrom<&str> for ServeDirSymlinkPolicy {
type Error = BoxError;
#[inline]
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DirectoryServeMode {
#[default]
AppendIndexHtml,
NotFound,
#[cfg(feature = "html")]
HtmlFileList,
}
impl fmt::Display for DirectoryServeMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::AppendIndexHtml => "append-index",
Self::NotFound => "not-found",
#[cfg(feature = "html")]
Self::HtmlFileList => "html-file-list",
}
)
}
}
impl FromStr for DirectoryServeMode {
type Err = BoxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
rama_utils::macros::match_ignore_ascii_case_str! {
match(s) {
"append-index" | "append_index" => Ok(Self::AppendIndexHtml),
"not-found" | "not_found" => Ok(Self::NotFound),
"html-file-list" | "html_file_list" => html_file_list_from_str(),
_ => Err(BoxError::from_static_str("invalid DirectoryServeMode str")),
}
}
}
}
#[inline(always)]
fn html_file_list_from_str() -> Result<DirectoryServeMode, BoxError> {
#[cfg(feature = "html")]
{
Ok(DirectoryServeMode::HtmlFileList)
}
#[cfg(not(feature = "html"))]
{
Err(BoxError::from_static_str(
"invalid DirectoryServeMode str: html file list requires html feature",
))
}
}
impl TryFrom<&str> for DirectoryServeMode {
type Error = BoxError;
#[inline]
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[derive(Clone, Debug)]
enum ServeVariant {
Directory {
serve_mode: DirectoryServeMode,
html_as_default_extension: bool,
},
SingleFile {
mime: Mime,
},
}
impl ServeVariant {
fn build_and_validate_path(&self, source: &DirSource, requested_path: &str) -> Option<PathBuf> {
match self {
Self::Directory { .. } => {
let path = requested_path.trim_start_matches('/');
let path_decoded = percent_decode(path.as_ref()).decode_utf8().ok()?;
let relative = rama_utils::fs::sanitize_relative_path(&*path_decoded).ok()?;
let mut path_to_file = match source {
DirSource::Filesystem(base_path) => base_path.clone(),
DirSource::Embedded(_) => PathBuf::new(), };
path_to_file.push(relative);
Some(path_to_file)
}
Self::SingleFile { mime: _ } => match source {
DirSource::Filesystem(base_path) => Some(base_path.clone()),
DirSource::Embedded(_) => Some(PathBuf::new()), },
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DefaultServeDirFallback(Infallible);
impl<ReqBody> Service<Request<ReqBody>> for DefaultServeDirFallback
where
ReqBody: Send + 'static,
{
type Output = Response;
type Error = Infallible;
async fn serve(&self, _req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
match self.0 {}
}
}
#[derive(Clone, Copy, Debug, Default)]
struct PrecompressedVariants {
gzip: bool,
deflate: bool,
br: bool,
zstd: bool,
}
impl SupportedEncodings for PrecompressedVariants {
fn gzip(&self) -> bool {
self.gzip
}
fn deflate(&self) -> bool {
self.deflate
}
fn br(&self) -> bool {
self.br
}
fn zstd(&self) -> bool {
self.zstd
}
}