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::{
25 ConsumeErrLayer, LimitLayer, TimeoutLayer,
26 limit::policy::{ConcurrentPolicy, RateLimitReached, RatePolicy},
27 },
28 net::stream::layer::{ThrottleLayer, ThrottleMode},
29 proxy::haproxy::server::HaProxyLayer,
30 rt::Executor,
31 service::StaticOutput,
32 tcp::TcpStream,
33 telemetry::tracing,
34 ua::layer::classifier::UserAgentClassifierLayer,
35 utils::{octets::mib, rate::Rate},
36};
37
38use std::{convert::Infallible, path::PathBuf, sync::Arc, time::Duration};
39
40core::cfg_select! {
41 feature = "boring" => {
42 use crate::tls::boring::server::TlsAcceptorLayer;
43 }
44 feature = "rustls" => {
45 use crate::tls::rustls::server::TlsAcceptorLayer;
46 }
47 _ => {}
48}
49
50#[cfg(any(feature = "boring", feature = "rustls"))]
51use crate::tls::server::TlsServerConfig;
52
53#[derive(Debug, Clone)]
54pub struct FsServiceBuilder<H> {
57 concurrent_limit: usize,
58 rate_limit: Option<Rate>,
59 throttle: Option<Rate>,
60 body_limit: usize,
61 timeout: Duration,
62 forward: Option<ForwardKind>,
63
64 #[cfg(any(feature = "rustls", feature = "boring"))]
65 tls_server_config: Option<TlsServerConfig>,
66
67 http_version: Option<Version>,
68
69 http_service_builder: H,
70
71 content_path: Option<PathBuf>,
72 dir_serve_mode: DirectoryServeMode,
73 html_as_default_extension: bool,
74 symlink_policy: ServeDirSymlinkPolicy,
75}
76
77impl Default for FsServiceBuilder<()> {
78 fn default() -> Self {
79 Self {
80 concurrent_limit: 0,
81 rate_limit: None,
82 throttle: None,
83 body_limit: mib(1),
84 timeout: Duration::ZERO,
85 forward: None,
86
87 #[cfg(any(feature = "rustls", feature = "boring"))]
88 tls_server_config: None,
89
90 http_version: None,
91
92 http_service_builder: (),
93
94 content_path: None,
95 dir_serve_mode: DirectoryServeMode::HtmlFileList,
96 html_as_default_extension: false,
97 symlink_policy: ServeDirSymlinkPolicy::default(),
98 }
99 }
100}
101
102impl FsServiceBuilder<()> {
103 #[must_use]
105 pub fn new() -> Self {
106 Self::default()
107 }
108}
109
110impl<H> FsServiceBuilder<H> {
111 rama_utils::macros::generate_set_and_with! {
112 pub fn concurrent(mut self, limit: usize) -> Self {
116 self.concurrent_limit = limit;
117 self
118 }
119 }
120
121 rama_utils::macros::generate_set_and_with! {
122 pub fn rate_limit(mut self, rate: Option<Rate>) -> Self {
125 self.rate_limit = rate;
126 self
127 }
128 }
129
130 rama_utils::macros::generate_set_and_with! {
131 pub fn throttle(mut self, rate: Option<Rate>) -> Self {
134 self.throttle = rate;
135 self
136 }
137 }
138
139 rama_utils::macros::generate_set_and_with! {
140 pub fn body_limit(mut self, limit: usize) -> Self {
142 self.body_limit = limit;
143 self
144 }
145 }
146
147 rama_utils::macros::generate_set_and_with! {
148 pub fn timeout(mut self, timeout: Duration) -> Self {
152 self.timeout = timeout;
153 self
154 }
155 }
156
157 rama_utils::macros::generate_set_and_with! {
158 pub fn forward(mut self, kind: Option<ForwardKind>) -> Self {
170 self.forward = kind;
171 self
172 }
173 }
174
175 #[cfg(any(feature = "rustls", feature = "boring"))]
176 rama_utils::macros::generate_set_and_with! {
177 pub fn tls_server_config(mut self, cfg: Option<TlsServerConfig>) -> Self {
180 self.tls_server_config = cfg;
181 self
182 }
183 }
184
185 rama_utils::macros::generate_set_and_with! {
186 pub fn http_version(mut self, version: Option<Version>) -> Self {
188 self.http_version = version;
189 self
190 }
191 }
192
193 #[must_use]
195 pub fn with_http_layer<H2>(self, layer: H2) -> FsServiceBuilder<(H, H2)> {
196 FsServiceBuilder {
197 concurrent_limit: self.concurrent_limit,
198 rate_limit: self.rate_limit,
199 throttle: self.throttle,
200 body_limit: self.body_limit,
201 timeout: self.timeout,
202 forward: self.forward,
203
204 #[cfg(any(feature = "rustls", feature = "boring"))]
205 tls_server_config: self.tls_server_config,
206
207 http_version: self.http_version,
208
209 http_service_builder: (self.http_service_builder, layer),
210
211 content_path: self.content_path,
212 dir_serve_mode: self.dir_serve_mode,
213 html_as_default_extension: self.html_as_default_extension,
214 symlink_policy: self.symlink_policy,
215 }
216 }
217
218 rama_utils::macros::generate_set_and_with! {
219 pub fn content_path(mut self, path: impl Into<PathBuf>) -> Self {
221 self.content_path = Some(path.into());
222 self
223 }
224 }
225
226 #[must_use]
228 pub fn maybe_with_content_path<P: Into<PathBuf>>(mut self, path: Option<P>) -> Self {
229 self.content_path = path.map(Into::into);
230 self
231 }
232
233 pub fn maybe_set_content_path<P: Into<PathBuf>>(&mut self, path: Option<P>) -> &mut Self {
235 self.content_path = path.map(Into::into);
236 self
237 }
238
239 rama_utils::macros::generate_set_and_with! {
240 pub fn directory_serve_mode(mut self, mode: DirectoryServeMode) -> Self {
248 self.dir_serve_mode = mode;
249 self
250 }
251 }
252
253 rama_utils::macros::generate_set_and_with! {
254 pub fn html_as_default_extension(mut self, html_as_default_extension: bool) -> Self {
262 self.html_as_default_extension = html_as_default_extension;
263 self
264 }
265 }
266
267 rama_utils::macros::generate_set_and_with! {
268 pub fn symlink_policy(mut self, policy: ServeDirSymlinkPolicy) -> Self {
272 self.symlink_policy = policy;
273 self
274 }
275 }
276}
277
278impl<H> FsServiceBuilder<H>
279where
280 H: Layer<ServeService, Service: Service<Request, Output = Response, Error: Into<BoxError>>>,
281{
282 pub fn build(
284 self,
285 executor: Executor,
286 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
287 let tcp_forwarded_layer = match &self.forward {
288 Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
289 _ => None,
290 };
291
292 let http_service = Arc::new(self.build_http()?);
293
294 let tcp_service_builder = (
295 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
296 LimitLayer::new(if self.concurrent_limit > 0 {
297 Either::A(ConcurrentPolicy::max(self.concurrent_limit))
298 } else {
299 Either::B(UnlimitedPolicy::new())
300 }),
301 if !self.timeout.is_zero() {
302 TimeoutLayer::new(self.timeout)
303 } else {
304 TimeoutLayer::never()
305 },
306 self.throttle
307 .map(|rate| ThrottleLayer::symmetric(ThrottleMode::per_conn(rate))),
308 tcp_forwarded_layer,
309 BodyLimitLayer::request_only(self.body_limit),
310 #[cfg(any(feature = "rustls", feature = "boring"))]
311 self.tls_server_config
312 .map(|cfg| TlsAcceptorLayer::new(cfg).with_store_client_hello(true)),
313 );
314
315 let http_transport_service = match self.http_version {
316 Some(Version::HTTP_2) => Either3::A(HttpServer::new_h2(executor).service(http_service)),
317 Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => {
318 Either3::B(HttpServer::new_http1(executor).service(http_service))
319 }
320 Some(version) => {
321 return Err(BoxError::from_static_str("unsupported http version")
322 .context_debug_field("version", version));
323 }
324 None => Either3::C(HttpServer::auto(executor).service(http_service)),
325 };
326
327 Ok(tcp_service_builder.into_layer(http_transport_service))
328 }
329
330 pub fn build_http(
332 &self,
333 ) -> Result<impl Service<Request, Output: IntoResponse, Error = Infallible> + use<H>, BoxError>
334 {
335 let http_forwarded_layer = super::http_forwarded_layer(self.forward.as_ref());
336
337 let serve_service = match &self.content_path {
338 None => Either3::A(IntoResponseService::new(StaticOutput::new(Html(
339 include_str!("../../../docs/index.html"),
340 )))),
341 Some(path) if path.is_file() => {
342 Either3::B(ServeFile::new(path.clone()).with_symlink_policy(self.symlink_policy))
343 }
344 Some(path) if path.is_dir() => Either3::C(
345 ServeDir::new(path)
346 .with_directory_serve_mode(self.dir_serve_mode)
347 .with_html_as_default_extension(self.html_as_default_extension)
348 .with_symlink_policy(self.symlink_policy),
349 ),
350 Some(path) => {
351 return Err(
352 BoxError::from_static_str("invalid path: no such file or directory")
353 .with_context_debug_field("path", || path.clone()),
354 );
355 }
356 };
357
358 let http_service = (
359 TraceLayer::new_for_http(),
360 SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
361 AddRequiredResponseHeadersLayer::default(),
362 self.rate_limit.map(|rate| {
363 LimitLayer::new(RatePolicy::abort(rate)).with_error_into_response_fn(
364 |err: RateLimitReached| Ok::<_, Infallible>(err.into_response()),
365 )
366 }),
367 UserAgentClassifierLayer::new(),
368 ConsumeErrLayer::default(),
369 http_forwarded_layer,
370 )
371 .into_layer(self.http_service_builder.layer(serve_service));
372
373 Ok(http_service)
374 }
375}
376
377type ServeStaticHtml = IntoResponseService<StaticOutput<Html<&'static str>>>;
378type ServeService = Either3<ServeStaticHtml, ServeFile, ServeDir>;