1use crate::{
4 Layer, Service,
5 cli::ForwardKind,
6 combinators::{Either, Either3},
7 error::{BoxError, BoxErrorExt, ErrorExt},
8 http::BodyLimitLayer,
9 http::{
10 Request, Response, Version,
11 headers::exotic::XClacksOverhead,
12 layer::set_header::SetResponseHeaderLayer,
13 layer::{
14 into_response::IntoResponseService, required_header::AddRequiredResponseHeadersLayer,
15 trace::TraceLayer,
16 },
17 server::HttpServer,
18 service::{
19 fs::{DirectoryServeMode, ServeDir, ServeDirSymlinkPolicy, ServeFile},
20 web::response::{Html, IntoResponse},
21 },
22 },
23 layer::limit::policy::UnlimitedPolicy,
24 layer::{ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::ConcurrentPolicy},
25 proxy::haproxy::server::HaProxyLayer,
26 rt::Executor,
27 service::StaticOutput,
28 tcp::TcpStream,
29 telemetry::tracing,
30 ua::layer::classifier::UserAgentClassifierLayer,
31 utils::octets::mib,
32};
33
34use std::{convert::Infallible, path::PathBuf, sync::Arc, time::Duration};
35
36#[cfg(feature = "boring")]
37use crate::{
38 net::tls::server::ServerConfig,
39 tls::boring::server::{TlsAcceptorData, TlsAcceptorLayer},
40};
41
42#[cfg(all(feature = "rustls", not(feature = "boring")))]
43use crate::tls::rustls::server::{TlsAcceptorData, TlsAcceptorLayer};
44
45#[cfg(feature = "boring")]
46type TlsConfig = ServerConfig;
47
48#[cfg(all(feature = "rustls", not(feature = "boring")))]
49type TlsConfig = TlsAcceptorData;
50
51#[derive(Debug, Clone)]
52pub struct FsServiceBuilder<H> {
55 concurrent_limit: usize,
56 body_limit: usize,
57 timeout: Duration,
58 forward: Option<ForwardKind>,
59
60 #[cfg(any(feature = "rustls", feature = "boring"))]
61 tls_server_config: Option<TlsConfig>,
62
63 http_version: Option<Version>,
64
65 http_service_builder: H,
66
67 content_path: Option<PathBuf>,
68 dir_serve_mode: DirectoryServeMode,
69 html_as_default_extension: bool,
70 symlink_policy: ServeDirSymlinkPolicy,
71}
72
73impl Default for FsServiceBuilder<()> {
74 fn default() -> Self {
75 Self {
76 concurrent_limit: 0,
77 body_limit: mib(1),
78 timeout: Duration::ZERO,
79 forward: None,
80
81 #[cfg(any(feature = "rustls", feature = "boring"))]
82 tls_server_config: None,
83
84 http_version: None,
85
86 http_service_builder: (),
87
88 content_path: None,
89 dir_serve_mode: DirectoryServeMode::HtmlFileList,
90 html_as_default_extension: false,
91 symlink_policy: ServeDirSymlinkPolicy::default(),
92 }
93 }
94}
95
96impl FsServiceBuilder<()> {
97 #[must_use]
99 pub fn new() -> Self {
100 Self::default()
101 }
102}
103
104impl<H> FsServiceBuilder<H> {
105 rama_utils::macros::generate_set_and_with! {
106 pub fn concurrent(mut self, limit: usize) -> Self {
110 self.concurrent_limit = limit;
111 self
112 }
113 }
114
115 rama_utils::macros::generate_set_and_with! {
116 pub fn body_limit(mut self, limit: usize) -> Self {
118 self.body_limit = limit;
119 self
120 }
121 }
122
123 rama_utils::macros::generate_set_and_with! {
124 pub fn timeout(mut self, timeout: Duration) -> Self {
128 self.timeout = timeout;
129 self
130 }
131 }
132
133 rama_utils::macros::generate_set_and_with! {
134 pub fn forward(mut self, kind: Option<ForwardKind>) -> Self {
146 self.forward = kind;
147 self
148 }
149 }
150
151 #[cfg(any(feature = "rustls", feature = "boring"))]
152 rama_utils::macros::generate_set_and_with! {
153 pub fn tls_server_config(mut self, cfg: Option<TlsConfig>) -> Self {
156 self.tls_server_config = cfg;
157 self
158 }
159 }
160
161 rama_utils::macros::generate_set_and_with! {
162 pub fn http_version(mut self, version: Option<Version>) -> Self {
164 self.http_version = version;
165 self
166 }
167 }
168
169 #[must_use]
171 pub fn with_http_layer<H2>(self, layer: H2) -> FsServiceBuilder<(H, H2)> {
172 FsServiceBuilder {
173 concurrent_limit: self.concurrent_limit,
174 body_limit: self.body_limit,
175 timeout: self.timeout,
176 forward: self.forward,
177
178 #[cfg(any(feature = "rustls", feature = "boring"))]
179 tls_server_config: self.tls_server_config,
180
181 http_version: self.http_version,
182
183 http_service_builder: (self.http_service_builder, layer),
184
185 content_path: self.content_path,
186 dir_serve_mode: self.dir_serve_mode,
187 html_as_default_extension: self.html_as_default_extension,
188 symlink_policy: self.symlink_policy,
189 }
190 }
191
192 rama_utils::macros::generate_set_and_with! {
193 pub fn content_path(mut self, path: impl Into<PathBuf>) -> Self {
195 self.content_path = Some(path.into());
196 self
197 }
198 }
199
200 #[must_use]
202 pub fn maybe_with_content_path<P: Into<PathBuf>>(mut self, path: Option<P>) -> Self {
203 self.content_path = path.map(Into::into);
204 self
205 }
206
207 pub fn maybe_set_content_path<P: Into<PathBuf>>(&mut self, path: Option<P>) -> &mut Self {
209 self.content_path = path.map(Into::into);
210 self
211 }
212
213 rama_utils::macros::generate_set_and_with! {
214 pub fn directory_serve_mode(mut self, mode: DirectoryServeMode) -> Self {
222 self.dir_serve_mode = mode;
223 self
224 }
225 }
226
227 rama_utils::macros::generate_set_and_with! {
228 pub fn html_as_default_extension(mut self, html_as_default_extension: bool) -> Self {
236 self.html_as_default_extension = html_as_default_extension;
237 self
238 }
239 }
240
241 rama_utils::macros::generate_set_and_with! {
242 pub fn symlink_policy(mut self, policy: ServeDirSymlinkPolicy) -> Self {
246 self.symlink_policy = policy;
247 self
248 }
249 }
250}
251
252impl<H> FsServiceBuilder<H>
253where
254 H: Layer<ServeService, Service: Service<Request, Output = Response, Error: Into<BoxError>>>,
255{
256 pub fn build(
258 self,
259 executor: Executor,
260 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
261 let tcp_forwarded_layer = match &self.forward {
262 Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
263 _ => None,
264 };
265
266 let http_service = Arc::new(self.build_http()?);
267
268 #[cfg(all(feature = "rustls", not(feature = "boring")))]
269 let tls_cfg = self.tls_server_config;
270
271 #[cfg(feature = "boring")]
272 let tls_cfg: Option<TlsAcceptorData> = match self.tls_server_config {
273 Some(cfg) => Some(cfg.try_into()?),
274 None => None,
275 };
276
277 let tcp_service_builder = (
278 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
279 LimitLayer::new(if self.concurrent_limit > 0 {
280 Either::A(ConcurrentPolicy::max(self.concurrent_limit))
281 } else {
282 Either::B(UnlimitedPolicy::new())
283 }),
284 if !self.timeout.is_zero() {
285 TimeoutLayer::new(self.timeout)
286 } else {
287 TimeoutLayer::never()
288 },
289 tcp_forwarded_layer,
290 BodyLimitLayer::request_only(self.body_limit),
291 #[cfg(any(feature = "rustls", feature = "boring"))]
292 tls_cfg.map(|cfg| {
293 #[cfg(feature = "boring")]
294 return TlsAcceptorLayer::new(cfg).with_store_client_hello(true);
295 #[cfg(all(feature = "rustls", not(feature = "boring")))]
296 TlsAcceptorLayer::new(cfg).with_store_client_hello(true)
297 }),
298 );
299
300 let http_transport_service = match self.http_version {
301 Some(Version::HTTP_2) => Either3::A(HttpServer::new_h2(executor).service(http_service)),
302 Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => {
303 Either3::B(HttpServer::new_http1(executor).service(http_service))
304 }
305 Some(version) => {
306 return Err(BoxError::from_static_str("unsupported http version")
307 .context_debug_field("version", version));
308 }
309 None => Either3::C(HttpServer::auto(executor).service(http_service)),
310 };
311
312 Ok(tcp_service_builder.into_layer(http_transport_service))
313 }
314
315 pub fn build_http(
317 &self,
318 ) -> Result<impl Service<Request, Output: IntoResponse, Error = Infallible> + use<H>, BoxError>
319 {
320 let http_forwarded_layer = super::http_forwarded_layer(self.forward.as_ref());
321
322 let serve_service = match &self.content_path {
323 None => Either3::A(IntoResponseService::new(StaticOutput::new(Html(
324 include_str!("../../../docs/index.html"),
325 )))),
326 Some(path) if path.is_file() => {
327 Either3::B(ServeFile::new(path.clone()).with_symlink_policy(self.symlink_policy))
328 }
329 Some(path) if path.is_dir() => Either3::C(
330 ServeDir::new(path)
331 .with_directory_serve_mode(self.dir_serve_mode)
332 .with_html_as_default_extension(self.html_as_default_extension)
333 .with_symlink_policy(self.symlink_policy),
334 ),
335 Some(path) => {
336 return Err(
337 BoxError::from_static_str("invalid path: no such file or directory")
338 .with_context_debug_field("path", || path.clone()),
339 );
340 }
341 };
342
343 let http_service = (
344 TraceLayer::new_for_http(),
345 SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
346 AddRequiredResponseHeadersLayer::default(),
347 UserAgentClassifierLayer::new(),
348 ConsumeErrLayer::default(),
349 http_forwarded_layer,
350 )
351 .into_layer(self.http_service_builder.layer(serve_service));
352
353 Ok(http_service)
354 }
355}
356
357type ServeStaticHtml = IntoResponseService<StaticOutput<Html<&'static str>>>;
358type ServeService = Either3<ServeStaticHtml, ServeFile, ServeDir>;