use crate::{
Layer, Service,
cli::ForwardKind,
combinators::{Either, Either3},
error::{BoxError, BoxErrorExt, ErrorExt},
http::BodyLimitLayer,
http::{
Request, Response, Version,
headers::exotic::XClacksOverhead,
layer::set_header::SetResponseHeaderLayer,
layer::{
into_response::IntoResponseService, required_header::AddRequiredResponseHeadersLayer,
trace::TraceLayer,
},
server::HttpServer,
service::{
fs::{DirectoryServeMode, ServeDir, ServeDirSymlinkPolicy, ServeFile},
web::response::{Html, IntoResponse},
},
},
layer::limit::policy::UnlimitedPolicy,
layer::{ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::ConcurrentPolicy},
proxy::haproxy::server::HaProxyLayer,
rt::Executor,
service::StaticOutput,
tcp::TcpStream,
telemetry::tracing,
ua::layer::classifier::UserAgentClassifierLayer,
utils::octets::mib,
};
use std::{convert::Infallible, path::PathBuf, sync::Arc, time::Duration};
core::cfg_select! {
feature = "boring" => {
use crate::tls::boring::server::TlsAcceptorLayer;
}
feature = "rustls" => {
use crate::tls::rustls::server::TlsAcceptorLayer;
}
_ => {}
}
#[cfg(any(feature = "boring", feature = "rustls"))]
use crate::tls::server::TlsServerConfig;
#[derive(Debug, Clone)]
pub struct FsServiceBuilder<H> {
concurrent_limit: usize,
body_limit: usize,
timeout: Duration,
forward: Option<ForwardKind>,
#[cfg(any(feature = "rustls", feature = "boring"))]
tls_server_config: Option<TlsServerConfig>,
http_version: Option<Version>,
http_service_builder: H,
content_path: Option<PathBuf>,
dir_serve_mode: DirectoryServeMode,
html_as_default_extension: bool,
symlink_policy: ServeDirSymlinkPolicy,
}
impl Default for FsServiceBuilder<()> {
fn default() -> Self {
Self {
concurrent_limit: 0,
body_limit: mib(1),
timeout: Duration::ZERO,
forward: None,
#[cfg(any(feature = "rustls", feature = "boring"))]
tls_server_config: None,
http_version: None,
http_service_builder: (),
content_path: None,
dir_serve_mode: DirectoryServeMode::HtmlFileList,
html_as_default_extension: false,
symlink_policy: ServeDirSymlinkPolicy::default(),
}
}
}
impl FsServiceBuilder<()> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl<H> FsServiceBuilder<H> {
rama_utils::macros::generate_set_and_with! {
pub fn concurrent(mut self, limit: usize) -> Self {
self.concurrent_limit = limit;
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn body_limit(mut self, limit: usize) -> Self {
self.body_limit = limit;
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn forward(mut self, kind: Option<ForwardKind>) -> Self {
self.forward = kind;
self
}
}
#[cfg(any(feature = "rustls", feature = "boring"))]
rama_utils::macros::generate_set_and_with! {
pub fn tls_server_config(mut self, cfg: Option<TlsServerConfig>) -> Self {
self.tls_server_config = cfg;
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn http_version(mut self, version: Option<Version>) -> Self {
self.http_version = version;
self
}
}
#[must_use]
pub fn with_http_layer<H2>(self, layer: H2) -> FsServiceBuilder<(H, H2)> {
FsServiceBuilder {
concurrent_limit: self.concurrent_limit,
body_limit: self.body_limit,
timeout: self.timeout,
forward: self.forward,
#[cfg(any(feature = "rustls", feature = "boring"))]
tls_server_config: self.tls_server_config,
http_version: self.http_version,
http_service_builder: (self.http_service_builder, layer),
content_path: self.content_path,
dir_serve_mode: self.dir_serve_mode,
html_as_default_extension: self.html_as_default_extension,
symlink_policy: self.symlink_policy,
}
}
rama_utils::macros::generate_set_and_with! {
pub fn content_path(mut self, path: impl Into<PathBuf>) -> Self {
self.content_path = Some(path.into());
self
}
}
#[must_use]
pub fn maybe_with_content_path<P: Into<PathBuf>>(mut self, path: Option<P>) -> Self {
self.content_path = path.map(Into::into);
self
}
pub fn maybe_set_content_path<P: Into<PathBuf>>(&mut self, path: Option<P>) -> &mut Self {
self.content_path = path.map(Into::into);
self
}
rama_utils::macros::generate_set_and_with! {
pub fn directory_serve_mode(mut self, mode: DirectoryServeMode) -> Self {
self.dir_serve_mode = mode;
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn html_as_default_extension(mut self, html_as_default_extension: bool) -> Self {
self.html_as_default_extension = html_as_default_extension;
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn symlink_policy(mut self, policy: ServeDirSymlinkPolicy) -> Self {
self.symlink_policy = policy;
self
}
}
}
impl<H> FsServiceBuilder<H>
where
H: Layer<ServeService, Service: Service<Request, Output = Response, Error: Into<BoxError>>>,
{
pub fn build(
self,
executor: Executor,
) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
let tcp_forwarded_layer = match &self.forward {
Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
_ => None,
};
let http_service = Arc::new(self.build_http()?);
let tcp_service_builder = (
ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
LimitLayer::new(if self.concurrent_limit > 0 {
Either::A(ConcurrentPolicy::max(self.concurrent_limit))
} else {
Either::B(UnlimitedPolicy::new())
}),
if !self.timeout.is_zero() {
TimeoutLayer::new(self.timeout)
} else {
TimeoutLayer::never()
},
tcp_forwarded_layer,
BodyLimitLayer::request_only(self.body_limit),
#[cfg(any(feature = "rustls", feature = "boring"))]
self.tls_server_config
.map(|cfg| TlsAcceptorLayer::new(cfg).with_store_client_hello(true)),
);
let http_transport_service = match self.http_version {
Some(Version::HTTP_2) => Either3::A(HttpServer::new_h2(executor).service(http_service)),
Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => {
Either3::B(HttpServer::new_http1(executor).service(http_service))
}
Some(version) => {
return Err(BoxError::from_static_str("unsupported http version")
.context_debug_field("version", version));
}
None => Either3::C(HttpServer::auto(executor).service(http_service)),
};
Ok(tcp_service_builder.into_layer(http_transport_service))
}
pub fn build_http(
&self,
) -> Result<impl Service<Request, Output: IntoResponse, Error = Infallible> + use<H>, BoxError>
{
let http_forwarded_layer = super::http_forwarded_layer(self.forward.as_ref());
let serve_service = match &self.content_path {
None => Either3::A(IntoResponseService::new(StaticOutput::new(Html(
include_str!("../../../docs/index.html"),
)))),
Some(path) if path.is_file() => {
Either3::B(ServeFile::new(path.clone()).with_symlink_policy(self.symlink_policy))
}
Some(path) if path.is_dir() => Either3::C(
ServeDir::new(path)
.with_directory_serve_mode(self.dir_serve_mode)
.with_html_as_default_extension(self.html_as_default_extension)
.with_symlink_policy(self.symlink_policy),
),
Some(path) => {
return Err(
BoxError::from_static_str("invalid path: no such file or directory")
.with_context_debug_field("path", || path.clone()),
);
}
};
let http_service = (
TraceLayer::new_for_http(),
SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
AddRequiredResponseHeadersLayer::default(),
UserAgentClassifierLayer::new(),
ConsumeErrLayer::default(),
http_forwarded_layer,
)
.into_layer(self.http_service_builder.layer(serve_service));
Ok(http_service)
}
}
type ServeStaticHtml = IntoResponseService<StaticOutput<Html<&'static str>>>;
type ServeService = Either3<ServeStaticHtml, ServeFile, ServeDir>;