Skip to main content

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, 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)]
52/// Builder that can be used to run your own serve [`Service`],
53/// serving a file or directory, or a placeholder page.
54pub 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    /// Create a new [`FsServiceBuilder`].
98    #[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        /// set the number of concurrent connections to allow
107        ///
108        /// (0 = no limit)
109        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        /// set the body limit in bytes for each request
117        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        /// set the timeout in seconds for each connection
125        ///
126        /// (0 = no timeout)
127        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        /// enable support for one of the following "forward" headers or protocols
135        ///
136        /// Supported headers:
137        ///
138        /// Forwarded ("for="), X-Forwarded-For
139        ///
140        /// X-Client-IP Client-IP, X-Real-IP
141        ///
142        /// CF-Connecting-IP, True-Client-IP
143        ///
144        /// Or using HaProxy protocol.
145        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        /// define a tls server cert config to be used for tls terminaton
154        /// by the serve service.
155        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        /// set the http version to use for the http server (auto by default)
163        pub fn http_version(mut self, version: Option<Version>) -> Self {
164            self.http_version = version;
165            self
166        }
167    }
168
169    /// add a custom http layer which will be applied to the existing http layers
170    #[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        /// Set the content path to serve (by default it will serve the rama homepage).
194        pub fn content_path(mut self, path: impl Into<PathBuf>) -> Self {
195            self.content_path = Some(path.into());
196            self
197        }
198    }
199
200    /// Maybe set the content path to serve (by default it will serve the rama homepage).
201    #[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    /// Maybe set the content path to serve (by default it will serve the rama homepage).
208    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        /// Set the [`DirectoryServeMode`] which defines how to serve directories.
215        ///
216        /// By default it will use [`DirectoryServeMode::HtmlFileList`].
217        ///
218        /// Note that this is only used in case the content path is defined
219        /// (e.g. using [`Self::content_path`])
220        /// and that path points to a valid directory.
221        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        /// If true, requests for a path without a file extension that
229        /// doesn't resolve to anything will be retried with `.html` appended
230        /// (e.g. `/about` will serve `/about.html`).
231        ///
232        /// Only takes effect when the content path points to a directory.
233        ///
234        /// Defaults to `false`.
235        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        /// Set the [`ServeDirSymlinkPolicy`] used when serving a file or directory.
243        ///
244        /// Defaults to [`ServeDirSymlinkPolicy::RejectAll`].
245        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    /// build a tcp service ready to serve files
257    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    /// build an http service ready to serve files
316    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>;