rama/cli/service/
fs.rs

1//! [`Service`] that serves a file or directory using [`ServeFile`] or [`ServeDir`], or a placeholder page.
2
3use crate::{
4    Layer, Service,
5    cli::ForwardKind,
6    combinators::Either,
7    combinators::{Either3, Either7},
8    error::{BoxError, OpaqueError},
9    http::{
10        Request, Response, Version,
11        headers::exotic::XClacksOverhead,
12        headers::forwarded::{CFConnectingIp, ClientIp, TrueClientIp, XClientIp, XRealIp},
13        layer::set_header::SetResponseHeaderLayer,
14        layer::{
15            forwarded::GetForwardedHeaderLayer, required_header::AddRequiredResponseHeadersLayer,
16            trace::TraceLayer,
17        },
18        server::HttpServer,
19        service::{
20            fs::{DirectoryServeMode, ServeDir, ServeFile},
21            web::StaticService,
22            web::response::{Html, IntoResponse},
23        },
24    },
25    layer::limit::policy::UnlimitedPolicy,
26    layer::{ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::ConcurrentPolicy},
27    net::stream::layer::http::BodyLimitLayer,
28    proxy::haproxy::server::HaProxyLayer,
29    rt::Executor,
30    tcp::TcpStream,
31    telemetry::tracing,
32    ua::layer::classifier::UserAgentClassifierLayer,
33};
34
35use std::{convert::Infallible, path::PathBuf, time::Duration};
36
37#[cfg(feature = "boring")]
38use crate::{
39    net::tls::server::ServerConfig,
40    tls::boring::server::{TlsAcceptorData, TlsAcceptorLayer},
41};
42
43#[cfg(all(feature = "rustls", not(feature = "boring")))]
44use crate::tls::rustls::server::{TlsAcceptorData, TlsAcceptorLayer};
45
46#[cfg(feature = "boring")]
47type TlsConfig = ServerConfig;
48
49#[cfg(all(feature = "rustls", not(feature = "boring")))]
50type TlsConfig = TlsAcceptorData;
51
52#[derive(Debug, Clone)]
53/// Builder that can be used to run your own serve [`Service`],
54/// serving a file or directory, or a placeholder page.
55pub struct FsServiceBuilder<H> {
56    concurrent_limit: usize,
57    body_limit: usize,
58    timeout: Duration,
59    forward: Option<ForwardKind>,
60
61    #[cfg(any(feature = "rustls", feature = "boring"))]
62    tls_server_config: Option<TlsConfig>,
63
64    http_version: Option<Version>,
65
66    http_service_builder: H,
67
68    content_path: Option<PathBuf>,
69    dir_serve_mode: DirectoryServeMode,
70}
71
72impl Default for FsServiceBuilder<()> {
73    fn default() -> Self {
74        Self {
75            concurrent_limit: 0,
76            body_limit: 1024 * 1024,
77            timeout: Duration::ZERO,
78            forward: None,
79
80            #[cfg(any(feature = "rustls", feature = "boring"))]
81            tls_server_config: None,
82
83            http_version: None,
84
85            http_service_builder: (),
86
87            content_path: None,
88            dir_serve_mode: DirectoryServeMode::HtmlFileList,
89        }
90    }
91}
92
93impl FsServiceBuilder<()> {
94    /// Create a new [`FsServiceBuilder`].
95    #[must_use]
96    pub fn new() -> Self {
97        Self::default()
98    }
99}
100
101impl<H> FsServiceBuilder<H> {
102    rama_utils::macros::generate_set_and_with! {
103        /// set the number of concurrent connections to allow
104        ///
105        /// (0 = no limit)
106        pub fn concurrent(mut self, limit: usize) -> Self {
107            self.concurrent_limit = limit;
108            self
109        }
110    }
111
112    rama_utils::macros::generate_set_and_with! {
113        /// set the body limit in bytes for each request
114        pub fn body_limit(mut self, limit: usize) -> Self {
115            self.body_limit = limit;
116            self
117        }
118    }
119
120    rama_utils::macros::generate_set_and_with! {
121        /// set the timeout in seconds for each connection
122        ///
123        /// (0 = no timeout)
124        pub fn timeout(mut self, timeout: Duration) -> Self {
125            self.timeout = timeout;
126            self
127        }
128    }
129
130    rama_utils::macros::generate_set_and_with! {
131        /// enable support for one of the following "forward" headers or protocols
132        ///
133        /// Supported headers:
134        ///
135        /// Forwarded ("for="), X-Forwarded-For
136        ///
137        /// X-Client-IP Client-IP, X-Real-IP
138        ///
139        /// CF-Connecting-IP, True-Client-IP
140        ///
141        /// Or using HaProxy protocol.
142        pub fn forward(mut self, kind: Option<ForwardKind>) -> Self {
143            self.forward = kind;
144            self
145        }
146    }
147
148    #[cfg(any(feature = "rustls", feature = "boring"))]
149    rama_utils::macros::generate_set_and_with! {
150        /// define a tls server cert config to be used for tls terminaton
151        /// by the serve service.
152        pub fn tls_server_config(mut self, cfg: Option<TlsConfig>) -> Self {
153            self.tls_server_config = cfg;
154            self
155        }
156    }
157
158    rama_utils::macros::generate_set_and_with! {
159        /// set the http version to use for the http server (auto by default)
160        pub fn http_version(mut self, version: Option<Version>) -> Self {
161            self.http_version = version;
162            self
163        }
164    }
165
166    /// add a custom http layer which will be applied to the existing http layers
167    #[must_use]
168    pub fn with_http_layer<H2>(self, layer: H2) -> FsServiceBuilder<(H, H2)> {
169        FsServiceBuilder {
170            concurrent_limit: self.concurrent_limit,
171            body_limit: self.body_limit,
172            timeout: self.timeout,
173            forward: self.forward,
174
175            #[cfg(any(feature = "rustls", feature = "boring"))]
176            tls_server_config: self.tls_server_config,
177
178            http_version: self.http_version,
179
180            http_service_builder: (self.http_service_builder, layer),
181
182            content_path: self.content_path,
183            dir_serve_mode: self.dir_serve_mode,
184        }
185    }
186
187    rama_utils::macros::generate_set_and_with! {
188        /// Set the content path to serve (by default it will serve the rama homepage).
189        pub fn content_path(mut self, path: impl Into<PathBuf>) -> Self {
190            self.content_path = Some(path.into());
191            self
192        }
193    }
194
195    /// Maybe set the content path to serve (by default it will serve the rama homepage).
196    #[must_use]
197    pub fn maybe_with_content_path<P: Into<PathBuf>>(mut self, path: Option<P>) -> Self {
198        self.content_path = path.map(Into::into);
199        self
200    }
201
202    /// Maybe set the content path to serve (by default it will serve the rama homepage).
203    pub fn maybe_set_content_path<P: Into<PathBuf>>(&mut self, path: Option<P>) -> &mut Self {
204        self.content_path = path.map(Into::into);
205        self
206    }
207
208    rama_utils::macros::generate_set_and_with! {
209        /// Set the [`DirectoryServeMode`] which defines how to serve directories.
210        ///
211        /// By default it will use [`DirectoryServeMode::HtmlFileList`].
212        ///
213        /// Note that this is only used in case the content path is defined
214        /// (e.g. using [`Self::content_path`])
215        /// and that path points to a valid directory.
216        pub fn directory_serve_mode(mut self, mode: DirectoryServeMode) -> Self {
217            self.dir_serve_mode = mode;
218            self
219        }
220    }
221}
222
223impl<H> FsServiceBuilder<H>
224where
225    H: Layer<ServeService, Service: Service<Request, Response = Response, Error: Into<BoxError>>>,
226{
227    /// build a tcp service ready to serve files
228    pub fn build(
229        self,
230        executor: Executor,
231    ) -> Result<impl Service<TcpStream, Response = (), Error = Infallible>, BoxError> {
232        let tcp_forwarded_layer = match &self.forward {
233            Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
234            _ => None,
235        };
236
237        let http_service = self.build_http()?;
238
239        #[cfg(all(feature = "rustls", not(feature = "boring")))]
240        let tls_cfg = self.tls_server_config;
241
242        #[cfg(feature = "boring")]
243        let tls_cfg: Option<TlsAcceptorData> = match self.tls_server_config {
244            Some(cfg) => Some(cfg.try_into()?),
245            None => None,
246        };
247
248        let tcp_service_builder = (
249            ConsumeErrLayer::trace(tracing::Level::DEBUG),
250            LimitLayer::new(if self.concurrent_limit > 0 {
251                Either::A(ConcurrentPolicy::max(self.concurrent_limit))
252            } else {
253                Either::B(UnlimitedPolicy::new())
254            }),
255            if !self.timeout.is_zero() {
256                TimeoutLayer::new(self.timeout)
257            } else {
258                TimeoutLayer::never()
259            },
260            tcp_forwarded_layer,
261            BodyLimitLayer::request_only(self.body_limit),
262            #[cfg(any(feature = "rustls", feature = "boring"))]
263            tls_cfg.map(|cfg| {
264                #[cfg(feature = "boring")]
265                return TlsAcceptorLayer::new(cfg).with_store_client_hello(true);
266                #[cfg(all(feature = "rustls", not(feature = "boring")))]
267                TlsAcceptorLayer::new(cfg).with_store_client_hello(true)
268            }),
269        );
270
271        let http_transport_service = match self.http_version {
272            Some(Version::HTTP_2) => Either3::A(HttpServer::h2(executor).service(http_service)),
273            Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => {
274                Either3::B(HttpServer::http1().service(http_service))
275            }
276            Some(_) => {
277                return Err(OpaqueError::from_display("unsupported http version").into_boxed());
278            }
279            None => Either3::C(HttpServer::auto(executor).service(http_service)),
280        };
281
282        Ok(tcp_service_builder.into_layer(http_transport_service))
283    }
284
285    /// build an http service ready to serve files
286    pub fn build_http(
287        &self,
288    ) -> Result<impl Service<Request, Response: IntoResponse, Error = Infallible> + use<H>, BoxError>
289    {
290        let http_forwarded_layer = match &self.forward {
291            None | Some(ForwardKind::HaProxy) => None,
292            Some(ForwardKind::Forwarded) => Some(Either7::A(GetForwardedHeaderLayer::forwarded())),
293            Some(ForwardKind::XForwardedFor) => {
294                Some(Either7::B(GetForwardedHeaderLayer::x_forwarded_for()))
295            }
296            Some(ForwardKind::XClientIp) => {
297                Some(Either7::C(GetForwardedHeaderLayer::<XClientIp>::new()))
298            }
299            Some(ForwardKind::ClientIp) => {
300                Some(Either7::D(GetForwardedHeaderLayer::<ClientIp>::new()))
301            }
302            Some(ForwardKind::XRealIp) => {
303                Some(Either7::E(GetForwardedHeaderLayer::<XRealIp>::new()))
304            }
305            Some(ForwardKind::CFConnectingIp) => {
306                Some(Either7::F(GetForwardedHeaderLayer::<CFConnectingIp>::new()))
307            }
308            Some(ForwardKind::TrueClientIp) => {
309                Some(Either7::G(GetForwardedHeaderLayer::<TrueClientIp>::new()))
310            }
311        };
312
313        let serve_service = match &self.content_path {
314            None => Either3::A(StaticService::new(Html(include_str!(
315                "../../../docs/index.html"
316            )))),
317            Some(path) if path.is_file() => Either3::B(ServeFile::new(path.clone())),
318            Some(path) if path.is_dir() => {
319                Either3::C(ServeDir::new(path).with_directory_serve_mode(self.dir_serve_mode))
320            }
321            Some(path) => {
322                return Err(OpaqueError::from_display(format!(
323                    "invalid path {path:?}: no such file or directory"
324                ))
325                .into_boxed());
326            }
327        };
328
329        let http_service = (
330            TraceLayer::new_for_http(),
331            SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
332            AddRequiredResponseHeadersLayer::default(),
333            UserAgentClassifierLayer::new(),
334            ConsumeErrLayer::default(),
335            http_forwarded_layer,
336        )
337            .into_layer(self.http_service_builder.layer(serve_service));
338
339        Ok(http_service)
340    }
341}
342
343type ServeStaticHtml = StaticService<Html<&'static str>>;
344type ServeService = Either3<ServeStaticHtml, ServeFile, ServeDir>;