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
36core::cfg_select! {
37 feature = "boring" => {
38 use crate::tls::boring::server::TlsAcceptorLayer;
39 }
40 feature = "rustls" => {
41 use crate::tls::rustls::server::TlsAcceptorLayer;
42 }
43 _ => {}
44}
45
46#[cfg(any(feature = "boring", feature = "rustls"))]
47use crate::tls::server::TlsServerConfig;
48
49#[derive(Debug, Clone)]
50pub struct FsServiceBuilder<H> {
53 concurrent_limit: usize,
54 body_limit: usize,
55 timeout: Duration,
56 forward: Option<ForwardKind>,
57
58 #[cfg(any(feature = "rustls", feature = "boring"))]
59 tls_server_config: Option<TlsServerConfig>,
60
61 http_version: Option<Version>,
62
63 http_service_builder: H,
64
65 content_path: Option<PathBuf>,
66 dir_serve_mode: DirectoryServeMode,
67 html_as_default_extension: bool,
68 symlink_policy: ServeDirSymlinkPolicy,
69}
70
71impl Default for FsServiceBuilder<()> {
72 fn default() -> Self {
73 Self {
74 concurrent_limit: 0,
75 body_limit: mib(1),
76 timeout: Duration::ZERO,
77 forward: None,
78
79 #[cfg(any(feature = "rustls", feature = "boring"))]
80 tls_server_config: None,
81
82 http_version: None,
83
84 http_service_builder: (),
85
86 content_path: None,
87 dir_serve_mode: DirectoryServeMode::HtmlFileList,
88 html_as_default_extension: false,
89 symlink_policy: ServeDirSymlinkPolicy::default(),
90 }
91 }
92}
93
94impl FsServiceBuilder<()> {
95 #[must_use]
97 pub fn new() -> Self {
98 Self::default()
99 }
100}
101
102impl<H> FsServiceBuilder<H> {
103 rama_utils::macros::generate_set_and_with! {
104 pub fn concurrent(mut self, limit: usize) -> Self {
108 self.concurrent_limit = limit;
109 self
110 }
111 }
112
113 rama_utils::macros::generate_set_and_with! {
114 pub fn body_limit(mut self, limit: usize) -> Self {
116 self.body_limit = limit;
117 self
118 }
119 }
120
121 rama_utils::macros::generate_set_and_with! {
122 pub fn timeout(mut self, timeout: Duration) -> Self {
126 self.timeout = timeout;
127 self
128 }
129 }
130
131 rama_utils::macros::generate_set_and_with! {
132 pub fn forward(mut self, kind: Option<ForwardKind>) -> Self {
144 self.forward = kind;
145 self
146 }
147 }
148
149 #[cfg(any(feature = "rustls", feature = "boring"))]
150 rama_utils::macros::generate_set_and_with! {
151 pub fn tls_server_config(mut self, cfg: Option<TlsServerConfig>) -> Self {
154 self.tls_server_config = cfg;
155 self
156 }
157 }
158
159 rama_utils::macros::generate_set_and_with! {
160 pub fn http_version(mut self, version: Option<Version>) -> Self {
162 self.http_version = version;
163 self
164 }
165 }
166
167 #[must_use]
169 pub fn with_http_layer<H2>(self, layer: H2) -> FsServiceBuilder<(H, H2)> {
170 FsServiceBuilder {
171 concurrent_limit: self.concurrent_limit,
172 body_limit: self.body_limit,
173 timeout: self.timeout,
174 forward: self.forward,
175
176 #[cfg(any(feature = "rustls", feature = "boring"))]
177 tls_server_config: self.tls_server_config,
178
179 http_version: self.http_version,
180
181 http_service_builder: (self.http_service_builder, layer),
182
183 content_path: self.content_path,
184 dir_serve_mode: self.dir_serve_mode,
185 html_as_default_extension: self.html_as_default_extension,
186 symlink_policy: self.symlink_policy,
187 }
188 }
189
190 rama_utils::macros::generate_set_and_with! {
191 pub fn content_path(mut self, path: impl Into<PathBuf>) -> Self {
193 self.content_path = Some(path.into());
194 self
195 }
196 }
197
198 #[must_use]
200 pub fn maybe_with_content_path<P: Into<PathBuf>>(mut self, path: Option<P>) -> Self {
201 self.content_path = path.map(Into::into);
202 self
203 }
204
205 pub fn maybe_set_content_path<P: Into<PathBuf>>(&mut self, path: Option<P>) -> &mut Self {
207 self.content_path = path.map(Into::into);
208 self
209 }
210
211 rama_utils::macros::generate_set_and_with! {
212 pub fn directory_serve_mode(mut self, mode: DirectoryServeMode) -> Self {
220 self.dir_serve_mode = mode;
221 self
222 }
223 }
224
225 rama_utils::macros::generate_set_and_with! {
226 pub fn html_as_default_extension(mut self, html_as_default_extension: bool) -> Self {
234 self.html_as_default_extension = html_as_default_extension;
235 self
236 }
237 }
238
239 rama_utils::macros::generate_set_and_with! {
240 pub fn symlink_policy(mut self, policy: ServeDirSymlinkPolicy) -> Self {
244 self.symlink_policy = policy;
245 self
246 }
247 }
248}
249
250impl<H> FsServiceBuilder<H>
251where
252 H: Layer<ServeService, Service: Service<Request, Output = Response, Error: Into<BoxError>>>,
253{
254 pub fn build(
256 self,
257 executor: Executor,
258 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
259 let tcp_forwarded_layer = match &self.forward {
260 Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
261 _ => None,
262 };
263
264 let http_service = Arc::new(self.build_http()?);
265
266 let tcp_service_builder = (
267 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
268 LimitLayer::new(if self.concurrent_limit > 0 {
269 Either::A(ConcurrentPolicy::max(self.concurrent_limit))
270 } else {
271 Either::B(UnlimitedPolicy::new())
272 }),
273 if !self.timeout.is_zero() {
274 TimeoutLayer::new(self.timeout)
275 } else {
276 TimeoutLayer::never()
277 },
278 tcp_forwarded_layer,
279 BodyLimitLayer::request_only(self.body_limit),
280 #[cfg(any(feature = "rustls", feature = "boring"))]
281 self.tls_server_config
282 .map(|cfg| TlsAcceptorLayer::new(cfg).with_store_client_hello(true)),
283 );
284
285 let http_transport_service = match self.http_version {
286 Some(Version::HTTP_2) => Either3::A(HttpServer::new_h2(executor).service(http_service)),
287 Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => {
288 Either3::B(HttpServer::new_http1(executor).service(http_service))
289 }
290 Some(version) => {
291 return Err(BoxError::from_static_str("unsupported http version")
292 .context_debug_field("version", version));
293 }
294 None => Either3::C(HttpServer::auto(executor).service(http_service)),
295 };
296
297 Ok(tcp_service_builder.into_layer(http_transport_service))
298 }
299
300 pub fn build_http(
302 &self,
303 ) -> Result<impl Service<Request, Output: IntoResponse, Error = Infallible> + use<H>, BoxError>
304 {
305 let http_forwarded_layer = super::http_forwarded_layer(self.forward.as_ref());
306
307 let serve_service = match &self.content_path {
308 None => Either3::A(IntoResponseService::new(StaticOutput::new(Html(
309 include_str!("../../../docs/index.html"),
310 )))),
311 Some(path) if path.is_file() => {
312 Either3::B(ServeFile::new(path.clone()).with_symlink_policy(self.symlink_policy))
313 }
314 Some(path) if path.is_dir() => Either3::C(
315 ServeDir::new(path)
316 .with_directory_serve_mode(self.dir_serve_mode)
317 .with_html_as_default_extension(self.html_as_default_extension)
318 .with_symlink_policy(self.symlink_policy),
319 ),
320 Some(path) => {
321 return Err(
322 BoxError::from_static_str("invalid path: no such file or directory")
323 .with_context_debug_field("path", || path.clone()),
324 );
325 }
326 };
327
328 let http_service = (
329 TraceLayer::new_for_http(),
330 SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
331 AddRequiredResponseHeadersLayer::default(),
332 UserAgentClassifierLayer::new(),
333 ConsumeErrLayer::default(),
334 http_forwarded_layer,
335 )
336 .into_layer(self.http_service_builder.layer(serve_service));
337
338 Ok(http_service)
339 }
340}
341
342type ServeStaticHtml = IntoResponseService<StaticOutput<Html<&'static str>>>;
343type ServeService = Either3<ServeStaticHtml, ServeFile, ServeDir>;