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